/* ============================================================
   Sitemaster Design System — Web
   Ported from /Users/powerpac/Documents/Powerpac/SitemasterMobile/
   Source of truth: SitemasterMobile/DESIGN-SYSTEM.md

   Additive only. All utilities are namespaced .ds-*.
   Load via: <link rel="stylesheet" href="/css/design-system.css" />
   ============================================================ */

@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700;800&display=swap');

/* =====================================================
   TOKENS
   ===================================================== */

:root {
  /* Brand */
  --ds-color-brand:          #C63527;
  --ds-color-brand-hover:    #A82B1F;
  --ds-color-brand-tint:     rgba(198, 53, 39, 0.08);

  /* Overlays (mobile signature: navy, not pure black) */
  --ds-overlay-primary:      rgba(0, 38, 62, 0.75);
  --ds-overlay-nested:       rgba(0, 0, 0, 0.15);

  /* Text
     Four readable greys do not fit between #1A202C and white. To clear WCAG
     AA (4.5:1) on both white and #F8F9FA a grey has to be at least as dark as
     ~#5F6E80, which is a handful of RGB points off tertiary — so `muted` is
     now only marginally lighter than `tertiary` rather than a distinct tier.
     That is the honest outcome, not an oversight: when tertiary was darkened
     from mobile's #718096, muted was left behind at #A0AEC0 and measured
     2.26:1, which is below even the 3:1 large-text floor. It had been
     carrying every search placeholder and several 10-11px labels.

     Use --ds-text-faint ONLY for non-text: em-dash placeholders standing in
     for "no value", decorative icon fills, hairline dividers. Never for
     words a user has to read. */
  --ds-text-primary:         #1A202C;
  --ds-text-secondary:       #4A5568;
  --ds-text-tertiary:        #5B6B7C; /* 5.47:1 on white, 5.18:1 on #F8F9FA */
  --ds-text-muted:           #5F6E80; /* 5.21:1 on white, 4.94:1 on #F8F9FA */
  --ds-text-faint:           #A0AEC0; /* 2.26:1 — NON-TEXT ONLY */
  --ds-text-on-brand:        #FFFFFF;

  /* Structural navy (site header / sidebar / "happening now" chip) */
  --ds-color-navy:           #00263E;

  /* Backgrounds */
  --ds-bg-screen:            #F8F9FA;
  --ds-bg-card:              #FFFFFF;
  --ds-bg-subtle:            #F7FAFC;

  /* Borders */
  --ds-border-default:       #E2E8F0;
  /* One step darker, for the few things that have to read as an OBJECT on a
     white card rather than a division between two. #E2E8F0 is right for a
     card edge and invisible around a 20px status chip — reported as "no
     border on the status badges", and measuring it confirmed it: the border
     was there and could not be seen. Same neutral ramp, next stop up. */
  --ds-border-object:        #CBD5E0;
  --ds-border-light:         #F0F0F0;

  /* Status — bright values are for DOTS/ICONS only (non-text, no contrast requirement).
     When status color is applied to TEXT, use the *-text variants (WCAG AA at small sizes). */
  --ds-status-success:       #10B981;
  --ds-status-warning:       #F59E0B;
  --ds-status-neutral:       #718096;
  --ds-status-info:          #3B82F6;
  --ds-status-violet:        #8B5CF6;
  --ds-status-success-text:  #187A4A;
  --ds-status-warning-text:  #8F6200;
  --ds-status-danger-text:   #C0392B;
  --ds-status-info-text:     #0B5CAD;
  --ds-status-violet-text:   #6D5AC4;

  /* Typography — ONE family for the whole app.
     What actually renders is MONTSERRAT, loaded by the @import at the top of
     this file. 'Futura PT' and 'Futura' are kept after it to record the brand
     intent (Futura PT is the face the mobile app and the Powerpac website
     use), but no @font-face declares them and no font files ship in this
     repo, so they only ever resolve for a user who happens to have Futura
     installed locally — and even then Montserrat wins, because it is first.
     Switching the app to real Futura needs licensed woff2 files plus an
     @font-face block; reordering this stack alone would do nothing.

     There is no monospace family. Five stylesheets had grown ad-hoc
     `ui-monospace` stacks for machine identifiers (container numbers, ticket
     numbers, quote numbers, SKUs) while the customers and print-pipeline
     tables set the same kind of data in Montserrat — so a reference number
     looked like two different things depending on the page. Digit alignment
     that the monospace was providing now comes from
     `font-variant-numeric: tabular-nums`, which is what it is for. */
  --ds-font-family:          'Montserrat', 'Futura PT', 'Futura', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;

  --ds-font-size-h1:         2.25rem;   /* 36px */
  --ds-font-size-h2:         1.75rem;   /* 28px */
  --ds-font-size-h3:         1.5rem;    /* 24px */
  --ds-font-size-h4:         1.25rem;   /* 20px */
  --ds-font-size-page:       1.375rem;  /* 22px — list-page <h1>. Between h4
                                           and h2: a page title on a dense
                                           table page should not compete with
                                           the table, but still has to read as
                                           the top of the hierarchy. */
  --ds-font-size-body:       1rem;      /* 16px */
  --ds-font-size-small:      0.875rem;  /* 14px */
  --ds-font-size-dense:      0.8125rem; /* 13px — tables, toolbars, popover rows.
                                           The size dense data UI across this app
                                           already uses; named here so the shell
                                           stops guessing at it. */
  --ds-font-size-label:      0.75rem;   /* 12px */
  --ds-font-size-micro:      0.6875rem; /* 11px — uppercase table headers and
                                           eyebrow labels ONLY. Letter-spaced
                                           all-caps reads larger than its
                                           nominal size; do not use it for
                                           sentence-case body copy. This is the
                                           floor — nothing in the app should be
                                           smaller. */

  --ds-font-weight-regular:  400;
  --ds-font-weight-medium:   500;
  --ds-font-weight-semibold: 600;
  --ds-font-weight-bold:     700;
  --ds-font-weight-heavy:    800;

  /* Spacing — 4pt scale */
  --ds-space-xs:             4px;
  --ds-space-sm:             8px;
  --ds-space-md:             12px;
  --ds-space-base:           16px;
  --ds-space-lg:             24px;
  --ds-space-xl:             32px;
  --ds-space-2xl:            48px;
  --ds-space-3xl:            64px;

  /* Radius — the whole scale. Anything not on it (2, 3, 4, 6, 9, 10px) is
     drift: at these sizes the differences are invisible individually and only
     add up to an inconsistent feel. */
  --ds-radius-xs:            4px;       /* Chips, meters, inline pills */
  --ds-radius-sm:            8px;       /* Secondary buttons, small cards */
  --ds-radius-md:            12px;      /* Primary buttons, primary cards */
  --ds-radius-lg:            20px;      /* Bottom sheets, large surfaces */
  --ds-radius-pill:          999px;     /* Fully rounded: meter tracks, avatars */
  --ds-radius-chip:          6px;       /* Status chips ONLY. Off the 4/8/12
                                           scale on purpose — the locked chip
                                           spec in .impeccable.md fixes it at
                                           6px, so it gets a name rather than
                                           looking like drift. */

  /* Skeleton shimmer. One definition — the same three-stop gradient had been
     copy-pasted into eight stylesheets (containers, contact-details,
     customer-details, major-projects, major-project-details,
     powerpac-customers, ticket-inbox, dashboard-2), each with its own
     @keyframes beside it. Pages that predate .ds-skel below can at least share
     the values by using this token plus the ds-shimmer keyframes. */
  --ds-skel-gradient:        linear-gradient(90deg, #edf1f5 0%, #f7fafc 50%, #edf1f5 100%);

  /* Elevation */
  --ds-shadow-card:          0 2px 4px rgba(0, 0, 0, 0.08);
  --ds-shadow-card-hover:    0 4px 8px rgba(0, 0, 0, 0.08);
  --ds-shadow-modal:         0 12px 20px rgba(0, 0, 0, 0.25);

  /* Control heights (matches mobile button spec) */
  --ds-h-btn-primary:        56px;
  --ds-h-btn-tertiary:       52px;
  --ds-h-btn-inline:         40px;
  --ds-h-input:              44px;

  /* Motion */
  --ds-transition-fast:      0.15s ease;
  --ds-transition-base:      0.2s ease;
}

/* =====================================================
   TYPOGRAPHY UTILITIES
   ===================================================== */

.ds-h1 {
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-h1);
  font-weight: var(--ds-font-weight-bold);
  letter-spacing: -0.02em;
  line-height: 1.1;
  color: var(--ds-text-primary);
  margin: 0;
}

.ds-h2 {
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-h2);
  font-weight: var(--ds-font-weight-bold);
  letter-spacing: -0.01em;
  line-height: 1.15;
  color: var(--ds-text-primary);
  margin: 0;
}

.ds-h3 {
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-h3);
  font-weight: var(--ds-font-weight-bold);
  line-height: 1.2;
  color: var(--ds-text-primary);
  margin: 0;
}

.ds-h4 {
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-h4);
  font-weight: var(--ds-font-weight-semibold);
  line-height: 1.3;
  color: var(--ds-text-primary);
  margin: 0;
}

.ds-body {
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-body);
  font-weight: var(--ds-font-weight-regular);
  line-height: 1.5;
  color: var(--ds-text-secondary);
}

.ds-body-medium {
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-body);
  font-weight: var(--ds-font-weight-medium);
  line-height: 1.5;
  color: var(--ds-text-primary);
}

.ds-body-bold {
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-body);
  font-weight: var(--ds-font-weight-semibold);
  line-height: 1.5;
  color: var(--ds-text-primary);
}

.ds-small {
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-small);
  font-weight: var(--ds-font-weight-regular);
  line-height: 1.4;
  color: var(--ds-text-secondary);
}

.ds-small-bold {
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-small);
  font-weight: var(--ds-font-weight-semibold);
  line-height: 1.4;
  color: var(--ds-text-primary);
}

/* Signature pattern from mobile:
   12px bold UPPERCASE letter-spacing 0.8px tertiary-text-color.
   Use above every logical content group to echo the mobile feel. */
/* -----------------------------------------------------
   THE FONT, SAID ONCE

   forum.css is the vendor theme underneath all of this and it sets Poppins
   on p, td, li and friends. Any component here that sets a size and a
   weight but leaves the family to inheritance therefore renders in Poppins
   on some pages and Montserrat on others, depending on whether that page's
   own stylesheet happened to set a family on its content wrapper. Two did;
   the rest did not. The Tasks page's eyebrow was Poppins while the title
   above it was Montserrat, which is exactly the sort of thing you can see
   and cannot name.

   So every design-system component says it, rather than 36 rules each
   remembering to. Scoped to the ds- prefix on purpose: legacy markup keeps
   whatever forum.css gives it, because element selectors beat inheritance
   and nothing outside this file changes.
   ----------------------------------------------------- */

[class^="ds-"]:not(.material-icons),
[class*=" ds-"]:not(.material-icons) {
  font-family: var(--ds-font-family);
}

/* The exclusion above is not cosmetic. .ds-sort-icon is worn BY a Material
   Icons element — <i class="material-icons ds-sort-icon">arrow_upward</i>,
   21 of them across the table headers — and Material Icons draws its glyph
   from a ligature in its own font. Set Montserrat on that element and the
   browser has no ligature to draw, so it renders the ligature's NAME: the
   sort arrows on Customers, Containers and the Print Pipeline turned into
   the words "arrow_upward". Any future component worn by an icon needs the
   same treatment. */

.ds-section-label {
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-label);
  font-weight: var(--ds-font-weight-bold);
  color: var(--ds-text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  line-height: 1;
  margin: 0 0 var(--ds-space-md);
}

/* =====================================================
   BUTTONS
   ===================================================== */

.ds-btn {
  font-family: var(--ds-font-family);
  font-weight: var(--ds-font-weight-bold);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--ds-space-sm);
  transition: background var(--ds-transition-fast), border-color var(--ds-transition-fast), color var(--ds-transition-fast);
  -webkit-appearance: none;
  appearance: none;
  text-decoration: none;
  white-space: nowrap;
}

.ds-btn:focus-visible {
  outline: 2px solid var(--ds-color-brand);
  outline-offset: 2px;
}

.ds-btn:disabled,
.ds-btn.is-disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

/* Primary (filled red) — Save, Submit, primary CTAs */
.ds-btn-primary {
  height: var(--ds-h-btn-primary);
  padding: 0 var(--ds-space-lg);
  border-radius: var(--ds-radius-md);
  background: var(--ds-color-brand);
  color: var(--ds-text-on-brand);
  border: none;
  font-size: 1.125rem;
}
.ds-btn-primary:hover:not(:disabled) {
  background: var(--ds-color-brand-hover);
}

/* Secondary (red outline) — Add Note, secondary CTAs */
.ds-btn-secondary {
  height: var(--ds-h-btn-primary);
  padding: 0 var(--ds-space-lg);
  border-radius: var(--ds-radius-sm);
  background: var(--ds-bg-card);
  color: var(--ds-color-brand);
  border: 1px solid var(--ds-color-brand);
  font-size: var(--ds-font-size-body);
}
.ds-btn-secondary:hover:not(:disabled) {
  background: var(--ds-color-brand-tint);
}

/* Tertiary (neutral) — Cancel, Details, low-emphasis actions */
.ds-btn-tertiary {
  height: var(--ds-h-btn-tertiary);
  padding: 0 var(--ds-space-base);
  border-radius: var(--ds-radius-md);
  background: var(--ds-bg-subtle);
  color: var(--ds-text-secondary);
  border: 1.5px solid var(--ds-border-default);
  font-weight: var(--ds-font-weight-medium);
  font-size: var(--ds-font-size-small);
}
.ds-btn-tertiary:hover:not(:disabled) {
  background: var(--ds-bg-screen);
  border-color: var(--ds-text-tertiary);
}

/* Inline (compact) — for dense toolbars */
.ds-btn-inline {
  height: var(--ds-h-btn-inline);
  padding: 0 var(--ds-space-md);
  border-radius: var(--ds-radius-sm);
  background: var(--ds-bg-card);
  color: var(--ds-text-secondary);
  border: 1px solid var(--ds-border-default);
  font-weight: var(--ds-font-weight-medium);
  font-size: var(--ds-font-size-small);
}
.ds-btn-inline:hover:not(:disabled) {
  border-color: var(--ds-color-brand);
  color: var(--ds-color-brand);
}

/* Circular icon button — small */
.ds-btn-icon {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: var(--ds-bg-subtle);
  border: 1px solid var(--ds-border-default);
  color: var(--ds-text-tertiary);
  padding: 0;
}
.ds-btn-icon:hover:not(:disabled) {
  border-color: var(--ds-color-brand);
  color: var(--ds-color-brand);
}

/* =====================================================
   CARDS
   ===================================================== */

.ds-card {
  background: var(--ds-bg-card);
  border: 1px solid var(--ds-border-default);
  border-radius: var(--ds-radius-md);
  padding: var(--ds-space-lg);
  box-shadow: var(--ds-shadow-card);
}

.ds-card-subtle {
  background: var(--ds-bg-subtle);
  border: 1px solid var(--ds-border-default);
  border-radius: var(--ds-radius-sm);
  padding: var(--ds-space-md);
  box-shadow: none;
}

.ds-card-flat {
  background: var(--ds-bg-card);
  border: 1px solid var(--ds-border-default);
  border-radius: var(--ds-radius-md);
  padding: var(--ds-space-lg);
  box-shadow: none;
}

/* =====================================================
   MODAL OVERLAY
   Navy is the signature mobile overlay. Use nested for
   modals that open on top of an existing overlay.
   ===================================================== */

.ds-modal-overlay {
  position: fixed;
  inset: 0;
  background: var(--ds-overlay-primary);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1000;
}

.ds-modal-overlay-nested {
  position: fixed;
  inset: 0;
  background: var(--ds-overlay-nested);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1100;
}

.ds-modal-panel {
  background: var(--ds-bg-card);
  border-radius: var(--ds-radius-md);
  box-shadow: var(--ds-shadow-modal);
  max-width: 520px;
  width: calc(100% - var(--ds-space-xl));
  max-height: calc(100vh - var(--ds-space-2xl));
  overflow-y: auto;
}

/* =====================================================
   STATUS DOTS — 6px inline (matches tasks/tickets pattern)
   Anti-pattern per mobile doc: do NOT use colored left-borders
   on cards. Use these inline dots instead.
   ===================================================== */

.ds-status-dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  vertical-align: middle;
  flex-shrink: 0;
  margin-right: var(--ds-space-xs);
  background: var(--ds-status-neutral);
}
.ds-status-dot.is-success  { background: var(--ds-status-success); }
.ds-status-dot.is-warning  { background: var(--ds-status-warning); }
.ds-status-dot.is-info     { background: var(--ds-status-info); }
.ds-status-dot.is-violet   { background: var(--ds-status-violet); }
.ds-status-dot.is-brand    { background: var(--ds-color-brand); }

/* =====================================================
   STATUS CHIPS — canonical badge treatment (Jul 2026,
   approved via the rebuild concept deck).
   Softly tinted chip carrying its own status hue, with
   text dark enough to read on it. Superseded the original
   outlined-chip-plus-6px-dot rule on 24 Aug 2026: the dot
   was too small to read across a table, which is where
   these actually get used.
   Hue map (app-wide): success = done/positive ·
   warning = needs attention · info = in-motion ·
   violet = AI-generated · danger dot = negative outcome.
   Only two solid fills exist:
     .ds-chip-critical (brand red)  — overdue / delayed
     .ds-chip-now      (navy)      — happening right now
   ===================================================== */

.ds-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--ds-border-object);
  background: var(--ds-bg-card);
  color: var(--ds-text-secondary);
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-label);
  font-weight: var(--ds-font-weight-semibold);
  line-height: 1;
  padding: 4px 9px;
  border-radius: var(--ds-radius-chip);
  white-space: nowrap;
}
/* TINTED, NOT DOTTED.  (Changed 24 Aug 2026, at Marcel's direction.)

   The original rule above said the opposite: outlined chip, neutral text, a
   6px dot carrying the meaning. In practice the dot was a 6px target for a
   distinction people needed to read at a glance across a whole table — the
   quotes list is 37 rows of "Quote Submitted" in three different states, and
   telling them apart meant looking at six pixels. The tint carries the same
   information at the size of the whole chip.

   Kept from the old rule: the text is dark enough to read on its tint rather
   than being the same hue at the same weight, and the two solid fills below
   are still the only solid fills. */
.ds-chip.is-success { background: #E7F6EE; border-color: #C3E7D4; color: var(--ds-status-success-text); }
.ds-chip.is-warning { background: #FDF3E0; border-color: #F3DFB4; color: var(--ds-status-warning-text); }
.ds-chip.is-info    { background: #E8F1FC; border-color: #C4DBF5; color: var(--ds-status-info-text); }
.ds-chip.is-violet  { background: #F0ECFB; border-color: #D8CDF3; color: var(--ds-status-violet-text); }
.ds-chip.is-danger  { background: #FCEBE9; border-color: #F3CBC6; color: var(--ds-status-danger-text); }
/* Neutral by design: "nothing to say about this one". Transparent rather than
   grey-on-grey, so a table of mostly-neutral rows stays quiet. */
.ds-chip.is-muted   { background: transparent; border-color: var(--ds-border-default); color: var(--ds-text-tertiary); }

.ds-chip-critical {
  background: var(--ds-color-brand);
  border-color: var(--ds-color-brand);
  color: var(--ds-text-on-brand);
  font-weight: var(--ds-font-weight-bold);
}
.ds-chip-now {
  background: var(--ds-color-navy);
  border-color: var(--ds-color-navy);
  color: var(--ds-text-on-brand);
  font-weight: var(--ds-font-weight-bold);
}

/* -----------------------------------------------------
   CALL OUTCOME  —  how a call went, said the same way everywhere

   Glyph + word, not a coloured pill. Four states, and each one is
   distinguishable without colour: a tick, a clock, a cross and a
   filled circle. Red and green are the pair most colour-blind
   readers cannot separate, and "cancelled" and "successful" are
   exactly the two that must never be confused, so the shape does
   the work and the hue only reinforces it.

   Markup comes from public/js/ds-outcome.js — do not hand-write it,
   or the pages drift apart again, which is why this exists.
   ----------------------------------------------------- */

.ds-outcome {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-label);
  font-weight: var(--ds-font-weight-semibold);
  line-height: 1;
  white-space: nowrap;
  color: var(--ds-text-tertiary);
}
/* 15px, not the icon font's 24px default. Optically it sits a hair low
   against a cap-height label, hence the nudge. */
.ds-outcome > .material-icons {
  font-size: 15px;
  line-height: 1;
  position: relative;
  top: -0.5px;
}
/* The no-show circle is a solid disc where the others are outlined glyphs,
   so it reads heavier at the same size. */
.ds-outcome.is-no-show > .material-icons { font-size: 11px; }

.ds-outcome.is-successful  { color: var(--ds-status-success-text); }
.ds-outcome.is-rescheduled { color: var(--ds-status-danger-text); }
.ds-outcome.is-cancelled   { color: var(--ds-status-danger-text); }
.ds-outcome.is-no-show     { color: var(--ds-status-warning-text); }
/* Not an outcome — a call that has not happened yet. Deliberately the
   quietest of the set: it is a statement about the diary, not a result. */
.ds-outcome.is-scheduled   { color: var(--ds-text-tertiary); }
.ds-outcome.is-unknown     { color: var(--ds-text-tertiary); text-transform: capitalize; }

/* Glyph only, for a column with no room for the word. The word is still
   carried for a screen reader and on hover. */
.ds-outcome.is-compact { gap: 0; }

/* -----------------------------------------------------
   SPLIT CHIP  —  label │ value  (+ optional dismiss)

   Two segments divided by a hairline: a quiet label and the value that
   matters. Use where a single chip has to say two things and one of them
   is context — an active filter ("Sub Category │ Formwork Contractor"),
   a status with a qualifier ("Quote Submitted │ Expired"), a place
   ("Auckland │ North Auckland").

   Follows the locked chip rules: outlined, neutral text, colour carried
   only by the 6px dot (add is-success / is-warning / is-info / is-danger
   to .ds-split-value). Deliberately NOT offered with a solid fill —
   solids stay reserved for brand-red overdue/delayed and navy now.

   Markup:
     <span class="ds-chip-split">
       <span class="ds-split-label">Sub Category</span>
       <span class="ds-split-value">Formwork Contractor</span>
       <button type="button" class="ds-split-dismiss" aria-label="Remove">×</button>
     </span>
   ----------------------------------------------------- */

.ds-chip-split {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--ds-border-default);
  background: var(--ds-bg-card);
  border-radius: var(--ds-radius-pill);
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-label);
  line-height: 1;
  overflow: hidden;
  max-width: 100%;
}

.ds-chip-split > * {
  display: inline-flex;
  align-items: center;
  padding: 5px 10px;
  min-width: 0;
}

.ds-split-label {
  color: var(--ds-text-tertiary);
  font-weight: var(--ds-font-weight-semibold);
  white-space: nowrap;
}

.ds-split-value {
  color: var(--ds-text-primary);
  font-weight: var(--ds-font-weight-bold);
  gap: 6px;
  border-left: 1px solid var(--ds-border-default);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  display: inline-block;
  padding-top: 6px;
  padding-bottom: 6px;
}

/* Status dot, same vocabulary as .ds-chip */
.ds-split-value.is-success::before,
.ds-split-value.is-warning::before,
.ds-split-value.is-info::before,
.ds-split-value.is-violet::before,
.ds-split-value.is-danger::before {
  content: "";
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  margin-right: 6px;
  vertical-align: middle;
  position: relative;
  top: -1px;
}
.ds-split-value.is-success::before { background: var(--ds-status-success); }
.ds-split-value.is-warning::before { background: var(--ds-status-warning); }
.ds-split-value.is-info::before    { background: var(--ds-status-info); }
.ds-split-value.is-violet::before  { background: var(--ds-status-violet); }
.ds-split-value.is-danger::before  { background: var(--ds-color-brand); }

.ds-split-dismiss {
  border: none;
  border-left: 1px solid var(--ds-border-default);
  background: none;
  color: var(--ds-text-tertiary);
  font-family: inherit;
  font-size: var(--ds-font-size-small);
  line-height: 1;
  cursor: pointer;
  padding: 5px 9px;
  transition: background var(--ds-transition-fast), color var(--ds-transition-fast);
}
.ds-split-dismiss:hover {
  background: var(--ds-color-brand);
  color: var(--ds-text-on-brand);
}
.ds-split-dismiss:focus-visible {
  outline: 2px solid var(--ds-color-brand);
  outline-offset: -2px;
}

/* =====================================================
   FORM INPUTS
   ===================================================== */

.ds-input {
  height: var(--ds-h-input);
  width: 100%;
  padding: 0 var(--ds-space-md);
  border: 1px solid var(--ds-border-default);
  border-radius: var(--ds-radius-sm);
  background: var(--ds-bg-card);
  color: var(--ds-text-primary);
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-body);
  transition: border-color var(--ds-transition-fast), box-shadow var(--ds-transition-fast);
  -webkit-appearance: none;
  appearance: none;
}
/* A SEARCH BOX, NOT A FORM FIELD.

   .ds-input is 44px tall with 16px text because it is built for forms, where
   a field is the thing you have come to fill in. A search box sitting in a
   card head or a toolbar is not that — it is a control beside a heading, and
   at form size it shouted over the heading it belongs to and stood a head
   taller than the same box in the sidebar.

   These are the sidebar search's own metrics, so the two read as the same
   control in two places rather than two controls. Light-themed, because the
   sidebar's is dark for the sidebar's reasons. */
.ds-input.ds-input-search {
  height: 34px;
  border-radius: 9px;
  font-size: var(--ds-font-size-dense);
  font-weight: var(--ds-font-weight-medium);
}
.ds-input::placeholder { color: var(--ds-text-muted); }
.ds-input:focus {
  outline: none;
  border-color: var(--ds-color-brand);
  box-shadow: 0 0 0 3px var(--ds-color-brand-tint);
}

/* Search field with a leading icon.
   The WRAPPER carries the border and the focus ring; the input inside is
   borderless. Deliberately not an icon absolutely positioned over a .ds-input
   — that needs a matching left-padding override on the input, and the moment
   the two drift the icon lands on top of the placeholder text. Promoted here
   from the containers page (.cnt-search-group), which had it right; that page
   can adopt this class whenever it's next touched.

   Usage:
     <div class="ds-search">
       <i class="material-icons">search</i>
       <input type="text" placeholder="…" aria-label="…" />
     </div> */
.ds-search {
  display: flex;
  align-items: center;
  gap: var(--ds-space-sm);
  border: 1px solid var(--ds-border-default);
  border-radius: var(--ds-radius-sm);
  background: var(--ds-bg-card);
  padding: 6px 10px;
  min-width: 200px;
}
.ds-search .material-icons {
  font-size: 16px;
  color: var(--ds-text-faint);
  flex-shrink: 0;
}
.ds-search input {
  flex: 1;
  min-width: 0;
  border: none;
  outline: none;
  background: transparent;
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-dense);
  color: var(--ds-text-primary);
}
.ds-search input::placeholder { color: var(--ds-text-muted); }
.ds-search:focus-within {
  border-color: var(--ds-color-brand);
  box-shadow: 0 0 0 3px var(--ds-color-brand-tint);
}

/* Segmented control — a small set of mutually exclusive VIEWS, not filters.
   Visually heavier than .ds-chip on purpose: a chip narrows what you're
   looking at, a segment changes whose data it is. Active segment uses the
   navy fill, consistent with .ds-chip-now; brand red stays reserved for
   overdue/delayed. */
.ds-segmented {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--ds-border-default);
  border-radius: var(--ds-radius-sm);
  background: var(--ds-bg-card);
  overflow: hidden;
}
.ds-segmented > button {
  appearance: none;
  border: none;
  background: transparent;
  cursor: pointer;
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-label);
  font-weight: var(--ds-font-weight-semibold);
  color: var(--ds-text-secondary);
  padding: 8px 16px;
  line-height: 1;
  transition: background var(--ds-transition-fast), color var(--ds-transition-fast);

  /* Flex, so an icon inside centres against the text instead of sitting on
     its baseline. "My contacts" and "My customers" both carry one, and with
     line-height: 1 the glyph hung low and pressed against the M with no gap.
     The gap is here rather than as a margin on the icon so a button with no
     icon is unaffected. */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
}
.ds-segmented > button + button { border-left: 1px solid var(--ds-border-default); }
.ds-segmented > button:hover:not([aria-pressed="true"]) { background: var(--ds-bg-subtle); }
.ds-segmented > button[aria-pressed="true"] {
  /* Brand red, matching the customers page which had this first. The
     "red = overdue" rule in the locked decisions governs STATUS CHIPS;
     red-as-selected on a control is a different thing and the established
     precedent here. */
  background: var(--ds-color-brand);
  color: var(--ds-text-on-brand);
  font-weight: var(--ds-font-weight-bold);
}
.ds-segmented > button .material-icons {
  font-size: 14px;
  /* Without this the icon's own 1em line box is taller than the 14px glyph
     and adds two pixels to the button's height. */
  line-height: 1;
}
.ds-segmented > button:focus-visible {
  outline: 2px solid var(--ds-color-brand);
  outline-offset: -2px;
}

/**
 * Stands on its own.
 *
 * This used to be a MODIFIER — min-height, padding, resize, line-height —
 * that quietly assumed .ds-input was on the element as well. Four of the five
 * call sites did not know that, so the call planner's "What happened" box and
 * the drawer's notes had no border, no radius, the wrong font, and the
 * browser's default BLUE focus ring on a page where every other field glows
 * brand red.
 *
 * A class that only works next to another class is a trap. This carries the
 * whole field now, and still behaves when both are used together: height:auto
 * beats .ds-input's fixed 44px, whichever order they appear in.
 */
.ds-textarea {
  width: 100%;
  height: auto;
  min-height: 96px;
  padding: var(--ds-space-md);
  border: 1px solid var(--ds-border-default);
  border-radius: var(--ds-radius-sm);
  background: var(--ds-bg-card);
  color: var(--ds-text-primary);
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-body);
  line-height: 1.5;
  resize: vertical;
  transition: border-color var(--ds-transition-fast), box-shadow var(--ds-transition-fast);
  -webkit-appearance: none;
  appearance: none;
}
.ds-textarea::placeholder { color: var(--ds-text-muted); }
.ds-textarea:focus {
  outline: none;
  border-color: var(--ds-color-brand);
  box-shadow: 0 0 0 3px var(--ds-color-brand-tint);
}

/* =====================================================
   LAYOUT PRIMITIVES
   ===================================================== */

.ds-page {
  background: var(--ds-bg-screen);
  min-height: 100vh;
  color: var(--ds-text-primary);
  font-family: var(--ds-font-family);
}

.ds-stack-xs > * + * { margin-top: var(--ds-space-xs); }
.ds-stack-sm > * + * { margin-top: var(--ds-space-sm); }
.ds-stack-md > * + * { margin-top: var(--ds-space-md); }
.ds-stack-lg > * + * { margin-top: var(--ds-space-lg); }
.ds-stack-xl > * + * { margin-top: var(--ds-space-xl); }

.ds-row {
  display: flex;
  align-items: center;
  gap: var(--ds-space-md);
}
.ds-row-sm { gap: var(--ds-space-sm); }
.ds-row-lg { gap: var(--ds-space-lg); }
.ds-row-between { justify-content: space-between; }

/* =====================================================
   TEXT COLOR UTILITIES
   ===================================================== */

.ds-text-primary   { color: var(--ds-text-primary); }
.ds-text-secondary { color: var(--ds-text-secondary); }
.ds-text-tertiary  { color: var(--ds-text-tertiary); }
.ds-text-muted     { color: var(--ds-text-muted); }
.ds-text-brand     { color: var(--ds-color-brand); }
.ds-text-success   { color: var(--ds-status-success-text); }
.ds-text-warning   { color: var(--ds-status-warning-text); }
.ds-text-danger    { color: var(--ds-status-danger-text); }

/* =====================================================
   CTA CASE — Powerpac website button style (uppercase,
   letter-spaced, like the shop's ADD TO CART). Apply to
   primary/secondary CTAs on rebuilt pages.
   ===================================================== */

.ds-btn-caps {
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-size: var(--ds-font-size-label);
}

/* =====================================================
   PAGE SHELL
   -----------------------------------------------------
   Header, filter bar, popover and table furniture shared by the list pages
   (customers, containers, print pipeline).

   Extracted Jul 2026. Before this, each of those three pages had rebuilt the
   same eight things under its own prefix — 152 page-local classes between
   them against a design system almost none of them imported. The shapes here
   are taken from the customers page, which had them closest to right.

   Page CSS should now hold only what is genuinely specific to that page
   (column widths, bespoke cell renderers), never another copy of this.
   ===================================================== */

/* Toolbar button — header actions on list pages (Export, Sync, Refresh).
   Smaller and quieter than .ds-btn-primary, which is sized for form CTAs.
   Customers and containers had defined this identically as .pc-btn and
   .cnt-btn; this is that shape, once. */
.ds-btn-toolbar {
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-dense);
  font-weight: var(--ds-font-weight-semibold);
  padding: 8px 14px;
  border-radius: var(--ds-radius-sm);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  border: 1px solid var(--ds-border-default);
  background: var(--ds-bg-card);
  color: var(--ds-text-secondary);
  transition: background var(--ds-transition-fast), border-color var(--ds-transition-fast), color var(--ds-transition-fast);
}
.ds-btn-toolbar:hover:not(:disabled) {
  border-color: var(--ds-color-brand);
  color: var(--ds-color-brand);
}
.ds-btn-toolbar:disabled { opacity: 0.6; cursor: not-allowed; }
.ds-btn-toolbar:focus-visible {
  outline: 2px solid var(--ds-color-brand);
  outline-offset: 2px;
}
.ds-btn-toolbar .material-icons { font-size: 16px; }

/* A select sized for the header row rather than for a form.
   .ds-input is 44px and .ds-btn-primary is 56px; both are right where they
   belong and wrong side by side with a 37px toolbar button. Customers has
   had the correct look for months (search + toolbar button, both ~36px) and
   this is the missing third piece so a page with a dropdown up there can
   match it. */
.ds-select-toolbar {
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-dense);
  font-weight: var(--ds-font-weight-semibold);
  height: 37px;
  padding: 0 30px 0 12px;
  border-radius: var(--ds-radius-sm);
  border: 1px solid var(--ds-border-default);
  background: var(--ds-bg-card);
  color: var(--ds-text-secondary);
  cursor: pointer;
  -webkit-appearance: none;
  appearance: none;
  /* The native arrow disappears with appearance:none, so it is drawn back
     in as a background image rather than left to a wrapper element. */
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%235B6B7C' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 11px center;
  transition: border-color var(--ds-transition-fast), color var(--ds-transition-fast);
}
.ds-select-toolbar:hover { border-color: var(--ds-color-brand); }
.ds-select-toolbar:focus-visible {
  outline: 2px solid var(--ds-color-brand);
  outline-offset: 2px;
}

.ds-btn-toolbar-primary {
  background: var(--ds-color-brand);
  border-color: var(--ds-color-brand);
  color: var(--ds-text-on-brand);
}
.ds-btn-toolbar-primary:hover:not(:disabled) {
  background: var(--ds-color-brand-hover);
  border-color: var(--ds-color-brand-hover);
  color: var(--ds-text-on-brand);
}

.ds-spin { animation: ds-spin 1s linear infinite; }
@keyframes ds-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .ds-spin { animation: none; } }

/* ─── Page header ─── */

.ds-page-header { margin-bottom: var(--ds-space-base); }

.ds-page-header-row {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--ds-space-base);
  flex-wrap: wrap;
}

.ds-eyebrow {
  font-size: var(--ds-font-size-micro);
  font-weight: var(--ds-font-weight-bold);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ds-text-tertiary);
  margin: 0 0 var(--ds-space-xs);
}

.ds-page-title {
  font-size: var(--ds-font-size-page);
  font-weight: 800;
  color: var(--ds-text-primary);
  margin: 0;
  letter-spacing: -0.01em;
  font-family: var(--ds-font-family);
}

/* One quiet line under the title for counts and totals. Deliberately not a
   row of metric cards: four identical big-number tiles is the most
   recognisable dashboard cliché there is, and the numbers are nearly always
   restating a filter that sits directly below them. */
.ds-page-sub {
  font-size: var(--ds-font-size-dense);
  color: var(--ds-text-secondary);
  margin: 5px 0 0;
  font-variant-numeric: tabular-nums;
}

.ds-header-actions {
  display: flex;
  gap: var(--ds-space-sm);
  align-items: center;
  flex-wrap: wrap;
}

/* ─── Filter bar ─── */

.ds-filter-bar {
  display: flex;
  gap: var(--ds-space-sm);
  align-items: center;
  margin-bottom: 14px;
  flex-wrap: wrap;
}

.ds-filter-divider {
  width: 1px;
  height: 20px;
  background: var(--ds-border-default);
  margin: 0 2px;
  flex-shrink: 0;
}

/* A cluster of related filter dimensions. Buttons sit closer to each other
   than to the next group, so eight dimensions read as four things rather than
   eight — proximity doing the chunking that a flat row could not. */
.ds-filter-group {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-xs);
  flex-wrap: wrap;
}

/* Dropdown trigger. Reads as its own label until something is chosen, then
   as the choice — so the bar states the current filters rather than needing
   them read off a separate summary. */
.ds-filter-btn {
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-label);
  font-weight: var(--ds-font-weight-medium);
  padding: 6px 10px;
  /* Computed height was ~28px against a DS that defines
     --ds-h-btn-inline: 40px. Rather than inflate the whole bar on desktop —
     eight of these in a row, density is the point — the compact size stays
     for fine pointers and coarse pointers get a real target. See the
     pointer: coarse block at the end of this file. */
  min-height: 32px;
  border: 1px solid var(--ds-border-default);
  border-radius: var(--ds-radius-sm);
  background: var(--ds-bg-card);
  color: var(--ds-text-secondary);
  cursor: pointer;
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  transition: border-color var(--ds-transition-fast), color var(--ds-transition-fast);
}
.ds-filter-btn:hover { border-color: var(--ds-color-brand); }
.ds-filter-btn:focus-visible {
  outline: 2px solid var(--ds-color-brand);
  outline-offset: 1px;
}
.ds-filter-btn.is-active {
  border-color: var(--ds-color-brand);
  background: var(--ds-color-brand-tint);
  color: var(--ds-color-brand);
  font-weight: var(--ds-font-weight-semibold);
}

.ds-clear {
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-label);
  font-weight: var(--ds-font-weight-medium);
  padding: 6px 10px;
  background: transparent;
  border: 1px solid transparent;
  color: var(--ds-text-tertiary);
  cursor: pointer;
  border-radius: var(--ds-radius-sm);
}
.ds-clear:hover {
  color: var(--ds-color-brand);
  background: var(--ds-bg-subtle);
}
.ds-clear:focus-visible {
  outline: 2px solid var(--ds-color-brand);
  outline-offset: 2px;
}

/* Every selected value as its own removable chip, so a selection can be
   undone without reopening the popover to hunt for it. */
.ds-active-filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ds-space-sm);
  margin: var(--ds-space-md) 0 0;
}
.ds-active-filters[hidden] { display: none; }

/* ─── Filter popover ─── */

.ds-popover {
  position: absolute;
  z-index: 1000;
  min-width: 230px;
  max-width: 320px;
  max-height: 340px;
  overflow-y: auto;
  background: var(--ds-bg-card);
  border: 1px solid var(--ds-border-default);
  border-radius: var(--ds-radius-md);
  box-shadow: var(--ds-shadow-modal, 0 12px 20px rgba(0, 0, 0, 0.18));
  padding: 10px;
}
.ds-popover-title {
  margin: 2px 6px 8px;
  font-size: var(--ds-font-size-micro);
  font-weight: var(--ds-font-weight-bold);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ds-text-tertiary);
}
.ds-popover-empty {
  margin: 4px 6px 6px;
  font-size: var(--ds-font-size-dense);
  color: var(--ds-text-tertiary);
}
.ds-popover-options { display: flex; flex-direction: column; }
.ds-popover-options label {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 8px;
  cursor: pointer;
  border-radius: var(--ds-radius-sm);
  font-size: var(--ds-font-size-dense);
  font-family: var(--ds-font-family);
  color: var(--ds-text-secondary);
  transition: background var(--ds-transition-fast);
  margin: 0;
}
.ds-popover-options label:hover { background: var(--ds-bg-subtle); }
.ds-popover-options input[type="checkbox"] {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
  accent-color: var(--ds-color-brand);
}
.ds-popover-options input[type="checkbox"]:focus-visible {
  outline: 2px solid var(--ds-color-brand);
  outline-offset: 1px;
}


/* A person, chosen from a popover. Written for the call planner's rep
   switcher; the Tasks page needed the same control the week after, which is
   the moment it stops belonging to one page. */
.ds-repopt {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  text-align: left;
  appearance: none;
  border: 0;
  border-radius: var(--ds-radius-sm);
  background: none;
  padding: 6px var(--ds-space-sm);
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-dense);
  color: var(--ds-text-primary);
  cursor: pointer;
}
.ds-repopt:hover { background: var(--ds-bg-subtle); }
.ds-repopt:focus-visible { outline: 2px solid var(--ds-color-brand); outline-offset: -2px; }
/* Whose list you are already looking at. */
.ds-repopt[aria-current="true"] {
  background: var(--ds-bg-subtle);
  font-weight: var(--ds-font-weight-bold);
}
.ds-repopt .ds-avatar { flex: 0 0 auto; }
.ds-repopt > span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* A hairline between "my list" and the list of colleagues. */
.ds-repopt-sep {
  margin: 8px 4px;
  border: 0;
  border-top: 1px solid var(--ds-border-light);
}

/* ─── Table ─── */

.ds-table-wrap {
  background: var(--ds-bg-card);
  border: 1px solid var(--ds-border-default);
  border-radius: var(--ds-radius-md);
  overflow: hidden;
}
/* Wide tables scroll inside their own wrapper rather than the page. */
.ds-table-wrap.is-scrollable { overflow-x: auto; }

.ds-table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--ds-font-size-dense);
}
.ds-table thead th {
  background: var(--ds-bg-subtle);
  text-align: left;
  padding: 0;
  border-bottom: 1px solid var(--ds-border-default);
  white-space: nowrap;
}
/* Non-sortable headers still need their padding, since sortable ones get it
   from the button inside. */
.ds-table thead th.ds-th-plain {
  padding: 11px 14px;
  font-size: var(--ds-font-size-micro);
  font-weight: var(--ds-font-weight-bold);
  color: var(--ds-text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.08em;
}
.ds-table tbody td {
  padding: 10px 14px;
  border-bottom: 1px solid var(--ds-border-light);
  color: var(--ds-text-secondary);
  vertical-align: middle;
  line-height: 1.35;
}
.ds-table tbody tr { transition: background 0.12s; }
.ds-table tbody tr:hover { background: var(--ds-bg-subtle); }
.ds-table tbody tr:last-child td { border-bottom: none; }
.ds-table tbody tr.is-clickable { cursor: pointer; }

.ds-num { text-align: right; font-variant-numeric: tabular-nums; }

/* ─── SWITCH ────────────────────────────────────────────────────────────────
   A setting that is ON or OFF, and shows which without being read.

   The app had no such control, so a row that could be turned off grew a
   button labelled "Turn off" — which states the ACTION, leaving the reader to
   infer the current state backwards from it. Down a column of them that is a
   small piece of arithmetic per row. A switch states the state.

   Built on a real checkbox so it is focusable, announced, and toggles with
   the keyboard for free; the input is hidden visually but not from the
   accessibility tree. Put the label text in a <span> beside it, or use
   aria-label when the row already says what it is. */
.ds-switch {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  cursor: pointer;
  user-select: none;
}

.ds-switch input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
  margin: -1px;
}

.ds-switch-track {
  position: relative;
  flex: 0 0 auto;
  width: 36px;
  height: 20px;
  border-radius: 10px;
  background: var(--ds-border-default, #e2e8f0);
  transition: background 0.16s ease;
}

.ds-switch-track::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
  transition: transform 0.16s ease;
}

.ds-switch input:checked + .ds-switch-track {
  background: var(--ds-color-brand, #c63527);
}

.ds-switch input:checked + .ds-switch-track::after {
  transform: translateX(16px);
}

/* Focus goes on the track, since the input it belongs to is invisible. */
.ds-switch input:focus-visible + .ds-switch-track {
  outline: 2px solid var(--ds-color-brand, #c63527);
  outline-offset: 2px;
}

.ds-switch input:disabled + .ds-switch-track {
  opacity: 0.5;
  cursor: default;
}

.ds-switch-label {
  font-size: var(--ds-font-size-dense, 0.8125rem);
  color: var(--ds-text-tertiary, #5b6b7c);
}

/* Sorting lives in a real <button> inside the <th> rather than a click
   handler on the cell — it's focusable and announced without extra ARIA. */
.ds-sort-btn {
  display: flex;
  align-items: center;
  gap: 4px;
  width: 100%;
  padding: 11px 14px;
  background: transparent;
  border: none;
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-micro);
  font-weight: var(--ds-font-weight-bold);
  color: var(--ds-text-tertiary);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  text-align: left;
  cursor: pointer;
  transition: background var(--ds-transition-fast), color var(--ds-transition-fast);
}
.ds-sort-btn:hover {
  background: var(--ds-bg-screen);
  color: var(--ds-text-secondary);
}
.ds-sort-btn:focus-visible {
  outline: 2px solid var(--ds-color-brand);
  outline-offset: -2px;
  color: var(--ds-color-brand);
}
.ds-sort-icon {
  font-size: 14px;
  opacity: 0;
  transition: opacity var(--ds-transition-fast);
}
.ds-table th[aria-sort="ascending"] .ds-sort-btn,
.ds-table th[aria-sort="descending"] .ds-sort-btn { color: var(--ds-text-primary); }
.ds-table th[aria-sort="ascending"] .ds-sort-icon,
.ds-table th[aria-sort="descending"] .ds-sort-icon { opacity: 1; }
.ds-table th.ds-num .ds-sort-btn { justify-content: flex-end; }

/* ─── Empty / error / loading state rows ─────────────────────────────────
   Promoted Jul 2026 from .pc-state-* on the customers page, which was the
   fullest of the three implementations. Containers had a near-identical
   .cnt-state-* copy; print pipeline had bare text in a <td> with no icon, no
   title and no recovery action, which meant the page most likely to fail —
   it polls every 60s — had the weakest error state of the three.

   Specificity is high enough to beat `.ds-table tbody td` on its own, so the
   padding no longer needs !important.

   Markup:
     <tr class="ds-state-row"><td colspan="N">
       <i class="material-icons" aria-hidden="true">search_off</i>
       <p class="ds-state-title">No customers found</p>
       <p class="ds-state-msg">No customers match the current filters.</p>
       <button type="button" class="ds-state-action">Retry</button>
     </td></tr>                                                            */

.ds-table tbody tr.ds-state-row td,
.ds-state-row td {
  padding: 48px 24px;
  text-align: center;
  background: var(--ds-bg-card);
  border: none;
  color: var(--ds-text-tertiary);
}

.ds-state-row .material-icons {
  font-size: 32px;
  /* Non-text: a decorative glyph beside a title that already says it. */
  color: var(--ds-text-faint);
  display: block;
  margin: 0 auto var(--ds-space-sm);
}
/* Failure, not emptiness. Uses the AA-safe danger text token rather than a
   hard-coded red — an off-palette #dc2626 had drifted in here. */
.ds-state-row.is-error .material-icons { color: var(--ds-status-danger-text); }

/* display: block, so a caller that reaches for <span> instead of the <p> in
   the contract above still gets two lines. Inline, the title and the message
   ran together into "Nobody matches thatTry a wider date range" — the margins
   below do nothing on an inline box either, so there was not even a space
   between them. Both Contacts and Users build these with spans. */
.ds-state-title {
  display: block;
  font-size: var(--ds-font-size-small);
  font-weight: var(--ds-font-weight-semibold);
  color: var(--ds-text-secondary);
  margin: 0 0 var(--ds-space-xs);
}

.ds-state-msg {
  display: block;
  font-size: var(--ds-font-size-dense);
  color: var(--ds-text-tertiary);
  margin: 0 0 var(--ds-space-md);
  /* A sentence naming four filter conditions needs a measure. */
  max-width: 56ch;
  margin-left: auto;
  margin-right: auto;
  line-height: 1.55;
}

.ds-state-action {
  background: var(--ds-color-brand);
  color: var(--ds-text-on-brand);
  border: 1px solid var(--ds-color-brand);
  padding: 7px 14px;
  border-radius: var(--ds-radius-sm);
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-label);
  font-weight: var(--ds-font-weight-semibold);
  cursor: pointer;
}
.ds-state-action:focus-visible {
  outline: 2px solid var(--ds-color-brand);
  outline-offset: 2px;
}
.ds-state-action.is-secondary {
  background: var(--ds-bg-card);
  color: var(--ds-text-secondary);
  border-color: var(--ds-border-default);
}
.ds-state-action + .ds-state-action { margin-left: var(--ds-space-sm); }

/* ─── Meter ──────────────────────────────────────────────────────────────
   One horizontal bar for the whole app. Replaces .pc-target-bar,
   .cd-target-bar and .pp-progress-bar, which were three near-identical
   copies (54×5px, pill track, tabular-nums 11px value) that had already
   drifted apart in the two ways that matter:

   1. GREEN MEANT TWO THINGS. On print pipeline green meant "some steps are
      done"; on customers it meant "meeting or beating target". Adjacent
      pages, same pixels, different rule — so a rep learns the wrong one.
      Resolved by splitting the two ideas:

        .ds-meter            attainment against a target. Judged, so it
                             carries colour: behind / on track / met.
        .ds-meter-steps      position in a sequence. NOT judged — 4 of 9
                             steps done is neither good nor bad — so it is
                             always neutral and never green.

      Green now means exactly one thing app-wide: a target is being met.

   2. A MET TARGET RENDERED DIFFERENTLY on the list and the detail page,
      because the list also filled the TRACK green. Dropped: at 100% the
      fill covers the track anyway, so the override only ever showed up as
      an inconsistency.

   Also fixes a latent clipping bug in all three originals: the pace notch
   was 9px tall inside a 5px track with `overflow: hidden`, so it rendered
   flush and 4px shorter than intended. The track now clips the fill only;
   the notch sits in an unclipped positioned wrapper.

   Width is a variable because the customers/pipeline tables want a fixed
   54px column and the customer-details grid wants to fill its cell.

   Markup — percentages ride in as custom properties, so no inline
   `width:` on a child and no second nested span:

     <span class="ds-meter is-behind"
           style="--ds-meter-fill: 38%; --ds-meter-pace: 61%"
           title="$19k of $50k (FY2027), behind">
       <span class="ds-meter-bar">
         <span class="ds-meter-track"></span>
         <i class="ds-meter-pace" aria-hidden="true"></i>
       </span>
       <em>38%<span class="ds-sr-only">, behind</span></em>
     </span>

   The state word in .ds-sr-only is not optional. Without it the difference
   between "behind" and "on track" is carried by hue alone, which a screen
   reader cannot announce and a red-green colourblind user cannot see.     */

.ds-meter {
  display: flex;
  align-items: center;
  gap: var(--ds-space-sm);
  white-space: nowrap;
  /* Neutral until a state class says otherwise. */
  --ds-meter-color: var(--ds-status-neutral);
}

.ds-meter-bar {
  position: relative;
  display: block;
  width: var(--ds-meter-width, 54px);
  flex-shrink: 0;
}

.ds-meter-track {
  display: block;
  height: 5px;
  border-radius: var(--ds-radius-pill);
  background: var(--ds-border-default);
  overflow: hidden;
}

.ds-meter-track::before {
  content: "";
  display: block;
  height: 100%;
  width: var(--ds-meter-fill, 0%);
  border-radius: var(--ds-radius-pill);
  background: var(--ds-meter-color);
}

/* Where the customer should be by now. Annual only: part-way through a
   calendar month is not something a rep can act on. */
.ds-meter-pace {
  position: absolute;
  top: -2px;
  left: var(--ds-meter-pace, 0%);
  width: 1px;
  height: 9px;
  background: var(--ds-text-tertiary);
  opacity: 0.55;
}

/* Attainment states. Success is reserved for "target met or beaten". */
.ds-meter.is-behind   { --ds-meter-color: var(--ds-status-danger-text); }
.ds-meter.is-ontrack,
.ds-meter.is-done     { --ds-meter-color: var(--ds-status-success); }
/* Under target with no pace marker to judge against — in progress, not
   failing. Colouring a part-finished month red put 98% with two days left
   into alarm red. */
.ds-meter.is-progress { --ds-meter-color: var(--ds-status-neutral); }

/* Sequence progress. Deliberately has no success state. */
.ds-meter-steps { --ds-meter-color: var(--ds-status-neutral); }

.ds-meter > em {
  font-style: normal;
  font-size: var(--ds-font-size-micro);
  color: var(--ds-text-tertiary);
  font-variant-numeric: tabular-nums;
}
.ds-meter.is-behind > em { color: var(--ds-status-danger-text); }
.ds-meter.is-ontrack > em,
.ds-meter.is-done > em   { color: var(--ds-status-success-text); }

/* ─── Screen-reader-only text ─── */

.ds-sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* =====================================================
   TOUCH & NARROW VIEWPORTS
   -----------------------------------------------------
   The shell is built for a dense desktop table, which is where most of the
   time is spent. But per the design context these pages are also opened by
   reps on phones in vehicles and warehouses, and the shell previously had no
   rules for either case: `design-system.css` carried a single media query
   (prefers-reduced-motion on .ds-spin) and nothing else.
   ===================================================== */

/* Coarse pointer = finger. Compact 28-32px controls are fine for a mouse and
   too small for a thumb, so the targets grow only where a thumb is actually
   being used. Keeping this on pointer type rather than viewport width means a
   touchscreen laptop gets big targets and a narrow desktop window does not. */
@media (pointer: coarse) {
  .ds-filter-btn,
  .ds-clear,
  .ds-btn-toolbar {
    min-height: 44px;
    padding-top: var(--ds-space-sm);
    padding-bottom: var(--ds-space-sm);
  }
  .ds-segmented > button { min-height: 44px; }
  .ds-popover-options label { padding: var(--ds-space-md) var(--ds-space-sm); }
  .ds-split-dismiss { min-width: 36px; justify-content: center; }
  /* Rows are the primary tap target on a list page. */
  .ds-table tbody td { padding-top: 14px; padding-bottom: 14px; }
}

@media (max-width: 768px) {
  /* Header actions drop under the title rather than squeezing it. */
  .ds-page-header-row {
    flex-direction: column;
    align-items: stretch;
    gap: var(--ds-space-md);
  }
  .ds-header-actions { flex-wrap: wrap; }
  .ds-search { min-width: 0; flex: 1 1 100%; }

  /* A wrapped row of filter buttons needs its own breathing room, otherwise
     two rows of chips read as one block. */
  .ds-filter-bar { gap: var(--ds-space-sm) var(--ds-space-xs); }
  /* Dividers are meaningless once the bar wraps to three lines. */
  .ds-filter-divider { display: none; }

  .ds-popover {
    max-width: calc(100vw - 24px);
    min-width: 0;
  }

  /* Wide tables scroll horizontally on phones. Give the scroll a visible
     edge so it is discoverable rather than something you find by accident. */
  .ds-table-wrap.is-scrollable {
    -webkit-overflow-scrolling: touch;
    background-image: linear-gradient(to right, rgba(0, 38, 62, 0.06), transparent 12px);
    background-repeat: no-repeat;
    background-attachment: local;
  }
}

/* =====================================================
   SKELETON LOADING
   -----------------------------------------------------
   Promoted Jul 2026. The gradient and the keyframes existed independently in
   eight page stylesheets. Sizing stays page-local — a skeleton should match the
   shape of the thing it stands in for — but the shimmer itself is one thing.
   ===================================================== */

@keyframes ds-shimmer {
  0%   { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

.ds-skel {
  display: block;
  background: var(--ds-skel-gradient);
  background-size: 200% 100%;
  border-radius: var(--ds-radius-xs);
  animation: ds-shimmer 1.4s ease-in-out infinite;
}

@media (prefers-reduced-motion: reduce) {
  .ds-skel { animation: none; }
}

/* =====================================================
   SELECT
   -----------------------------------------------------
   Three pages had grown their own: .pc-per-page select, .pp-select-group
   select, .d2-rep-select. Same control, same job.
   ===================================================== */

.ds-select {
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-label);
  font-weight: var(--ds-font-weight-semibold);
  padding: 6px 10px;
  min-height: 32px;
  border: 1px solid var(--ds-border-default);
  border-radius: var(--ds-radius-sm);
  background: var(--ds-bg-card);
  color: var(--ds-text-primary);
  cursor: pointer;
}
.ds-select:focus-visible {
  outline: 2px solid var(--ds-color-brand);
  outline-offset: 1px;
}
/* Wider control for long option labels (rep names carrying a dollar figure). */
.ds-select-wide { max-width: 320px; }

@media (pointer: coarse) {
  .ds-select { min-height: 44px; }
}

/* =====================================================
   CHARTS
   -----------------------------------------------------
   Promoted Jul 2026 from .cd-chart-* / .cd-bar-* on the customer detail page,
   which was the only charting in the app and was built with real care: an
   oversized invisible hit target per bar, a per-bar role="img" with a full
   aria-label, keyboard focus, and the year carried on January when a range
   spans more than one.

   NO CHART LIBRARY, deliberately. Chart.js is ~200KB and ApexCharts ~500KB
   minified, both render to <canvas> that a screen reader cannot see, and both
   arrive with their own palette to fight. This is ~1KB of markup that already
   speaks the design tokens and is already accessible. Reps open these pages on
   rural 4G.

   Rendering lives in public/js/ds-chart.js.
   ===================================================== */

.ds-chart-wrap { position: relative; }

.ds-chart-svg {
  display: block;
  width: 100%;
  height: auto;
  max-height: 320px;
  margin: 0 auto;
}
/* Compact variant for a chart sharing a card with other content. */
.ds-chart-svg.is-short { max-height: 200px; }

.ds-chart-baseline {
  stroke: var(--ds-border-default);
  stroke-width: 1;
}

/* Horizontal gridline behind the plot. Kept to two or three — a ruled grid
   competes with the data it is meant to support. */
.ds-chart-gridline {
  stroke: var(--ds-border-light);
  stroke-width: 1;
}

.ds-chart-axis-label {
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-label);
  fill: var(--ds-text-tertiary);
}

/* ─── Bars ─── */

.ds-bar-group { cursor: pointer; outline: none; }
/* The fill is not optional decoration: an SVG <rect> with no fill declared
   falls back to the spec default of #000, so these columns rendered pure black
   next to a navy line chart and navy hbars while every other surface on the
   page resolved through the token. */
.ds-bar {
  fill: var(--ds-color-navy);
  transition: opacity var(--ds-transition-fast);
}
.ds-bar-group:hover .ds-bar,
.ds-bar-group:focus .ds-bar { opacity: 0.78; }
.ds-bar-group:focus-visible .ds-bar {
  stroke: var(--ds-color-brand);
  stroke-width: 1.5;
}

/* The same period a year earlier, drawn behind the current bar so "is this
   month normal?" is answerable without arithmetic. */
.ds-bar-ghost { fill: var(--ds-border-default); }

.ds-bar-label {
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-label);
  fill: var(--ds-text-tertiary);
}

.ds-bar-toplabel {
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-micro);
  font-weight: var(--ds-font-weight-bold);
  fill: var(--ds-text-secondary);
}

/* ─── Lines ─── */

.ds-chart-line {
  fill: none;
  stroke: var(--ds-color-navy);
  stroke-width: 2;
  stroke-linejoin: round;
  stroke-linecap: round;
}

/* The reference series — target pace. Thin and dashed so it reads as the
   yardstick rather than a second result. No fill between the lines: the white
   space IS the gap, and shading it just makes the chart heavier. */
.ds-chart-line-ref {
  fill: none;
  stroke: var(--ds-text-tertiary);
  stroke-width: 1.5;
  stroke-dasharray: 4 4;
  opacity: 0.75;
}

.ds-chart-dot {
  fill: var(--ds-color-navy);
  stroke: var(--ds-bg-card);
  stroke-width: 2;
}
.ds-chart-dot.is-behind { fill: var(--ds-status-danger-text); }
.ds-chart-dot.is-ahead  { fill: var(--ds-status-success); }

/* ─── Horizontal bars (rep comparison) ───────────────────────────────────
   Real DOM rows rather than one SVG, deliberately: the row labels carry avatar
   <img> elements that need an onerror fallback, and the names should be
   selectable text. An SVG would make both awkward for no gain. */

/* Rows flow in COLUMNS, not one tall stack.
   Thirteen reps in a single column made this the tallest card on the page.
   CSS multicol rather than a grid because multicol fills column-major — ranks
   1-7 down the first column, 8-13 down the second — so the sort order still
   reads top-to-bottom. An auto-fit grid would interleave the ranking
   left-to-right across each row instead.
   Every bar is scaled to the same maximum, so lengths stay comparable across
   the column break. */
.ds-hbar-rows {
  /* One column by default. The rep comparison now lives in a tall narrow card
     rather than a full-width band, so multicol would fight the container. The
     multicol variant remains available for any caller that wants a wide,
     short arrangement. */
  columns: 1;
}
.ds-hbar-rows.is-multicol {
  columns: 2;
  column-gap: var(--ds-space-xl);
}
.ds-hbar-rows > * {
  /* A row split across a column boundary would be unreadable. */
  break-inside: avoid;
  -webkit-column-break-inside: avoid;
}

@media (min-width: 1500px) {
  .ds-hbar-rows.is-multicol { columns: 3; }
}
@media (max-width: 860px) {
  .ds-hbar-rows.is-multicol { columns: 1; }
}

.ds-hbar-row {
  display: grid;
  grid-template-columns: minmax(120px, 1.1fr) minmax(80px, 2fr) 68px;
  align-items: center;
  gap: var(--ds-space-md);
  padding: 7px 4px;
  border-radius: var(--ds-radius-xs);
  cursor: pointer;
  background: none;
  border: 0;
  width: 100%;
  text-align: left;
}
.ds-hbar-row:hover { background: var(--ds-bg-subtle); }
.ds-hbar-row:focus-visible {
  outline: 2px solid var(--ds-color-brand);
  outline-offset: -2px;
}

.ds-hbar-track {
  position: relative;
  display: block;
  height: 10px;
  border-radius: var(--ds-radius-pill);
  background: var(--ds-bg-subtle);
  border: 1px solid var(--ds-border-default);
}

.ds-hbar-fill {
  display: block;
  height: 100%;
  border-radius: var(--ds-radius-pill);
  background: var(--ds-color-navy);
  transition: width 0.35s ease;
}
.ds-hbar-fill.is-behind { background: var(--ds-status-danger-text); }
.ds-hbar-fill.is-ahead  { background: var(--ds-status-success); }

/* Where this rep's own work-day pace says they should be. Sits above the track
   rather than inside it so the fill's rounding cannot clip it. */
.ds-hbar-pace {
  position: absolute;
  top: -3px;
  width: 2px;
  height: 16px;
  background: var(--ds-text-primary);
  opacity: 0.55;
  border-radius: var(--ds-radius-pill);
}

.ds-hbar-val {
  text-align: right;
  font-size: var(--ds-font-size-label);
  font-weight: var(--ds-font-weight-bold);
  color: var(--ds-text-primary);
  font-variant-numeric: tabular-nums;
}

/* Shared empty/error line for any chart mount. */
.ds-chart-empty {
  margin: 0;
  padding: var(--ds-space-lg) 0;
  text-align: center;
  font-size: var(--ds-font-size-dense);
  color: var(--ds-text-tertiary);
}

@media (prefers-reduced-motion: reduce) {
  .ds-hbar-fill { transition: none; }
}

@media (max-width: 768px) {
  .ds-hbar-row { grid-template-columns: minmax(90px, 1fr) minmax(60px, 1.4fr) 56px; }
}

/* ─── Tooltip ─── */

.ds-chart-tooltip {
  position: absolute;
  top: 0;
  transform: translateX(-50%);
  background: var(--ds-color-navy);
  color: var(--ds-text-on-brand);
  font-size: var(--ds-font-size-label);
  line-height: 1.4;
  padding: 6px 10px;
  border-radius: var(--ds-radius-sm);
  pointer-events: none;
  white-space: nowrap;
  z-index: 20;
}
.ds-chart-tooltip strong {
  display: block;
  font-size: var(--ds-font-size-label);
  opacity: 0.8;
  font-weight: var(--ds-font-weight-semibold);
}

/* ─── Legend ─── */

.ds-chart-legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ds-space-md);
  margin-top: var(--ds-space-sm);
  font-size: var(--ds-font-size-micro);
  color: var(--ds-text-tertiary);
}
.ds-chart-legend span { display: inline-flex; align-items: center; gap: 6px; }
.ds-chart-legend i {
  display: inline-block;
  width: 14px;
  height: 0;
  border-top: 2px solid var(--ds-color-navy);
}
.ds-chart-legend i.is-ref {
  border-top: 1.5px dashed var(--ds-text-tertiary);
}
.ds-chart-legend i.is-swatch {
  height: 10px;
  border: 0;
  border-radius: 2px;
  background: var(--ds-border-default);
}

/* =====================================================
   AVATAR
   -----------------------------------------------------
   Promoted Jul 2026 from .dash-ava, with photo support added.

   CONVENTION (established by sidebar.ejs and call-calendar.js): User.image_url
   holds a BARE FILENAME, served from /img/ (see the express.static mount in
   app.js). "/profile.jpg" is the sentinel for "no photo" — and that file does
   not actually exist, which is why every caller guards against the value
   rather than trusting it, and why <img> needs an onerror fallback too.

   Three states, because reps are not all people:
     .ds-avatar with an <img>      a real photo (6 of 12 rep ids have one)
     .ds-avatar with initials      a person with no photo
     .ds-avatar.is-queue          a team queue, not a person. "Customer
                                   Service" owns 2,690 customers and $769k of
                                   FY revenue but is a mailbox, so it gets an
                                   icon rather than a person's initials in a
                                   round frame.
   ===================================================== */

.ds-avatar {
  width: 28px;
  height: 28px;
  border-radius: 50%;
  background: var(--ds-bg-subtle);
  border: 1px solid var(--ds-border-default);
  color: var(--ds-color-navy);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-micro);
  font-weight: var(--ds-font-weight-bold);
  flex: 0 0 auto;
  overflow: hidden;
  /* Initials must not be selectable as text when they sit inside a button. */
  user-select: none;
}

.ds-avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* Inline with micro text — a rep's face beside their name on a docket, where a
   22px avatar would set the line height rather than sit inside it. */
.ds-avatar-xs {
  width: 16px;
  height: 16px;
  font-size: 8px;
}
.ds-avatar-sm { width: 22px; height: 22px; }
.ds-avatar-lg {
  width: 40px;
  height: 40px;
  font-size: var(--ds-font-size-dense);
}

/* A queue, not a person. Squared-off enough to read as different at a glance
   while keeping the same footprint so rows stay aligned. */
.ds-avatar.is-queue {
  border-radius: var(--ds-radius-xs);
  background: var(--ds-bg-screen);
  color: var(--ds-text-tertiary);
}
.ds-avatar.is-queue .material-icons { font-size: 16px; }
.ds-avatar-lg.is-queue .material-icons { font-size: 22px; }

/* Name + avatar, the unit that appears in table cells, popover rows and
   chart labels. */
.ds-person {
  display: inline-flex;
  align-items: center;
  gap: var(--ds-space-sm);
  min-width: 0;
}
.ds-person-name {
  font-size: var(--ds-font-size-dense);
  color: var(--ds-text-primary);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.ds-person-sub {
  display: block;
  font-size: var(--ds-font-size-micro);
  color: var(--ds-text-tertiary);
  font-variant-numeric: tabular-nums;
}


/* -----------------------------------------------------
   @ MENTION PICKER

   Floats above whatever box asked for it, so it works inside a scrolling
   drawer where an absolutely-positioned menu would be clipped by the panel.
   Positioned from viewport coordinates in public/js/ds-mentions.js.
   ----------------------------------------------------- */

.dsm-menu {
  position: fixed;
  z-index: 1200;
  max-height: 220px;
  overflow-y: auto;
  padding: 4px;
  background: var(--ds-bg-card);
  border: 1px solid var(--ds-border-default);
  border-radius: var(--ds-radius-md);
  box-shadow: var(--ds-shadow-modal, 0 12px 20px rgba(0, 0, 0, 0.18));
}
.dsm-menu[hidden] { display: none; }

.dsm-opt {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 6px 8px;
  border: 0;
  border-radius: var(--ds-radius-sm);
  background: none;
  font-family: var(--ds-font-family);
  font-size: var(--ds-font-size-dense);
  color: var(--ds-text-primary);
  text-align: left;
  cursor: pointer;
}
.dsm-opt:hover,
.dsm-opt[aria-selected="true"] { background: var(--ds-bg-subtle); }
.dsm-opt .ds-avatar { flex: 0 0 auto; }
