﻿/* ==========================================================================
   DataMan design system
   --------------------------------------------------------------------------
   Shares its chrome and palette with OnPlan so the two apps read as one suite:
   dark #212529 bars top and bottom, Bootstrap's own #0d6efd primary, Roboto,
   Bootstrap Icons. If you are changing a colour here, check OnPlan first —
   the point is that they match.

   What is NOT shared is density. OnPlan lays out planograms; DataMan edits
   39,000 brands and 9,000 stores in a spreadsheet. So the frame stays:

     page head  →  filter card  →  data card (toolbar + table)

   and two ideas carry it:

   1. Shelf-ticket labels. Column headers and field labels share one treatment
      — small, uppercase, wide-tracked — so a grid header and a form label read
      as the same language. (OnPlan spells this
      `thead class="bg-light text-muted small text-uppercase"`; same idea.)
   2. The state rail. A 3px left edge is the ONLY thing in this UI that uses
      that shape, and it always means the same thing: blue = you are here /
      add here, amber = unsaved, green = saved, red = failed. Nothing else gets
      a left rail, which is what makes it readable at a glance across 200 rows.

   Sections
     1. Tokens
     2. Base & typography
     3. Bootstrap tuning (density only — the colours are Bootstrap's own)
     4. App shell (navbar, footer)
     5. Page frame (head, filters, data card, toolbar, status)
     6. Tables
     7. Inline-editable grids
     8. Products grid
     9. Movement view
    9b. Dashboard stat cards
    10. Defined markets
    11. Forms & modals
    12. Sign-in
    13. Utilities, responsive, motion
   ========================================================================== */

/* ==========================================================================
   1. Tokens
   --------------------------------------------------------------------------
   Every value here is either a Bootstrap 5.3 default or a hex lifted from
   OnPlan's header/footer SCSS, so the two apps cannot drift apart by accident.
   ========================================================================== */

:root {
  /* Chrome — OnPlan's header and footer, to the hex */
  --dm-ink: #212529;
  --dm-ink-raised: #2b3035;
  --dm-ink-line: #343a40;

  /* Text — Bootstrap's body/secondary/tertiary ramp */
  --dm-text: #212529;
  --dm-text-muted: #6c757d;
  --dm-text-faint: #adb5bd;

  /* Surfaces. The canvas is Bootstrap's secondary-subtle, which is what
     OnPlan puts on <app-root>. */
  --dm-paper: #e2e3e5;
  --dm-surface: #ffffff;
  --dm-surface-sunken: #f8f9fa;
  --dm-surface-hover: #f1f3f5;

  /* Hairlines */
  --dm-line: #dee2e6;
  --dm-line-strong: #ced4da;

  /* The one accent: Bootstrap primary. Links, primary actions, "you are here". */
  --dm-accent: #0d6efd;
  --dm-accent-hover: #0a58ca;
  --dm-accent-soft: #cfe2ff;
  --dm-accent-rgb: 13, 110, 253;

  /* Semantic — row state and messages.
     Amber is the one colour that needs two values: #ffc107 is bright enough to
     read as a 3px rail but far too light to be text, so anything wordy uses the
     emphasis shade instead. Green and red are legible at both jobs. */
  --dm-ok: #198754;
  --dm-ok-soft: #d1e7dd;
  --dm-warn: #997404;
  --dm-warn-solid: #ffc107;
  --dm-warn-soft: #fff3cd;
  --dm-danger: #dc3545;
  --dm-danger-soft: #f8d7da;

  /* Type scale */
  --dm-fs-micro: 0.6875rem;  /* shelf-ticket labels */
  --dm-fs-sm: 0.8125rem;
  --dm-fs-base: 0.875rem;
  --dm-fs-lg: 1rem;
  --dm-fs-title: 1.25rem;

  /* Geometry. OnPlan leaves Bootstrap's 0.375rem radius alone; so do we. */
  --dm-radius: 0.375rem;
  --dm-radius-sm: 0.25rem;
  --dm-rail: 3px;

  /* Chrome heights — px, not rem, so they match OnPlan's 60px bars even though
     our root font size is smaller. */
  --dm-header-h: 60px;

  /* Elevation — OnPlan's card shadow, plus a lighter and a heavier step */
  --dm-shadow-sm: 0 1px 2px rgba(0, 0, 0, .06);
  --dm-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, .075);
  --dm-shadow-lg: 0 12px 32px rgba(0, 0, 0, .18);

  --dm-font: Roboto, "Helvetica Neue", Arial, sans-serif;

  /* Hand these to Bootstrap so its own components inherit them rather than
     having to be overridden one at a time (this is the 5.3 upgrade paying off).

     --bs-body-bg is deliberately NOT set here. In 5.3 it is the default for
     card, modal, dropdown and form-control backgrounds too, so pointing it at
     the grey canvas turns every card and every input grey. The canvas colour
     belongs on body and nowhere else. */
  --bs-body-font-family: var(--dm-font);
  --bs-body-color: var(--dm-text);
  --bs-border-color: var(--dm-line);
  --bs-link-color: var(--dm-accent);
  --bs-link-hover-color: var(--dm-accent-hover);
}

/* ==========================================================================
   2. Base & typography
   ========================================================================== */

/* 14px root, against OnPlan's 16px. This is the density trade deliberately kept:
   OnPlan gets the same effect at 16px by scaling --bs-body-font-size down at four
   breakpoints, which is the same destination by a longer road. */
html {
  font-size: 14px;
}

body {
  font-family: var(--dm-font);
  font-size: var(--dm-fs-base);
  color: var(--dm-text);
  background-color: var(--dm-paper);
  -webkit-font-smoothing: antialiased;
  /* Numbers line up under each other everywhere they appear. */
  font-variant-numeric: tabular-nums;
  /* The header is fixed, so the document has to start below it. */
  padding-top: var(--dm-header-h);
}

h1, h2, h3, h4, h5, h6 {
  color: var(--dm-ink);
  letter-spacing: -0.011em;
}

a {
  text-underline-offset: 0.15em;
}

code {
  color: var(--dm-accent-hover);
  background: var(--dm-accent-soft);
  padding: 0.05em 0.35em;
  border-radius: var(--dm-radius-sm);
}

/* Bootstrap Icons sit slightly high next to Roboto's baseline. One nudge here
   beats a `position-relative` on every icon in every view. */
.bi {
  vertical-align: -0.125em;
}

/* The shelf-ticket label. Used by column headers, field labels and any
   micro-heading, so a grid and a form speak the same language. */
.dm-label,
.dm-filters .form-label,
.dm-form .form-label,
.table > thead th,
.dm-card-toolbar .form-label {
  font-size: var(--dm-fs-micro);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--dm-text-muted);
  margin-bottom: 0.25rem;
}

/* ==========================================================================
   3. Bootstrap tuning
   --------------------------------------------------------------------------
   This section used to be 200 lines restating the palette on .btn-primary,
   .form-control, .alert-* and friends, because Bootstrap 5.1 compiled its
   colours to hex and DataMan's petrol was not one of them.

   Both halves of that problem are gone: 5.3 exposes per-component custom
   properties, and the accent is now Bootstrap's own #0d6efd. So the buttons,
   alerts, badges, dropdowns and pagination are simply left alone, and what is
   left below is density — this app is a spreadsheet, and Bootstrap's default
   control sizing is built for a marketing page.
   ========================================================================== */

.btn-sm {
  --bs-btn-font-size: var(--dm-fs-sm);
  --bs-btn-padding-y: 0.25rem;
  --bs-btn-padding-x: 0.6rem;
}

/* A disabled button here nearly always means "not allowed yet, and there is a
   reason" (row unchanged, record in use) — those reasons are in the title
   attribute, so keep the control legible enough to hover rather than ghosted. */
.btn:disabled, .btn.disabled {
  opacity: .45;
}

.form-text {
  font-size: var(--dm-fs-sm);
}

.dropdown-menu {
  --bs-dropdown-font-size: var(--dm-fs-base);
  --bs-dropdown-padding-x: 0.3rem;
  --bs-dropdown-padding-y: 0.3rem;
  --bs-dropdown-item-padding-y: 0.35rem;
  --bs-dropdown-item-padding-x: 0.65rem;
  --bs-dropdown-border-color: var(--dm-line);
  box-shadow: var(--dm-shadow);
}

.dropdown-item {
  border-radius: var(--dm-radius-sm);
}

/* Alerts keep the state rail rather than a full border — same shape and the same
   meaning as a dirty or failed row. The colours are Bootstrap's own subtle set. */
.alert {
  --bs-alert-padding-y: 0.7rem;
  --bs-alert-padding-x: 1rem;
  border-width: 0 0 0 var(--dm-rail);
  box-shadow: var(--dm-shadow-sm);
}
.alert-success { border-color: var(--dm-ok); }
.alert-danger  { border-color: var(--dm-danger); }
.alert-warning { border-color: var(--dm-warn-solid); }
.alert-info    { border-color: var(--dm-accent); }

/* Cards get OnPlan's borderless-with-a-shadow treatment. */
.card {
  --bs-card-border-color: var(--dm-line);
  box-shadow: var(--dm-shadow);
}

.card-header {
  background-color: var(--dm-surface);
  border-bottom-color: var(--dm-line);
  font-weight: 600;
  color: var(--dm-ink);
}

/* ==========================================================================
   4. App shell
   --------------------------------------------------------------------------
   The bit that makes DataMan and OnPlan recognisably the same product: a fixed
   60px #212529 bar with a 1px #343a40 underline, an icon-plus-wordmark brand,
   and transparent toolbar buttons that fill in on hover.
   ========================================================================== */

.dm-nav {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: var(--dm-header-h);
  z-index: 1030;
  padding-top: 0;
  padding-bottom: 0;
  background-color: var(--dm-ink);
  border-bottom: 1px solid var(--dm-ink-line);
  box-shadow: var(--dm-shadow);
}

.dm-nav > .container-fluid {
  height: 100%;
}

.dm-nav .navbar-brand {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-weight: 700;
  font-size: 1.15rem;
  /* OnPlan's .tracking-tight — the wordmark is set noticeably tighter than body copy. */
  letter-spacing: -0.5px;
  color: #fff;
  padding-top: 0;
  padding-bottom: 0;
}

/* The brand mark is a Bootstrap icon in the primary blue, exactly as OnPlan
   sets its own — same size, same colour, different glyph. */
.dm-nav .navbar-brand .bi {
  font-size: 1.4rem;
  color: var(--dm-accent);
  line-height: 1;
}

.dm-brand-sub {
  color: rgba(255, 255, 255, .45);
  font-weight: 400;
  font-size: var(--dm-fs-sm);
  letter-spacing: 0;
  padding-left: 0.55rem;
  margin-left: 0.1rem;
  border-left: 1px solid rgba(255, 255, 255, .16);
}

/* OnPlan's .btn-dark-toolbar, applied to nav links: invisible until you reach
   for them, then a soft white wash. */
.dm-nav .nav-link {
  color: #e9ecef;
  font-weight: 500;
  padding: 0.4rem 0.75rem;
  border: 1px solid transparent;
  border-radius: var(--dm-radius-sm);
  transition: all 0.2s ease-in-out;
  white-space: nowrap;   /* "Document bank" is the first two-word label on the bar */
}

.dm-nav .nav-link:hover,
.dm-nav .nav-link:focus,
.dm-nav .nav-item.show > .nav-link {
  color: #fff;
  background-color: rgba(255, 255, 255, .1);
  border-color: rgba(255, 255, 255, .1);
}

.dm-nav .nav-link.active {
  color: #fff;
  background-color: rgba(255, 255, 255, .1);
  box-shadow: inset var(--dm-rail) 0 0 var(--dm-accent);
}

.dm-nav .navbar-toggler {
  border-color: rgba(255, 255, 255, .2);
  padding: 0.2rem 0.5rem;
}

/* Icons inside the menus are muted until the row is hovered — OnPlan's
   `<i class="bi bi-save me-2 text-muted"></i>` idiom. */
.dropdown-item .bi {
  color: var(--dm-text-muted);
  margin-right: 0.5rem;
}

.dropdown-item:hover .bi,
.dropdown-item:focus .bi,
.dropdown-item.active .bi {
  color: inherit;
}

/* A link into another CGS application. The trailing glyph is pushed to the far edge
   and stays muted on hover — it is a signpost, not part of the label. */
.dropdown-item.dm-external {
  display: flex;
  align-items: center;
}

.dm-external .dm-external-hint {
  margin-left: auto;
  margin-right: 0;
  font-size: 0.75em;
  opacity: .7;
}

/* The "you are here" rail again, on the item inside the open menu. */
.dm-nav .dropdown-item.active {
  background-color: var(--dm-accent-soft);
  color: var(--dm-accent-hover);
  font-weight: 600;
  box-shadow: inset var(--dm-rail) 0 0 var(--dm-accent);
}

/* Database switcher and user menu sit in the dark bar. */
.dm-nav-tools {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

.dm-db-switch {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  margin-bottom: 0;
}

.dm-db-switch .dm-label {
  color: rgba(255, 255, 255, .5);
  margin-bottom: 0;
  white-space: nowrap;
}

.dm-db-switch .form-select {
  background-color: var(--dm-ink-raised);
  border-color: rgba(255, 255, 255, .18);
  color: #fff;
  font-size: var(--dm-fs-sm);
  min-width: 10rem;
  background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");
}

.dm-db-switch .form-select:disabled {
  color: rgba(255, 255, 255, .6);
}

.dm-user-toggle {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  color: #e9ecef;
  text-decoration: none;
  font-weight: 500;
  padding: 0.3rem 0.5rem;
  border: 1px solid transparent;
  border-radius: var(--dm-radius-sm);
  transition: all 0.2s ease-in-out;
}

.dm-user-toggle:hover, .dm-user-toggle:focus, .show > .dm-user-toggle {
  color: #fff;
  background-color: rgba(255, 255, 255, .1);
  border-color: rgba(255, 255, 255, .1);
}

/* Initials disc — cheaper and steadier than a full name in a fixed bar. */
.dm-avatar {
  width: 1.65rem;
  height: 1.65rem;
  border-radius: 50%;
  background: var(--dm-accent);
  color: #fff;
  font-size: var(--dm-fs-micro);
  font-weight: 700;
  letter-spacing: 0.02em;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
}

.dm-user-card {
  padding: 0.55rem 0.65rem 0.35rem;
  min-width: 14rem;
}

/* Tight on purpose. The navbar and footer are 60px each because they are shared chrome with OnPlan;
   that is 120px this app does not get to spend, so everything between them is set as close as it
   reads well at. See [Page frame] below for the rest of the same trade. */
.dm-main {
  padding-top: 0.6rem;
  padding-bottom: 0.75rem;
}

/* --- Footer status bar ---
   OnPlan's footer: dark, gradient, context on the left and stat pills on the
   right. It is not position-fixed here — DataMan's long grids already cap
   themselves at 68vh, so the bar lands on screen anyway, and pinning it would
   fight the sticky table headers for the bottom of the viewport. */
.dm-footer {
  background: linear-gradient(180deg, var(--dm-ink-raised) 0%, var(--dm-ink) 100%);
  border-top: 1px solid var(--dm-ink-line);
  color: #adb5bd;
  font-size: var(--dm-fs-sm);
  min-height: var(--dm-header-h);
  display: flex;
  align-items: center;
}

.dm-footer > .container-fluid {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
}

.dm-footer-spacer {
  flex: 1 1 auto;
}

.dm-footer-stats {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* OnPlan's .stat-pill, unchanged. */
.dm-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  color: #e9ecef;
  background-color: rgba(255, 255, 255, .05);
  padding: 4px 12px;
  border-radius: 20px;
  font-size: var(--dm-fs-sm);
  white-space: nowrap;
  border: 1px solid transparent;
  transition: all 0.2s ease-in-out;
}

.dm-pill:hover {
  background-color: rgba(255, 255, 255, .1);
  border-color: rgba(255, 255, 255, .1);
}

.dm-pill .bi {
  color: var(--dm-text-faint);
}

.dm-pill strong {
  font-weight: 500;
  color: #fff;
}

/* ==========================================================================
   5. Page frame
   ========================================================================== */

.dm-page-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.4rem 1rem;
  flex-wrap: wrap;
  padding-bottom: 0.35rem;
  margin-bottom: 0.6rem;
  border-bottom: 1px solid var(--dm-line-strong);
}

/* The title and its count sit on ONE line rather than stacked, which is a whole row of the window
   back for a heading that was never two lines' worth of information.

   Done here rather than in the fifteen views because every one of them writes the same shape — the
   head, an unclassed div holding the title and the meta, then an optional .dm-page-actions. */
.dm-page-head > div:first-child {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.15rem 0.6rem;
  min-width: 0;
}

.dm-page-title {
  font-size: var(--dm-fs-title);
  font-weight: 700;
  margin: 0;
  line-height: 1.2;
}

/* An "explain this number" affordance sitting inline with a count in the page meta. A real <button>
   rather than an <i> so it is focusable and reachable by keyboard — the popover opens on focus, which
   is also what dismisses it on the next click anywhere. Inherits colour so it reads as part of the
   count it belongs to, including the red one. */
.dm-info-btn {
  border: 0;
  background: none;
  padding: 0 0 0 0.15rem;
  margin: 0;
  line-height: 1;
  color: inherit;
  opacity: 0.7;
  cursor: pointer;
  vertical-align: baseline;
}

.dm-info-btn:hover,
.dm-info-btn:focus-visible {
  opacity: 1;
}

/* Wider than Bootstrap's 276px default: this one carries three short paragraphs of definition, and at
   the default width it becomes a column of six-word lines. */
.dm-info-popover {
  --bs-popover-max-width: 24rem;
  --bs-popover-body-font-size: var(--dm-fs-sm);
}

.dm-info-popover .popover-body p:last-child {
  margin-bottom: 0;
}

/* A qualifier inside the title — the merch category you drilled into, under the store's name. Lighter
   and smaller so the store still reads as the heading and this reads as "…filtered to this". */
.dm-page-title-sub {
  display: block;
  font-size: var(--dm-fs-sm);
  font-weight: 500;
  color: var(--dm-text-muted);
  margin-top: 0.15rem;
}

/* The count sits with the title, not in the toolbar: it is a property of the
   thing you are looking at, not an action you can take. */
.dm-page-meta {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 0.5rem;
  font-size: var(--dm-fs-sm);
  color: var(--dm-text-muted);
}

.dm-page-meta .dm-dot::before {
  content: "·";
  margin-right: 0.5rem;
  color: var(--dm-text-faint);
}

.dm-count {
  font-weight: 600;
  color: var(--dm-text);
}

/* A list's "Products" column: the number, then a quiet shortcut into the product
   grid filtered to exactly those products. Muted until hovered so a column of them
   does not read as a column of buttons. */
.dm-count-cell {
  white-space: nowrap;
}

.dm-count-link {
  margin-left: 0.4rem;
  color: var(--dm-text-muted);
  text-decoration: none;
}

/* "4/8" for a reader whose merchandising restriction hides some of the row's
   products. The denominator is muted so the column still scans as a column of
   numbers — the figure that matters is the one they can act on, and the total is
   context for why the delete button is disabled. */
.dm-count-total {
  color: var(--dm-text-muted);
  font-size: 0.85em;
}

.dm-count-link:hover,
.dm-count-link:focus-visible {
  color: var(--dm-accent);
}

/* The hierarchy edit cascade panel names the two categories in its prose; scoped
   rather than global because "cascade-old" is far too generic a class name. */
#cascadePanel .cascade-old,
#cascadePanel .cascade-new {
  font-weight: 600;
}

.dm-page-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* --- Collapsed filters (site.js) ---
   The fold-away toggle lives in the page heading, not on the filter card: on the card it would need
   a row of its own, and spending a row to save rows is not a trade. */
.dm-filters-toggle .bi {
  margin-right: 0.35rem;
}

/* What the folded card is still filtering on, drawn into the meta line beside the count. Without it
   "61 matched" is a number with no explanation and a narrow result reads as missing data. */
.dm-filter-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 0.3rem;
  max-width: 20rem;
  padding: 0.05rem 0.4rem;
  border-radius: var(--dm-radius-sm);
  background: var(--dm-accent-soft);
  color: var(--dm-accent-hover);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.dm-filter-chip-k {
  font-size: var(--dm-fs-micro);
  text-transform: uppercase;
  letter-spacing: 0.05em;
  opacity: 0.75;
}

.dm-filter-chip b {
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* --- Filter card --- */
.dm-filters {
  background: var(--dm-surface);
  border: 1px solid var(--dm-line);
  border-radius: var(--dm-radius);
  box-shadow: var(--dm-shadow);
  padding: 0.55rem 0.75rem;
  margin-bottom: 0.6rem;
}

.dm-filters .row {
  --bs-gutter-y: 0.45rem;
}

/* The label above a filter is a shelf ticket, not a heading — it needs to be readable, not spaced. */
.dm-filters .form-label {
  margin-bottom: 0.1rem;
}

.dm-filters .form-control,
.dm-filters .form-select {
  font-size: var(--dm-fs-sm);
  padding: 0.28rem 0.55rem;
}

.dm-filters .form-select {
  padding-right: 1.9rem;
}

/* Auto-width columns size to their content, which for a select is its longest
   option — give those a floor so a placeholder like "— all regions —" fits.
   Grid-sized columns (col, col-md-2) size themselves and are left alone. */
/* .dm-combo is listed alongside the controls it wraps: enhancing a select moves it inside
   a wrapper, so a rule written for a direct child would stop reaching it. */
.dm-filters .col-auto > .form-control,
.dm-filters .col-auto > .form-select,
.dm-filters .col-auto > .dm-combo {
  min-width: 11rem;
}

/* Sales by date's "Find a store" spans every region, so each option reads
   "Store (Alias) — Region". At the 11rem floor above, the part that tells two
   similarly named stores apart is the part that gets truncated. */
.dm-filters .col-auto.dm-store-find > .form-select,
.dm-filters .col-auto.dm-store-find > .dm-combo {
  min-width: 20rem;
}

/* The products filter's value control is three swappable inputs in a wrapper, so the
   min-width above (direct children only) does not reach them. Sized a little wider
   because one of the three is a lookup list showing full category and supplier names. */
.dm-filters .dm-filter-value {
  min-width: 15rem;
}

.dm-filters .dm-filter-value > .form-control,
.dm-filters .dm-filter-value > .form-select,
.dm-filters .dm-filter-value > .dm-combo {
  width: 100%;
}

.dm-filters .form-check-label {
  font-size: var(--dm-fs-sm);
  color: var(--dm-text);
}

/* Every filter form ends with the same pair, aligned to the field baseline. */
.dm-filter-actions {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

/* --- Month-on-month change (Sales by date) ---
   Colour is the quick read, but it is not the only one: the figure is always signed,
   so the direction survives being printed, photocopied or read by someone who cannot
   tell the two hues apart. */
.dm-delta-up {
  color: var(--dm-ok);
  font-weight: 600;
}

.dm-delta-down {
  color: var(--dm-danger);
  font-weight: 600;
}

.dm-delta-flat {
  color: var(--dm-text-muted);
}

/* --- Searchable combo (searchable-select.js) ---
   The original <select> is hidden but still in the DOM and still holds the value; what
   the user sees is the input plus this list. Absolutely positioned, so a select inside a
   scrolling container must not opt in — the list would be clipped by its overflow. */
.dm-combo {
  position: relative;
}

.dm-combo > select {
  display: none;
}

.dm-combo-list {
  position: absolute;
  z-index: 1060; /* above .modal (1055), so it works inside a dialog too */
  top: 100%;
  left: 0;
  right: 0;
  margin-top: 0.15rem;
  max-height: 15rem;
  overflow-y: auto;
  background: var(--dm-surface);
  border: 1px solid var(--dm-line);
  border-radius: var(--dm-radius);
  box-shadow: var(--dm-shadow);
}

.dm-combo-option {
  padding: 0.3rem 0.6rem;
  font-size: var(--dm-fs-sm);
  color: var(--dm-text);
  cursor: pointer;
}

.dm-combo-option:hover,
.dm-combo-option.is-active {
  background: var(--dm-accent-soft);
  color: var(--dm-accent-hover);
}

.dm-combo-empty {
  padding: 0.3rem 0.6rem;
  font-size: var(--dm-fs-micro);
  color: var(--dm-text-muted);
}

/* --- Data card --- */
.dm-card {
  background: var(--dm-surface);
  border: 1px solid var(--dm-line);
  border-radius: var(--dm-radius);
  box-shadow: var(--dm-shadow);
  overflow: hidden;
}

.dm-card-toolbar {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
  padding: 0.35rem 0.65rem;
  background: var(--dm-surface);
  border-bottom: 1px solid var(--dm-line);
  min-height: 2.25rem;
}

.dm-card-toolbar .mb-2 {
  margin-bottom: 0 !important; /* table-tools injects its button wrapper here */
}

.dm-toolbar-spacer {
  flex: 1 1 auto;
}

/* Icons sit in buttons alongside a label; nudge them onto the text baseline. */
.dm-icon {
  vertical-align: -0.13em;
  margin-right: 0.35rem;
  flex: none;
}

.dm-card-body {
  padding: 0.9rem;
}

.dm-card > .table-responsive > .table:last-child,
.dm-card > .table:last-child {
  margin-bottom: 0;
}

/* Keeps a long grid usable without losing the page frame. */
.dm-scroll {
  max-height: 68vh;
  overflow: auto;
}

/* Column headings stay put while the rows scroll under them — with a hundred rows in
   the box, headings that scroll away leave you counting columns. */
.dm-scroll > .table > thead > tr > th {
  position: sticky;
  top: 0;
  z-index: 2;
}

/* Duplicates & unused stacks two scroll boxes on one page, so the first is kept shorter than a
   lone grid's — the unused list's heading stays in reach without scrolling the page far. */
.dm-scroll.dm-dup-scroll {
  max-height: 50vh;
}

/* The blank "add" row rides just under the headings, so creating a record is always
   one click away rather than something you scroll to find. */
.dm-scroll > .inline-grid > tbody > tr.ig-new > td {
  position: sticky;
  top: 2.35rem;
  z-index: 1;
}

/* --- Status line --- */
/* Save feedback is a persistent line in the toolbar rather than a toast: in a
   grid you save many rows in a row, and a message that flies away is a message
   you miss. (OnPlan can use toasts because its saves are one at a time.) */
.dm-status,
.ig-status {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  font-size: var(--dm-fs-sm);
  font-weight: 500;
  color: var(--dm-text-muted);
  min-height: 1.25rem;
}

.dm-status:not(:empty)::before,
.ig-status:not(:empty)::before {
  content: "";
  width: 0.45rem;
  height: 0.45rem;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}

.dm-status.text-success, .ig-status.text-success { color: var(--dm-ok) !important; }
.dm-status.text-danger,  .ig-status.text-danger  { color: var(--dm-danger) !important; }

/* --- Empty state ---
   Icon, then heading, then hint — OnPlan's shape for "nothing here". */
.dm-empty {
  text-align: center;
  color: var(--dm-text-muted);
  padding: 2.5rem 1rem !important;
}

.dm-empty-icon {
  color: var(--dm-text-faint);
  opacity: .5;
  font-size: 2rem;
  line-height: 1;
  margin-bottom: 0.75rem;
}

.dm-empty-title {
  font-weight: 600;
  color: var(--dm-text);
  margin-bottom: 0.15rem;
}

.dm-empty-hint {
  font-size: var(--dm-fs-sm);
  margin: 0;
}

/* --- Help note under a page head --- */
.dm-note {
  font-size: var(--dm-fs-sm);
  color: var(--dm-text-muted);
  margin-bottom: 1rem;
}

/* ==========================================================================
   6. Tables
   ========================================================================== */

.table {
  --bs-table-hover-bg: var(--dm-surface-hover);
  --bs-table-striped-bg: var(--dm-surface-sunken);
  color: var(--dm-text);
  border-color: var(--dm-line);
  margin-bottom: 0;
}

.table > thead th {
  background: var(--dm-surface-sunken);
  border-bottom: 1px solid var(--dm-line-strong);
  white-space: nowrap;
  vertical-align: bottom;
  padding-top: 0.55rem;
  padding-bottom: 0.5rem;
}

.table > tbody > tr > td {
  border-color: var(--dm-line);
  vertical-align: middle;
}

/* Numeric columns: right-aligned and lined up on the digit. */
.table td.text-end, .table th.text-end,
.dm-num {
  font-variant-numeric: tabular-nums;
}

/* --- In-table column filters (the server-paged list screens) -----------------
   A second header row of narrow text boxes, one per filterable column. Deliberately the same
   shape as the Products grid's filter row so the two read as one feature, but this one submits
   to the server — see ListHtmlExtensions.ColumnFilter for why. */
/* Qualified to outrank `.dm-scroll > .table > thead > tr > th`, which makes every heading cell
   sticky at top:0. Inheriting that verbatim would park the filter row ON TOP of the headings it
   belongs under, so it gets its own offset — set precisely by column-filter.js, because the
   heading row is a different height on each screen and the 2.35rem here is only a fallback. */
.dm-scroll > .table > thead > tr.dm-filter-row > th {
  position: sticky;
  top: 2.35rem;
  z-index: 2;
  padding: 2px 4px;
  background: var(--dm-surface);
  border-bottom: 1px solid var(--dm-line-strong);
  vertical-align: middle;
}

.dm-filter-row .dm-col-filter {
  font-size: var(--dm-fs-sm);
  min-width: 5rem;
}

/* A column with no filter still needs its cell, or the row loses alignment with the headings
   above it. Kept visually empty rather than omitted. */
.dm-scroll > .table > thead > tr.dm-filter-row > th:empty {
  background: var(--dm-surface-sunken);
}

/* --- Pack families (Ranging → Products, "Group by family") ------------------
   A block of rows that are the same product in different pack sizes, introduced by a banner that
   names the family and says what the store does with it. The banner is where the grouping is
   stated, which is what lets the member rows stay ordinary rows: no rail, no indent, no glyph
   competing with the misclassified tint and the DOS badges they already carry. */
/* Qualified past `.table > :not(caption) > * > *`, which is where Bootstrap paints striping and
   hover as an inset shadow over the cell background. Left at plain `.dm-fam-head > td` the two
   selectors tie on specificity, and the banner would alternate light and dark down the page and
   lift under the pointer — it is chrome, not one of the rows being striped. */
.table > tbody > tr.dm-fam-head > td {
  padding-top: 0.3rem;
  padding-bottom: 0.3rem;
  font-size: var(--dm-fs-sm);
  color: var(--dm-text-muted);
  /* A tint of the accent rather than --dm-accent-soft: the chip can afford that much colour over a
     few characters, a band across twelve columns cannot. It also has to stay distinguishable from
     the striped rows, which are already --dm-surface-sunken. */
  background-color: rgba(var(--dm-accent-rgb), 0.07);
  box-shadow: inset var(--dm-rail) 0 0 0 var(--dm-accent);
}

/* Hairlines above the banner and below the last member, so a family reads as a block without
   boxing rows that already sit inside a striped table. Qualified down to `.table > tbody > tr` to
   outweigh the blanket `border-color: var(--dm-line)` a few rules above, which is otherwise the
   more specific selector and quietly flattens these back to an ordinary row divider. */
.table > tbody > tr.dm-fam-head > td { border-top: 1px solid var(--dm-line-strong); }
.table > tbody > tr.dm-fam-end > td { border-bottom: 1px solid var(--dm-line-strong); }

.dm-fam-head .bi { color: var(--dm-accent); margin-right: 0.35rem; }

.dm-fam-head-name {
  font-weight: 600;
  color: var(--dm-text);
}

/* The counts trail the name and are separated by dots rather than punctuation typed into the
   markup, so a missing count leaves no orphaned separator behind. */
.dm-fam-head-meta > span::before {
  content: "·";
  margin: 0 0.4rem;
  color: var(--dm-text-faint);
}

.dm-fam-head-gap { color: var(--dm-text-muted); font-weight: 600; }

.dm-fam-chip, .dm-fam-ratio {
  display: inline-block;
  margin-left: 0.4rem;
  padding: 0.05rem 0.35rem;
  border-radius: var(--dm-radius-sm);
  font-size: var(--dm-fs-micro);
  vertical-align: middle;
  white-space: nowrap;
}

.dm-fam-chip {
  background: var(--dm-accent-soft);
  color: var(--dm-accent-hover);
  font-weight: 600;
}

.dm-fam-ratio {
  background: var(--dm-surface-hover);
  color: var(--dm-text-muted);
  font-weight: 500;
}

/* Not ranged here at all. Hatched rather than tinted: a flat wash reads as another status colour
   in a grid that already spends its colours on warnings, whereas a texture reads as "this row is
   not the same kind of thing as its neighbours" — which is exactly the claim. */
.dm-fam-ghost > td {
  background-image: repeating-linear-gradient(45deg,
      transparent, transparent 6px,
      rgba(var(--dm-accent-rgb), 0.05) 6px, rgba(var(--dm-accent-rgb), 0.05) 12px);
  color: var(--dm-text-muted);
}

.dm-fam-ghost .dm-copy-text { color: var(--dm-text-muted); }

.badge.dm-fam-gap {
  background: var(--dm-surface);
  color: var(--dm-text-muted);
  border: 1px dashed var(--dm-line-strong);
  font-weight: 500;
}

/* Action cells never wrap and never grow. */
.dm-actions, .ig-actions, .table td .dm-actions {
  width: 1%;
  white-space: nowrap;
  text-align: right;
}

.dm-actions .btn + .btn,
.dm-actions .btn + form,
.dm-actions form + form,
.table td .btn + .btn,
.table td .btn + form,
.table td form + form {
  margin-left: 0.25rem;
}

/* --- Sortable headers ---
   The heading itself is the link, so the whole label is the hit target and the
   sort survives in the URL (shareable, and it comes back on refresh). */
.tt-sort {
  color: inherit;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 0.2rem;
  white-space: nowrap;
}

/* Opt-in on the th for a label too long to sit on one line. Without it the nowrap above makes the
   header the column's minimum width, and in a crowded grid that slack comes out of whichever
   neighbour has no fixed width — the name column, which is the one that can least afford it. */
.th-wrap .tt-sort {
  white-space: normal;
}

.tt-sort:hover {
  color: var(--dm-accent);
}

/* --- Banded column group ---
   For a grid whose columns come from more than one table: a spanning banner names the block and
   a hairline down each edge carries that boundary through the rows, so a value in the middle of a
   wide grid still reads as belonging to the group rather than to the column beside it. */
.ig-group-row th {
  border-bottom: 0;
  padding-bottom: 0.1rem;
  font-size: var(--dm-fs-micro);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--dm-text-muted);
}

.dm-col-group {
  background: var(--dm-surface-sunken);
  border-radius: var(--dm-radius-sm) var(--dm-radius-sm) 0 0;
}

.dm-col-group,
.inline-grid .dm-rule-start {
  border-left: 1px solid var(--dm-line);
}

.dm-col-group,
.inline-grid .dm-rule-end {
  border-right: 1px solid var(--dm-line);
}

.tt-sort.is-sorted {
  color: var(--dm-accent);
}

.tt-ind {
  color: var(--dm-accent);
  font-size: 0.7em;
}

/* Legacy hook: kept for any table still driven by table-tools.js. */
th.tt-sortable {
  cursor: pointer;
  user-select: none;
}

th.tt-sortable:hover {
  color: var(--dm-accent);
  background: var(--dm-accent-soft);
}

/* --- Pager ---
   Sits in the card footer as the counterpart to the toolbar: what you are looking
   at on the left, how much of it you see in the middle, where to go on the right. */
.dm-pager {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
  padding: 0.5rem 0.75rem;
  background: var(--dm-surface-sunken);
  border-top: 1px solid var(--dm-line);
}

.dm-pager-count {
  font-size: var(--dm-fs-sm);
  color: var(--dm-text-muted);
}

.dm-pager-count strong {
  color: var(--dm-text);
  font-variant-numeric: tabular-nums;
}

.dm-pager-size {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.dm-pager-size .form-select {
  width: auto;
  font-size: var(--dm-fs-sm);
  padding: 0.15rem 1.6rem 0.15rem 0.5rem;
}

.dm-pager .pagination {
  margin-left: auto;
  --bs-pagination-padding-y: 0.2rem;
  --bs-pagination-padding-x: 0.55rem;
  --bs-pagination-font-size: var(--dm-fs-sm);
  --bs-pagination-border-color: var(--dm-line);
  --bs-pagination-hover-bg: var(--dm-accent-soft);
  --bs-pagination-disabled-color: var(--dm-text-faint);
  --bs-pagination-disabled-border-color: var(--dm-line);
}

.dm-pager .page-link {
  font-variant-numeric: tabular-nums;
}

@media (max-width: 575.98px) {
  .dm-pager { gap: 0.5rem; }
  .dm-pager .pagination { margin-left: 0; }
}

/* Clickable rows (stores). The whole row is the control, so say so. */
tr.row-link {
  cursor: pointer;
}

tr.row-link:hover > td {
  background: var(--dm-surface-hover);
}

tr.row-link:hover > td:first-child {
  box-shadow: inset var(--dm-rail) 0 0 var(--dm-accent);
}

/* ==========================================================================
   7. Inline-editable grids
   --------------------------------------------------------------------------
   Fields sit flush in the cell until you touch them, so a grid of 300 inputs
   still reads as a table rather than as a wall of boxes.
   ========================================================================== */

.inline-grid {
  margin-bottom: 0;
}

.inline-grid td {
  vertical-align: middle;
  padding: 0.2rem 0.5rem;
  position: relative;
}

.inline-grid tbody tr > td:first-child {
  box-shadow: inset var(--dm-rail) 0 0 transparent;
}

.inline-grid .form-control,
.inline-grid .form-select {
  height: 1.85rem;
  padding: 0.1rem 0.4rem;
  font-size: var(--dm-fs-sm);
  border-color: transparent;
  background-color: transparent;
  box-shadow: none;
}

.inline-grid tr:hover .form-control:not(:disabled),
.inline-grid tr:hover .form-select:not(:disabled) {
  border-color: var(--dm-line);
  background-color: var(--dm-surface);
}

.inline-grid .form-control:focus,
.inline-grid .form-select:focus {
  border-color: var(--dm-accent);
  background-color: var(--dm-surface);
  box-shadow: 0 0 0 0.2rem rgba(var(--dm-accent-rgb), .18);
}

.inline-grid .form-control:disabled,
.inline-grid .form-select:disabled {
  background-color: transparent;
  color: var(--dm-text);
  opacity: 1;
}

.inline-grid .form-select {
  padding-right: 1.6rem;
  background-position: right 0.35rem center;
}

/* --- Row state: the rail ---
   The tint is a plain background-color, NOT Bootstrap 5.3's --bs-table-bg-state.
   That variable is applied as `box-shadow: inset 0 0 0 9999px`, which the rail —
   also a box-shadow, on the same first cell — would replace outright, leaving the
   first column as the one untinted cell in the row. These grids are table-hover
   rather than table-striped, so there is no striping shadow to lose to either. */
.inline-grid tr.ig-dirty > td {
  background: var(--dm-warn-soft);
}
.inline-grid tr.ig-dirty > td:first-child {
  box-shadow: inset var(--dm-rail) 0 0 var(--dm-warn-solid);
}

.inline-grid tr.ig-ok > td {
  background: var(--dm-ok-soft) !important;
  transition: background .3s;
}
.inline-grid tr.ig-ok > td:first-child {
  box-shadow: inset var(--dm-rail) 0 0 var(--dm-ok);
}

.inline-grid tr.ig-err > td {
  background: var(--dm-danger-soft) !important;
  transition: background .3s;
}
.inline-grid tr.ig-err > td:first-child {
  box-shadow: inset var(--dm-rail) 0 0 var(--dm-danger);
}

/* The add row. Blue rail = "this is where new records go", matching the brand
   mark and the active nav item. */
.inline-grid tr.ig-new > td {
  background: var(--dm-accent-soft);
}
.inline-grid tr.ig-new > td:first-child {
  box-shadow: inset var(--dm-rail) 0 0 var(--dm-accent);
}

.inline-grid tr.ig-new .form-control,
.inline-grid tr.ig-new .form-select {
  border-color: var(--dm-line);
  background-color: var(--dm-surface);
}

.inline-grid .ig-actions {
  width: 1%;
  white-space: nowrap;
  text-align: right;
}

/* Save is the row's own verb — keep it quiet until there is something to save. */
.inline-grid .ig-save:disabled {
  opacity: .3;
}

.inline-grid tr.ig-dirty .ig-save {
  box-shadow: 0 0 0 0.15rem rgba(25, 135, 84, .2);
}

/* Constrains a short list so a single name field is not 1600px wide. Goes on a
   wrapper around BOTH the filter card and the data card — constraining only the
   table leaves the filters floating above a narrower island. */
.dm-grid-narrow { max-width: 48rem; }
.dm-grid-medium { max-width: 64rem; }

/* ==========================================================================
   8. Products grid
   ========================================================================== */

.grid-table {
  font-size: var(--dm-fs-sm);
  white-space: nowrap;
  margin-bottom: 0;
}

.grid-table thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: var(--dm-surface-sunken);
  border-bottom: 1px solid var(--dm-line-strong);
}

.grid-table thead tr.filter-row th {
  top: 2.35rem;
  padding: 2px;
  background: var(--dm-surface);
}

.grid-table .grid-sort {
  cursor: pointer;
  user-select: none;
}

.grid-table .grid-sort:hover {
  color: var(--dm-accent);
}

.grid-table td {
  padding: 1px 4px;
  vertical-align: middle;
}

.grid-table .cell-inp {
  min-width: 5rem;
  height: 1.65rem;
  padding: 0 4px;
  font-size: var(--dm-fs-sm);
  border-color: transparent;
  background: transparent;
}

/* Description is nvarchar(120) and is what people read the grid by, so it gets
   room; the 5rem default cut it off after roughly a dozen characters. National
   ID and Size are middling-length text and get a smaller bump. */
.grid-table .col-description .cell-inp { min-width: 24rem; }

.grid-table .col-nationalId .cell-inp,
.grid-table .col-sizeDesc .cell-inp { min-width: 8rem; }

.grid-table tr:hover .cell-inp:not(:disabled) {
  border-color: var(--dm-line);
  background: var(--dm-surface);
}

.grid-table .cell-inp:focus {
  border-color: var(--dm-accent);
  background: var(--dm-surface);
}

/* A foreign-key cell opens a picker rather than typing — it behaves like a
   button, so it gets the affordance of one on hover. */
.grid-table .cell-fk {
  cursor: pointer;
  min-width: 8rem;
}

.grid-table .cell-fk:hover {
  background: var(--dm-accent-soft);
  box-shadow: inset 0 -1px 0 var(--dm-accent);
}

.grid-table .grid-actions {
  width: 1%;
  position: sticky;
  left: 0;
  background: var(--dm-surface);
  z-index: 1;
  box-shadow: 1px 0 0 var(--dm-line);
  /* Two buttons live here — save the row, and set a replacement barcode. Without this the
     minimum-width column wraps them onto two lines and every row grows taller. */
  white-space: nowrap;
}

.grid-table .grid-actions .btn + .btn {
  margin-left: 2px;
}

/* Selection column sits left of the save button and, like it, stays put while the grid scrolls
   sideways — you need to see what is ticked while working in a far-right column. */
.grid-table .grid-select {
  width: 1%;
  position: sticky;
  left: 0;
  background: var(--dm-surface);
  z-index: 1;
  text-align: center;
  padding: 1px 6px;
}

.grid-table .grid-actions {
  left: 2.1rem;
}

.grid-table tr.row-selected > td {
  background: var(--dm-accent-soft);
}

.grid-table tr.row-selected > td.grid-select,
.grid-table tr.row-selected > td.grid-actions {
  background: var(--dm-accent-soft);
}

.dm-selcount {
  font-size: var(--dm-fs-sm);
  color: var(--dm-text-muted);
  white-space: nowrap;
}

/* The toolbar's 'i' button: same height as the other small buttons, but square. */
.dm-info-btn {
  padding: 0.15rem 0.4rem;
  line-height: 1;
}

.dm-help-head {
  margin-top: 1.1rem;
  font-size: var(--dm-fs-base);
  font-weight: 600;
}

.dm-help-list dt { font-weight: 600; }
.dm-help-list dd { margin-bottom: 0.5rem; }
.dm-help-list kbd { font-size: 0.8em; }

.grid-table tr.saved-ok td  { background: var(--dm-ok-soft) !important;     transition: background .3s; }
.grid-table tr.saved-err td { background: var(--dm-danger-soft) !important; transition: background .3s; }

#colMenu { min-width: 16rem; }
#colMenu .form-check { margin-bottom: 2px; }
#colMenu .form-check-label { font-size: var(--dm-fs-sm); }

/* ==========================================================================
   9. Movement view
   ========================================================================== */

.mv-filters .form-label {
  font-size: var(--dm-fs-micro);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--dm-text-muted);
  margin-bottom: 0.2rem;
}

.mv-pick {
  border: 1px solid var(--dm-line);
  border-radius: var(--dm-radius-sm);
  background: var(--dm-surface);
  overflow: hidden;
}

.mv-pick-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.2rem 0.45rem;
  background: var(--dm-surface-sunken);
  border-bottom: 1px solid var(--dm-line);
  font-size: var(--dm-fs-micro);
}

.mv-pick-head a { text-decoration: none; font-weight: 600; }
.mv-pick-count { color: var(--dm-text-muted); font-weight: 500; }

/* Scoped through .mv-pick so it outranks the generic .dm-filters .form-control
   sizing — this input is chrome inside a facet, not a filter field in its own right. */
.mv-pick .mv-pick-filter {
  border: 0;
  border-bottom: 1px solid var(--dm-line);
  border-radius: 0;
  height: 1.7rem;
  padding: 1px 0.45rem;
  font-size: var(--dm-fs-sm);
}

.mv-pick .mv-pick-filter:focus {
  box-shadow: none;
  border-bottom-color: var(--dm-accent);
}

.mv-pick-list {
  height: 8.5rem;
  overflow-y: auto;
  padding: 0.3rem 0.45rem;
}

.mv-pick-list .form-check { margin-bottom: 0; font-size: var(--dm-fs-sm); min-height: auto; }
.mv-pick-list .form-check-label { cursor: pointer; }
.mv-pick-list .form-check.mv-hide-filter,
.mv-pick-list .form-check.mv-hide-owner { display: none; }

.mv-presets { display: flex; flex-wrap: wrap; gap: 0.35rem; }
.mv-presets .btn { font-size: var(--dm-fs-sm); }

.mv-dims-list { height: auto; max-height: 18rem; display: flex; flex-wrap: wrap; gap: 0 2rem; }
.mv-dim-block { break-inside: avoid; }
.mv-dim-block-wide { column-count: 2; column-gap: 2rem; }
.mv-dim-block-wide .form-check { break-inside: avoid; }

.mv-dim-group {
  font-size: var(--dm-fs-micro);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--dm-text-muted);
  margin: 0.35rem 0 0.15rem;
}

.mv-dims-list .form-check { margin-bottom: 1px; font-size: var(--dm-fs-sm); min-height: auto; }
.mv-dims-list .form-check-label { cursor: pointer; }
.mv-facet[hidden] { display: none !important; }

/* Says how much of a facet you are actually looking at, since the candidates are
   loaded on demand and capped. */
.mv-pick-note {
  font-size: var(--dm-fs-micro);
  color: var(--dm-text-muted);
  padding: 0 0.45rem 0.3rem;
  min-height: 0.9rem;
}

.mv-pick-note:empty {
  padding-bottom: 0;
}

.mv-pick-remove {
  text-decoration: none;
  font-size: 0.95rem;
  line-height: 1;
  color: var(--dm-text-faint);
  margin-left: 0.4rem;
}
.mv-pick-remove:hover { color: var(--dm-danger); }

.mv-grid { font-size: var(--dm-fs-sm); }

.mv-grid thead th {
  position: sticky;
  top: 0;
  background: var(--dm-surface-sunken);
  z-index: 2;
  white-space: nowrap;
}

.mv-grid thead th a { color: var(--dm-text-muted); }
.mv-grid thead th a:hover { color: var(--dm-accent); }

.mv-grid td.mv-num, .mv-grid th.mv-num {
  text-align: right;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}

.mv-grid tfoot td {
  font-weight: 600;
  border-top: 2px solid var(--dm-line-strong);
  background: var(--dm-surface-sunken);
  position: sticky;
  bottom: 0;
  color: var(--dm-ink);
}

.mv-scroll { max-height: 65vh; overflow: auto; }

/* ==========================================================================
   9b. Dashboard stat cards
   --------------------------------------------------------------------------
   OnPlan's .stat-card, to the pixel: a 4px left accent that lifts on hover.
   A check is a number and a verdict; the accent carries the verdict so the
   number itself never has to be red to be read.
   ========================================================================== */

.dm-stat {
  position: relative;
  height: 100%;
  background: var(--dm-surface);
  border: 1px solid var(--dm-line);
  border-left: 4px solid var(--dm-line-strong);
  border-radius: var(--dm-radius);
  box-shadow: var(--dm-shadow);
  padding: 0.9rem 1rem;
  display: flex;
  flex-direction: column;
  transition: transform 0.2s;
}

.dm-stat:hover {
  transform: translateY(-2px);
}

.dm-stat-title {
  font-size: var(--dm-fs-micro);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--dm-text-muted);
  margin: 0 0 0.35rem;
}

.dm-stat-value {
  font-size: 1.9rem;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--dm-ink);
  margin-bottom: 0.4rem;
}

.dm-stat-message {
  font-size: var(--dm-fs-sm);
  color: var(--dm-text-muted);
  margin: 0;
}

.dm-stat-link {
  margin-top: auto;
  padding-top: 0.7rem;
  font-size: var(--dm-fs-sm);
  font-weight: 500;
}

/* Three colours, not four. Red is critical, yellow is a warning, and green covers both "nothing to do"
   and "worth knowing, nothing is wrong". Info used to be blue, which read as a fourth severity sitting
   between ok and warning and made the row harder to scan than it needed to be. */
.dm-stat--ok      { border-left-color: var(--dm-ok); }
.dm-stat--info    { border-left-color: var(--dm-ok); }
.dm-stat--warning { border-left-color: var(--dm-warn-solid); }
.dm-stat--error   { border-left-color: var(--dm-danger); }

/* Context, not a verdict. Every other modifier says how worried to be, which is
   the wrong thing to say about a figure nobody is expected to act on — a green
   1.4 million reads as "under control" and an amber one reads as an alarm that
   cannot be silenced. Neither is true, so this one stays out of the way. */
.dm-stat--neutral { border-left-color: var(--dm-line-strong); }

.dm-stat--ok .dm-stat-value      { color: var(--dm-ok); }
.dm-stat--info .dm-stat-value    { color: var(--dm-ok); }
.dm-stat--warning .dm-stat-value { color: var(--dm-warn); }
.dm-stat--error .dm-stat-value   { color: var(--dm-danger); }
.dm-stat--neutral .dm-stat-value { color: var(--dm-text); }

/* A stat card that is also a control. The whole card is the target rather than a
   small link in a corner — the number is the thing being clicked. */
.dm-stat-link {
  display: block;
  text-decoration: none;
  color: inherit;
  cursor: pointer;
  transition: transform 0.12s, box-shadow 0.12s;
}

.dm-stat-link:hover {
  transform: translateY(-2px);
  box-shadow: 0 2px 8px rgb(0 0 0 / 12%);
  color: inherit;
}

.dm-stat-link:focus-visible {
  outline: 2px solid var(--dm-accent);
  outline-offset: 2px;
}

/* Selection is shown with a ring rather than by changing the fill: the left border
   already carries severity, and recolouring it would make "selected" and "urgent"
   compete for the same signal. */
.dm-stat--selected {
  box-shadow: 0 0 0 2px var(--dm-accent);
}

/* --- Dashboard groups ----------------------------------------------------- */

.dm-stat-group + .dm-stat-group {
  margin-top: 1.6rem;
}

.dm-stat-group-head {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
  margin-bottom: 0.6rem;
}

.dm-stat-group-title {
  font-size: var(--dm-fs-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--dm-ink);
  margin: 0;
}

.dm-stat-group-meta {
  font-size: var(--dm-fs-sm);
  color: var(--dm-text-muted);
}

/* ==========================================================================
   10. Defined markets
   ========================================================================== */

.dm-transfer .card-header {
  padding: 0.45rem 0.75rem;
}

.dm-transfer .dm-scroll {
  max-height: 58vh;
}

/* ==========================================================================
   10b. Sales by date
   --------------------------------------------------------------------------
   The grid has a row per month whether or not that month loaded, so a blank
   row is data. It is dimmed rather than coloured: there is nothing wrong with
   a month that has not been imported yet, and the state rail (see the note at
   the top of this file) is reserved for verdicts.
   ========================================================================== */

.dm-row-empty > td {
  color: var(--dm-text-muted);
  background-image: repeating-linear-gradient(
    -45deg,
    transparent,
    transparent 7px,
    var(--dm-surface-sunken) 7px,
    var(--dm-surface-sunken) 14px
  );
}

/* ==========================================================================
   10d. Product images
   --------------------------------------------------------------------------
   Unlike flows, these come off an image pipeline: the server resizes to the
   requested box before sending, so the browser is never scaling a 512px photo
   down into a 40px cell. Hence `contain` rather than `cover` throughout — a
   product cropped to fill a square is a product you can no longer identify.
   ========================================================================== */

.dm-prod-thumb-col {
  width: 1%;
  padding-right: 0;
}

.dm-prod-thumb {
  display: block;
  width: 40px;
  height: 40px;
  object-fit: contain;
  border: 1px solid var(--dm-line);
  border-radius: calc(var(--dm-radius) / 2);
  background: var(--dm-surface-sunken);
  cursor: pointer;
  transition: transform 0.15s, border-color 0.15s;
}

.dm-prod-thumb:hover {
  transform: scale(1.5);
  border-color: var(--dm-accent);
}

/* The frame is drawn whether or not there is a picture, so rows stay aligned
   and the grid does not reflow as thumbnails load. */
.dm-prod-thumb--none {
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--dm-text-muted);
  font-size: 0.9rem;
  cursor: default;
}

.dm-prod-thumb--none:hover {
  transform: none;
  border-color: var(--dm-line);
}

/* The coverage list's image cell holds the frame AND its buttons. .dm-prod-thumb is
   display:block for the grid, where it is alone in its column, so it has to be
   relaxed here or every row grows to two stacked lines. Written against the cell
   rather than the span so a thumbnail restored by script after an upload — a real
   <img>, not the placeholder — lines up the same way. */
.dm-image-cell {
  white-space: nowrap;
}

.dm-image-cell .dm-prod-thumb {
  display: inline-flex;
  vertical-align: middle;
  margin-right: 0.4rem;
}

/* The larger view, on the edit form and in the single-product editor. */
.dm-prod-image-frame {
  display: flex;
  align-items: center;
  justify-content: center;
  aspect-ratio: 1;
  padding: 0.5rem;
  border: 1px solid var(--dm-line);
  border-radius: var(--dm-radius);
  background: var(--dm-surface-sunken);
}

.dm-prod-image {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
}

.dm-prod-image-none {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.35rem;
  color: var(--dm-text-muted);
  font-size: 0.8rem;
  text-align: center;
}

.dm-prod-image-none i {
  font-size: 1.6rem;
}

/* In the single-product editor the frame would otherwise stretch to the dialog's
   full width, and being square that makes it taller than everything below it. */
.pe-image {
  max-width: 200px;
}

/* The file input behind an "Upload…" label: invisible, still focusable, and — the
   part that matters — STILL IN FLOW.

   Bootstrap's .visually-hidden was the obvious choice and is the wrong one here. It
   is position:absolute, and with no positioned ancestor the containing block is the
   document, so each input escapes the .dm-scroll box that clips the table. One per
   row, a hundred rows, the last landing at y≈5,600: the page grew to four times its
   content and scrolled into empty space. `hidden` avoided that only by being
   display:none, which is exactly why the control could not be reached by keyboard.

   Zero-sized and transparent keeps both properties — nothing to see, nothing to
   lay out, and focusable because it is neither display:none nor visibility:hidden. */
[data-image-upload] {
  position: static;
  width: 0;
  height: 0;
  padding: 0;
  margin: 0;
  border: 0;
  opacity: 0;
  overflow: hidden;
}

/* --- Enlarged view -------------------------------------------------------- */

/* Anything that opens the lightbox says so. .dm-prod-thumb already had a pointer
   for its own reasons; .dm-prod-image did not, and a large picture that silently
   responds to a click is one nobody discovers. */
[data-image-zoom] {
  cursor: zoom-in;
}

[data-image-zoom]:focus-visible {
  outline: 2px solid var(--dm-accent);
  outline-offset: 2px;
}

/* Checkerboard, the same device the flow viewer uses: product packshots are
   frequently cut out on transparency, and on a white modal body a white-backed
   pack has no edges at all. */
.dm-prod-stage {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 40vh;
  padding: 0.75rem;
  background:
    repeating-conic-gradient(var(--dm-surface-sunken) 0% 25%, transparent 0% 50%)
    50% / 18px 18px;
}

/* The stored image is capped at 512px, so it is shown at its own size rather than
   stretched to fill the dialog — upscaling a packshot to 900px makes it look worse,
   not bigger. */
.dm-prod-full {
  max-width: 100%;
  max-height: 70vh;
  object-fit: contain;
}

/* ==========================================================================
   11. Forms & modals
   ========================================================================== */

.dm-form-section + .dm-form-section {
  margin-top: 1.35rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--dm-line);
}

.dm-form-section-title {
  font-size: var(--dm-fs-micro);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--dm-accent);
  margin-bottom: 0.75rem;
}

/* The save/cancel pair for a full-page form, pinned below the fields. */
.dm-form-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 1.5rem;
  padding-top: 1rem;
  border-top: 1px solid var(--dm-line);
}

.dm-form-narrow { max-width: 60rem; }

.modal-content {
  border: 0;
  box-shadow: var(--dm-shadow-lg);
}

.modal-header {
  border-bottom-color: var(--dm-line);
  padding: 0.75rem 1rem;
}

.modal-title {
  font-size: var(--dm-fs-lg);
  font-weight: 700;
}

.modal-footer {
  border-top-color: var(--dm-line);
  background: var(--dm-surface-sunken);
  padding: 0.65rem 1rem;
}

.validation-summary-errors ul {
  margin-bottom: 0;
  padding-left: 1.1rem;
}

/* The validation-summary tag helper always renders its container; without this an
   empty red alert sits above every form that has not been submitted yet. */
.validation-summary-valid {
  display: none;
}

/* ==========================================================================
   12. Sign-in
   ========================================================================== */

.auth-body {
  /* Dark, with a blue wash behind the card — the same two colours as the shell,
     so signing in already looks like the product. The band sits behind the card
     rather than above it so the card reads as lit from the page, not floating. */
  background:
    radial-gradient(48rem 30rem at 50% 42%, rgba(var(--dm-accent-rgb), .45) 0%, transparent 68%),
    radial-gradient(70rem 45rem at 50% 120%, rgba(var(--dm-accent-rgb), .18) 0%, transparent 60%),
    var(--dm-ink);
  /* The sign-in page has no fixed header to clear. */
  padding-top: 0;
}

.auth-card {
  width: 100%;
  max-width: 24rem;
  padding: 1rem;
}

.auth-brand {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  margin-bottom: 0.35rem;
  letter-spacing: -0.5px;
}

.auth-brand .bi {
  font-size: 1.75rem;
  color: var(--dm-accent);
  line-height: 1;
}

/* ==========================================================================
   13. Audit trail
   ========================================================================== */

/* The "what changed" column is prose, not a field, so it is the one place on the
   site where a table cell is allowed to wrap. Capped so a bulk entry listing a
   dozen category moves cannot push the numeric column off the right-hand edge.

   SEVENTH, not sixth: the expander column was added in front of the date. These
   two nth-child selectors are the only place the column ORDER is encoded, so
   inserting a column means coming back here. */
.dm-audit td:nth-child(7) {
  white-space: normal;
  min-width: 24rem;
  line-height: 1.35;
}

/* Table and key, under the sentence. Present because an id is what still ties an
   entry to its record after everything in the sentence has been renamed — and
   dimmed because it is the last thing a person reads and the first thing that
   would clutter the column if it were not. */
.dm-audit-ref {
  font-size: var(--dm-fs-sm);
  font-variant-numeric: tabular-nums;
  margin-top: 0.1rem;
}

/* Date over time in one narrow column, rather than a wide single line. */
.dm-audit td:nth-child(2) span {
  margin-left: 0.35rem;
}

.dm-audit .badge .bi {
  margin-right: 0.2rem;
}

/* The expander. A chevron with no button chrome, because a column of outlined
   buttons down the left of a dense table reads as the most important thing on
   the screen, which it is not. */
.dm-audit-toggle {
  padding-right: 0 !important;
}

.dm-audit-expand {
  color: var(--dm-text-muted);
  text-decoration: none;
  line-height: 1;
}

.dm-audit-expand:hover,
.dm-audit-expand:focus {
  color: var(--dm-text);
}

/* The drawer. Inset and tinted so that a fifty-row table opening inside another
   table still reads as belonging to the row above it rather than as the page
   having changed under the reader. */
.dm-audit-detail > td {
  background: var(--dm-surface-sunken);
  padding: 0.5rem 1rem 0.75rem 3rem;
  border-top: 0;
}

.dm-audit-detail-empty {
  font-size: var(--dm-fs-sm);
  padding: 0.25rem 0;
}

.dm-audit-changes {
  background: transparent;
  font-size: var(--dm-fs-sm);
}

.dm-audit-changes th {
  font-weight: 600;
  color: var(--dm-text-muted);
}

/* Before and after WRAP. A description or a note is prose, and clipping it is
   the exact thing this drawer exists to stop happening. */
.dm-audit-changes td {
  white-space: normal;
  vertical-align: top;
}

.dm-audit-record {
  font-weight: 600;
}

/* The old value dimmed and the new one not, so the direction of a change reads
   without anybody consulting the column headings. */
.dm-audit-before {
  color: var(--dm-text-muted);
}

.dm-audit-after {
  color: var(--dm-text);
}

/* ==========================================================================
   14. Product editor (all fields of one product, in sections)
   ========================================================================== */

/* Sections are separated by a rule rather than boxed. With seven of them, cards
   would stack seven borders down a scrolling dialog and make it read as seven
   things rather than one product. */
.pe-group + .pe-group {
  margin-top: 1.1rem;
  padding-top: 1.1rem;
  border-top: 1px solid var(--dm-line);
}

.pe-group-title {
  font-size: var(--dm-fs-sm);
  font-weight: 600;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--dm-text-muted);
  margin-bottom: 0.6rem;
}

/* Labels here are the small caps the filter bar uses, so the dialog reads as
   part of the same workbench rather than as a web form. */
#productEditorModal .form-label {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--dm-text-muted);
  margin-bottom: 0.15rem;
}

/* Read-only values sit on the same baseline as the inputs beside them, so a
   row of mixed fields does not look ragged. Tabular numerals because most of
   them are numbers, dates or ids. */
.pe-ro {
  min-height: calc(1.5em + 0.5rem + 2px);
  padding: 0.25rem 0;
  font-variant-numeric: tabular-nums;
  color: var(--dm-text);
  border-bottom: 1px dashed var(--dm-line);
}

/* The unsaved-changes question, asked where the footer is rather than in a
   second dialog on top. Laid out like the footer it replaces the meaning of. */
.pe-pending:not([hidden]) {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  border-width: 1px 0 0;
  font-size: var(--dm-fs-sm);
}

/* History tab: one entry per recorded change, newest first, separated by a
   rule like the sections of the Details tab. */
.pe-hist-entry + .pe-hist-entry {
  margin-top: 0.9rem;
  padding-top: 0.9rem;
  border-top: 1px solid var(--dm-line);
}

.pe-hist-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin-bottom: 0.25rem;
}

.pe-hist-when {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}

.pe-hist-by {
  font-size: var(--dm-fs-sm);
}

/* The entry's Undo sits at the far end of its heading line. */
.pe-hist-head .pe-hist-undo-all {
  margin-left: auto;
}

.pe-hist-summary {
  margin-bottom: 0.35rem;
}

.pe-hist-fields {
  font-size: var(--dm-fs-sm);
}

.pe-hist-fields th {
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--dm-text-muted);
}

.pe-hist-fields td:first-child {
  width: 12rem;
  color: var(--dm-text-muted);
}

.pe-hist-before {
  width: 35%;
}

.pe-hist-act {
  width: 2rem;
  text-align: center;
}

.pe-hist-note {
  color: var(--dm-warn);
}

/* Already undone by a later change: still readable history, no longer a
   thing to act on. */
.pe-hist-entry.is-undone .pe-hist-fields {
  opacity: 0.6;
}

/* Undone in this window but not saved yet — marked with the "unsaved" amber
   the rest of the site uses, down the left edge. */
.pe-hist-entry.is-pending {
  box-shadow: inset 3px 0 0 var(--dm-warn-solid);
  padding-left: 0.6rem;
}

/* ==========================================================================
   User administration
   Users (list, create, edit) and Job titles. One form per person, with the
   permission table on the right as a live preview. Colours are the tokens
   above and nothing else: blue is "here", amber "unsaved", green "can",
   red "taken away" — the same meanings the rail has everywhere.
   ========================================================================== */

/* --- Page header extras ---------------------------------------------------- */

.dm-seats {
  display: inline-flex;
  align-items: center;
  gap: 0.35rem;
  color: var(--dm-text-muted);
}

.dm-seats strong { color: var(--dm-text); font-weight: 600; }

.dm-seats-bar {
  display: inline-block;
  width: 4.5rem;
  height: 0.35rem;
  border-radius: 1rem;
  background: var(--dm-line);
  overflow: hidden;
}

.dm-seats-bar > span {
  display: block;
  height: 100%;
  background: var(--dm-accent);
}

.dm-seats.is-full { color: var(--dm-warn); }
.dm-seats.is-full .dm-seats-bar > span { background: var(--dm-warn-solid); }

.dm-company-switch .dm-combo { min-width: 15rem; }

.dm-acting {
  box-shadow: inset var(--dm-rail) 0 0 var(--dm-warn-solid);
}

.dm-subnav {
  font-size: var(--dm-fs-sm);
  border-bottom: 1px solid var(--dm-line);
  gap: 1.25rem;
}

.dm-subnav .nav-link {
  padding: 0.35rem 0;
  color: var(--dm-text-muted);
}

.dm-subnav .nav-link.active {
  color: var(--dm-text);
  font-weight: 600;
}

/* --- The list -------------------------------------------------------------- */

.dm-users-table tr.is-inactive > td,
.dm-users-table tr.is-inactive a.dm-user-name { color: var(--dm-text-muted); }

.dm-user-name { font-weight: 500; text-decoration: none; }
.dm-user-name:hover { text-decoration: underline; }

.dm-status-dot {
  font-size: 0.45rem;
  vertical-align: 0.2em;
  margin-right: 0.35rem;
}

tr.is-inactive > td { color: var(--dm-text-muted); }

/* --- The form -------------------------------------------------------------- */

.dm-user-card .form-label { font-size: var(--dm-fs-sm); margin-bottom: 0.2rem; }

/* .dm-card clips its contents (overflow: hidden, for the rounded corners on tables). These cards hold
   chip pickers whose lists must spill out over the next card, so they opt out. */
.dm-user-card { overflow: visible; }

/* The read-only wrapper on a service account's page: a fieldset for its disabling, with none of its look. */
.dm-plain-fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }

.dm-external-hint {
  display: flex;
  gap: 0.5rem;
  align-items: flex-start;
  padding: 0.5rem 0.65rem;
  border-radius: var(--dm-radius-sm);
  background: var(--dm-warn-soft);
  box-shadow: inset var(--dm-rail) 0 0 var(--dm-warn-solid);
  font-size: var(--dm-fs-sm);
}

.dm-external-hint > .bi { color: var(--dm-warn); margin-top: 0.1rem; }
.dm-external-hint[hidden] { display: none; }
.dm-user-card .form-text { font-size: var(--dm-fs-micro); }

.dm-user-facts {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
  margin-top: 0.9rem;
  padding-top: 0.75rem;
  border-top: 1px solid var(--dm-line);
  font-size: var(--dm-fs-sm);
  color: var(--dm-text-muted);
}

.dm-user-invite {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  padding: 0.45rem 0.6rem;
  border-radius: var(--dm-radius-sm);
  background: var(--dm-accent-soft);
  color: var(--dm-text);
  box-shadow: inset var(--dm-rail) 0 0 var(--dm-accent);
}

.dm-derived {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.35rem;
}

.dm-chip-static {
  display: inline-block;
  padding: 0.05rem 0.5rem;
  border-radius: 1rem;
  background: var(--dm-surface-sunken);
  border: 1px solid var(--dm-line);
  font-size: var(--dm-fs-micro);
  color: var(--dm-text-muted);
}

.dm-not-applying {
  padding: 0.5rem 0.6rem;
  border-radius: var(--dm-radius-sm);
  background: var(--dm-danger-soft);
  box-shadow: inset var(--dm-rail) 0 0 var(--dm-danger);
}

/* Sticky, so the preview stays in view while the roles and databases on the
   left are scrolled through — the point of it is to watch it change. Only
   on wide screens, where the two columns sit side by side. */
@media (min-width: 1200px) {
  .dm-perm-card {
    position: sticky;
    top: calc(var(--dm-header-h) + 0.75rem);
  }
}

/* --- Chip picker (multi-select.js) ------------------------------------------
   Same contract as the searchable combo: the <select multiple> stays in the
   DOM holding the value, and is hidden. The list is absolutely positioned,
   so a picker must not sit inside a scrolling container. */
.dm-multi { position: relative; }
.dm-multi > select { display: none; }

.dm-multi-box {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.25rem;
  min-height: calc(1.5em + 0.75rem + 2px);
  padding: 0.2rem 0.35rem;
  cursor: text;
}

.dm-multi-box:focus-within {
  border-color: #86b7fe;
  box-shadow: 0 0 0 0.25rem rgba(var(--dm-accent-rgb), .25);
}

.dm-multi.is-disabled .dm-multi-box { background: var(--dm-surface-sunken); cursor: default; }

.dm-multi-chips { display: contents; }

.dm-multi-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  max-width: 100%;
  padding: 0.05rem 0.2rem 0.05rem 0.55rem;
  border-radius: 1rem;
  background: var(--dm-accent-soft);
  color: var(--dm-accent-hover);
  font-size: var(--dm-fs-sm);
  line-height: 1.5;
}

.dm-multi-chip > span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dm-multi-chip.is-locked {
  padding-right: 0.55rem;
  background: var(--dm-surface-sunken);
  color: var(--dm-text-muted);
  border: 1px solid var(--dm-line);
}

.dm-multi-chip .bi-lock-fill { font-size: 0.65rem; }

.dm-multi-remove {
  border: 0;
  background: transparent;
  color: inherit;
  width: 1.1rem;
  height: 1.1rem;
  line-height: 1;
  border-radius: 50%;
  padding: 0;
  opacity: .7;
}

.dm-multi-remove:hover,
.dm-multi-remove:focus-visible { opacity: 1; background: rgba(var(--dm-accent-rgb), .15); }

.dm-multi-input {
  flex: 1 1 6rem;
  min-width: 6rem;
  border: 0;
  outline: 0;
  padding: 0.15rem 0.2rem;
  background: transparent;
  font-size: var(--dm-fs-base);
}

.dm-multi-list {
  position: absolute;
  z-index: 1060;
  top: 100%;
  left: 0;
  right: 0;
  margin-top: 0.15rem;
  max-height: 17rem;
  overflow-y: auto;
  background: var(--dm-surface);
  border: 1px solid var(--dm-line);
  border-radius: var(--dm-radius);
  box-shadow: var(--dm-shadow);
}

/* Flipped above the box when there is no room below (multi-select.js place()). */
.dm-multi.is-up .dm-multi-list {
  top: auto;
  bottom: 100%;
  margin-top: 0;
  margin-bottom: 0.15rem;
}

.dm-multi-group {
  padding: 0.4rem 0.6rem 0.15rem;
  font-size: var(--dm-fs-micro);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--dm-text-muted);
}

.dm-multi-option {
  display: flex;
  align-items: flex-start;
  gap: 0.5rem;
  padding: 0.3rem 0.6rem;
  font-size: var(--dm-fs-sm);
  cursor: pointer;
}

.dm-multi-option:hover,
.dm-multi-option.is-active { background: var(--dm-accent-soft); }

.dm-multi-option.is-selected .dm-multi-tick { color: var(--dm-accent); }
.dm-multi-option.is-disabled,
.dm-multi-option.is-locked { cursor: default; color: var(--dm-text-muted); }
.dm-multi-option.is-disabled:hover { background: transparent; }

.dm-multi-tick { margin-top: 0.1rem; color: var(--dm-text-faint); }

.dm-multi-text { display: flex; flex-direction: column; min-width: 0; }

.dm-multi-desc {
  font-size: var(--dm-fs-micro);
  color: var(--dm-text-muted);
}

.dm-multi-empty {
  padding: 0.4rem 0.6rem;
  font-size: var(--dm-fs-micro);
  color: var(--dm-text-muted);
}

/* --- The permission table ---------------------------------------------------
   Each cell: the tick that grants the action on the person's own card, and a
   mark for what they END UP WITH from everything together. The mark is what
   the eye reads down a column; the tick is the thing you change. */
.dm-perm-table { margin-bottom: 0.5rem; }

.dm-perm-table .dm-perm-head {
  width: 7.5rem;
  text-align: center;
}

.dm-perm-area {
  font-weight: 500;
  font-size: var(--dm-fs-sm);
}

.dm-perm-cell {
  text-align: center;
  vertical-align: top;
  transition: background-color .2s;
}

.dm-perm-inner {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
}

.dm-perm-inner .form-check-input { margin: 0; }

.dm-perm-mark {
  display: inline-block;
  width: 1.1rem;
  font-size: 1rem;
  line-height: 1;
}

.dm-perm-why {
  min-height: 1em;
  margin-top: 0.1rem;
  font-size: var(--dm-fs-micro);
  line-height: 1.2;
  color: var(--dm-text-muted);
}

.dm-perm-cell.is-held .dm-perm-mark { color: var(--dm-ok); }
.dm-perm-cell.is-role .dm-perm-mark { color: var(--dm-ok); opacity: .75; }
.dm-perm-cell.is-role .dm-perm-why { color: var(--dm-ok); }
.dm-perm-cell.is-denied .dm-perm-mark,
.dm-perm-cell.is-denied .dm-perm-why { color: var(--dm-danger); }
.dm-perm-cell.is-denied .dm-perm-inner .form-check-input:checked { opacity: .5; }
.dm-perm-cell.is-needs-admin .dm-perm-mark { color: var(--dm-text-faint); }
.dm-perm-cell.is-none .dm-perm-mark { color: var(--dm-text-faint); }
.dm-perm-cell.is-na { color: var(--dm-text-faint); font-size: var(--dm-fs-micro); }

/* A cell an unsaved change would alter. The amber of "unsaved", tinted rather
   than railed — a rail is a row's shape, and this is one cell. */
.dm-perm-cell.is-changed { background: var(--dm-warn-soft); }

.dm-perm-table.is-previewing .dm-perm-mark { opacity: .45; }

.dm-perm-legend {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1rem;
  font-size: var(--dm-fs-micro);
  color: var(--dm-text-muted);
}

.dm-legend-held { color: var(--dm-ok); }
.dm-legend-role { color: var(--dm-ok); opacity: .75; }
.dm-legend-denied { color: var(--dm-danger); }
.dm-legend-none { color: var(--dm-text-faint); }

.dm-legend-changed {
  padding: 0 0.35rem;
  border-radius: var(--dm-radius-sm);
  background: var(--dm-warn-soft);
}

.dm-rail-danger { box-shadow: inset var(--dm-rail) 0 0 var(--dm-danger); }

.dm-perm-scope {
  margin-top: 0.9rem;
  padding: 0.5rem 0.65rem;
  border-radius: var(--dm-radius-sm);
  background: var(--dm-surface-sunken);
  border: 1px solid var(--dm-line);
  display: grid;
  gap: 0.15rem;
}

.dm-perm-scope.is-dirty,
#data-access.is-dirty { box-shadow: inset var(--dm-rail) 0 0 var(--dm-warn-solid); }

/* --- Save bar ----------------------------------------------------------------
   Pinned to the bottom of the window above the footer, so Save is always one
   click away however far down the roles or databases go. Amber rail once
   there is something to save. */
.dm-savebar {
  position: sticky;
  bottom: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 1rem;
  padding: 0.55rem 0.8rem;
  background: var(--dm-surface);
  border: 1px solid var(--dm-line);
  border-radius: var(--dm-radius);
  box-shadow: 0 -2px 8px rgba(0, 0, 0, .06);
}

.dm-savebar-text {
  font-size: var(--dm-fs-sm);
  color: var(--dm-text-muted);
}

.dm-savebar.is-dirty {
  background: var(--dm-warn-soft);
  box-shadow: inset var(--dm-rail) 0 0 var(--dm-warn-solid), 0 -2px 8px rgba(0, 0, 0, .06);
}

.dm-savebar.is-dirty .dm-savebar-text { color: var(--dm-warn); font-weight: 500; }

/* --- Job titles -------------------------------------------------------------- */

.dm-standard-roles > summary {
  cursor: pointer;
  list-style: none;
}

.dm-standard-roles > summary::-webkit-details-marker { display: none; }

.dm-standard-roles > summary::before {
  content: "\F285"; /* bi-chevron-right */
  font-family: "bootstrap-icons";
  display: inline-block;
  margin-right: 0.4rem;
  font-size: 0.7rem;
  transition: transform .15s;
}

.dm-standard-roles[open] > summary::before { transform: rotate(90deg); }

.dm-title-grid td { width: 8.5rem; }

.dm-tri .btn {
  padding: 0.1rem 0.45rem;
  line-height: 1.2;
}

.dm-tri .btn-check:disabled + .btn { opacity: .35; }

/* ==========================================================================
   15. Utilities, responsive, motion
   ========================================================================== */

.dm-hint {
  font-size: var(--dm-fs-sm);
  color: var(--dm-text-muted);
}

/* --- Click-to-copy ---
   Both variants are <button> so they are reachable by keyboard and announced as an action, while
   looking like whatever they sit among. Only the cursor and the focus ring say they do anything. */
.dm-copy {
  /* Has to keep looking exactly like the plain .badge siblings in the same cell. */
  border: 0;
  /* The feedback text ("Copied") is narrower than the label, and a badge that shrinks mid-click
     drags the badges after it leftwards across the flags cell. */
  min-width: 6.5rem;
}

/* No min-width here, unlike .dm-copy: this one is alone in its cell and the column is already as
   wide as the longest barcode in the page, so the button narrowing to "Copied" moves nothing. */
.dm-copy-text {
  /* The opposite job: strip every trace of a button so the cell still reads as its own text. */
  border: 0;
  padding: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: inherit;
}

/* :not(:disabled) only to outrank Bootstrap's reboot, which sets cursor: pointer on
   button:not(:disabled) — a bare .dm-copy loses to it on specificity and these render with the
   plain pointer instead of the copy cursor. */
.dm-copy:not(:disabled),
.dm-copy-text:not(:disabled) {
  cursor: copy;
}

/* The only affordance, and only on hover — a permanent underline on every barcode in a
   200-row grid would read as a column of links. */
.dm-copy-text:hover {
  text-decoration: underline dotted;
}

@media (max-width: 991.98px) {
  /* Below lg the navbar collapses, so the menu drops out of the 60px bar and
     the bar can no longer be a fixed height. */
  .dm-nav {
    position: static;
    height: auto;
    padding-top: 0.35rem;
    padding-bottom: 0.35rem;
  }

  body {
    padding-top: 0;
  }

  .dm-nav .navbar-collapse {
    background: var(--dm-ink);
  }

  .dm-nav-tools {
    width: 100%;
    padding-top: 0.6rem;
    margin-top: 0.5rem;
    border-top: 1px solid rgba(255, 255, 255, .12);
    justify-content: space-between;
  }

  .dm-page-head {
    align-items: flex-start;
    flex-direction: column;
    gap: 0.6rem;
  }
}

@media (max-width: 575.98px) {
  .dm-card-toolbar { gap: 0.35rem; }
  .dm-filters { padding: 0.65rem; }
  .dm-footer > .container-fluid { gap: 0.5rem; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

@media print {
  .dm-nav, .dm-footer, .dm-card-toolbar, .dm-filters, .dm-page-actions { display: none !important; }
  body { padding-top: 0; }
  .dm-card { border: 0; box-shadow: none; }
  .dm-scroll { max-height: none; overflow: visible; }
}

/* ==========================================================================
   Hierarchy flow (experimental)
   The drag-to-order screen at Hierarchies/Flow. Six columns of siblings, one
   per hierarchy level, dragged into the order the flow should follow.
   ========================================================================== */

.hf-columns {
  display: flex;
  align-items: stretch;
  gap: 0;
  overflow-x: auto;
  min-height: 22rem;
}

.hf-col {
  flex: 1 0 11rem;
  min-width: 11rem;
  padding: 0.5rem;
  border-right: 1px solid var(--dm-line);
}

.hf-col:last-child { border-right: 0; }

/* A level with nothing to choose between still numbers every code beneath it;
   it is hidden rather than removed. */
.hf-col-hidden { display: none; }

.hf-col-head {
  font-size: var(--dm-fs-micro);
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--dm-text-muted);
  padding: 0 0.15rem 0.4rem;
}

.hf-col-empty {
  font-size: var(--dm-fs-sm);
  color: var(--dm-text-faint);
  padding: 0.4rem 0.15rem;
}

.hf-list {
  display: flex;
  flex-direction: column;
  gap: 0.25rem;
}

.hf-list-over { background: var(--dm-surface-hover); border-radius: var(--dm-radius-sm); }

.hf-tray {
  margin-bottom: 0.5rem;
  padding: 0.3rem;
  border: 1px dashed var(--dm-warn-solid);
  border-radius: var(--dm-radius-sm);
  background: var(--dm-warn-soft);
}

.hf-tray-head {
  font-size: var(--dm-fs-micro);
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--dm-warn);
  padding-bottom: 0.2rem;
}

.hf-block {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.35rem 0.45rem;
  border: 1px solid var(--dm-line-strong);
  border-left: var(--dm-rail) solid var(--dm-accent);
  border-radius: var(--dm-radius-sm);
  background: var(--dm-surface);
  font-size: var(--dm-fs-sm);
  cursor: grab;
  user-select: none;
}

.hf-block:hover { background: var(--dm-surface-hover); }
.hf-block:focus-visible { outline: 2px solid var(--dm-accent); outline-offset: 1px; }

.hf-block-selected {
  background: var(--dm-accent-soft);
  border-color: var(--dm-accent);
}

.hf-block-tray { border-left-color: var(--dm-warn-solid); }

.hf-block-dragging { opacity: .45; cursor: grabbing; }

.hf-block-name {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.hf-block-code {
  font-family: var(--bs-font-monospace, monospace);
  font-size: var(--dm-fs-micro);
  color: var(--dm-text-muted);
}

.hf-block-count {
  font-size: var(--dm-fs-micro);
  color: var(--dm-text-faint);
}

.hf-place {
  flex: 0 0 auto;
  width: 1.15rem;
  height: 1.15rem;
  line-height: 1;
  padding: 0;
  border: 1px solid var(--dm-accent);
  border-radius: var(--dm-radius-sm);
  background: transparent;
  font-size: var(--dm-fs-micro);
  color: var(--dm-accent);
  cursor: pointer;
}

.hf-place:hover { background: var(--dm-accent); color: #fff; }

/* The drop indicator rides on the block the dragged one would land against. */
.hf-drop-before { box-shadow: 0 -2px 0 0 var(--dm-accent); }
.hf-drop-after { box-shadow: 0 2px 0 0 var(--dm-accent); }

.hf-code { font-family: var(--bs-font-monospace, monospace); font-size: var(--dm-fs-sm); }

.hf-row-changed { background: var(--dm-warn-soft); }
.hf-row-unallocated { color: var(--dm-text-muted); font-style: italic; }

/* ==========================================================================
   Hierarchy tree (experimental)
   The top-down chart at Hierarchies/Tree — the second of the two flow
   sandboxes. Blocks fan out beneath their parent and drag sideways to change
   the adjacency. Connectors are the classic pseudo-element tree: each li
   draws half the horizontal bar above it plus the vertical drop into itself,
   and a nested ul draws the stub down from its parent.
   ========================================================================== */

.ht-crumbs {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  flex-wrap: wrap;
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid var(--dm-line);
}

.ht-crumb {
  border: 0;
  background: transparent;
  padding: 0.1rem 0.3rem;
  border-radius: var(--dm-radius-sm);
  font-size: var(--dm-fs-sm);
  color: var(--dm-accent);
  cursor: pointer;
}

.ht-crumb:hover { background: var(--dm-surface-hover); }
.ht-crumb-current { color: var(--dm-text); font-weight: 500; cursor: default; }
.ht-crumb-sep { color: var(--dm-text-faint); font-size: var(--dm-fs-sm); }

.ht-canvas {
  overflow: auto;
  padding: 1rem 0.5rem 1.5rem;
  min-height: 20rem;
}

.ht-empty {
  color: var(--dm-text-faint);
  font-size: var(--dm-fs-sm);
  text-align: center;
  padding: 2rem 0;
}

.ht-children {
  position: relative;
  display: flex;
  justify-content: center;
  padding-top: 20px;
  margin: 0;
  list-style: none;
}

/* width:max-content stops the row being wider than its content, which is what
   makes justify-content:center spill past BOTH edges of the scroll container
   and leaves the left-hand blocks permanently unreachable. min-width keeps it
   centred while the tree is still narrower than the canvas. */
.ht-root {
  padding-top: 0;
  width: max-content;
  min-width: 100%;
}

/* The stub dropping from a parent block into its children's horizontal bar. */
.ht-children .ht-children::before {
  content: '';
  position: absolute;
  top: 0;
  left: 50%;
  width: 0;
  height: 20px;
  border-left: 2px solid var(--dm-line-strong);
}

.ht-node {
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding: 20px 6px 0;
  list-style: none;
}

/* Two halves of the bar above a block: ::before reaches left, ::after reaches
   right and also carries the vertical drop into the block itself. */
.ht-node::before,
.ht-node::after {
  content: '';
  position: absolute;
  top: 0;
  right: 50%;
  width: 50%;
  height: 20px;
  border-top: 2px solid var(--dm-line-strong);
}

.ht-node::after {
  right: auto;
  left: 50%;
  border-left: 2px solid var(--dm-line-strong);
}

/* An only child needs no bar — the parent's stub already reaches it. */
.ht-node:only-child::before,
.ht-node:only-child::after { display: none; }
.ht-node:only-child { padding-top: 0; }

/* The outermost siblings stop the bar at their own centre rather than running
   it off the end of the row. */
.ht-node:first-child::before,
.ht-node:last-child::after { border: 0 none; }
.ht-node:last-child::before { border-right: 2px solid var(--dm-line-strong); border-radius: 0 5px 0 0; }
.ht-node:first-child::after { border-radius: 5px 0 0 0; }

.ht-block {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  min-width: 7.5rem;
  max-width: 11rem;
  padding: 0.4rem 0.5rem;
  border: 1px solid transparent;
  border-radius: var(--dm-radius);
  font-size: var(--dm-fs-sm);
  cursor: grab;
  user-select: none;
  box-shadow: var(--dm-shadow-sm);
}

.ht-block:focus-visible { outline: 2px solid var(--dm-ink); outline-offset: 2px; }
.ht-block-dragging { opacity: .45; cursor: grabbing; }

.ht-block-name {
  flex: 1 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.ht-block-code {
  font-family: var(--bs-font-monospace, monospace);
  font-size: var(--dm-fs-micro);
  opacity: .75;
}

.ht-caret { font-size: var(--dm-fs-micro); opacity: .8; }

.ht-zoom {
  flex: 0 0 auto;
  border: 0;
  background: transparent;
  padding: 0 0.1rem;
  font-size: var(--dm-fs-micro);
  color: inherit;
  opacity: .6;
  cursor: pointer;
}

.ht-zoom:hover { opacity: 1; }

/* The level ramp from the proposal's diagram: darkest at Department, lightest
   at Base, so depth reads at a glance without reading the labels. */
.ht-depth-0 { background: #1f4e9c; color: #fff; }
.ht-depth-1 { background: #2b6cb8; color: #fff; }
.ht-depth-2 { background: #4a90cd; color: #fff; }
.ht-depth-3 { background: #85b8e0; color: var(--dm-text); }
.ht-depth-4 { background: #aed3ef; color: var(--dm-text); }
.ht-depth-5 { background: #cfe5f7; color: var(--dm-text); }

.ht-block-unallocated {
  background: var(--dm-warn-soft);
  color: var(--dm-text);
  border-color: var(--dm-warn-solid);
  border-style: dashed;
}

.ht-leafcode {
  margin-top: 0.3rem;
  font-family: var(--bs-font-monospace, monospace);
  font-size: var(--dm-fs-micro);
  color: var(--dm-text-muted);
  white-space: nowrap;
}

.ht-drop-before { box-shadow: -3px 0 0 0 var(--dm-accent); }
.ht-drop-after { box-shadow: 3px 0 0 0 var(--dm-accent); }

/* ==========================================================================
   Document bank — browse rail, list, preview. Three columns on a wide screen;
   the preview drops under the list below 1400px, and everything stacks on a phone.
   ========================================================================== */

.dm-docs {
  display: grid;
  grid-template-columns: 17rem minmax(0, 1fr) 24rem;
  gap: 0.75rem;
  align-items: start;
}

.dm-docs-rail,
.dm-docs-detail {
  position: sticky;
  top: calc(var(--dm-header-h) + 0.75rem);
  max-height: calc(100vh - var(--dm-header-h) - 1.5rem);
  display: flex;
  flex-direction: column;
}

.dm-docs-detail { overflow-y: auto; }

.dm-docs-tabs {
  display: flex;
  border-bottom: 1px solid var(--dm-line);
}

.dm-docs-tab {
  flex: 1 1 auto;
  border: 0;
  background: transparent;
  padding: 0.5rem 0.25rem;
  font-size: var(--dm-fs-sm);
  color: var(--dm-text-muted);
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
}

.dm-docs-tab:hover { color: var(--dm-text); }

.dm-docs-tab.active {
  color: var(--dm-accent);
  border-bottom-color: var(--dm-accent);
  font-weight: 600;
}

.dm-docs-rail-search {
  padding: 0.5rem 0.6rem;
  border-bottom: 1px solid var(--dm-line);
  font-size: var(--dm-fs-sm);
}

.dm-docs-tree {
  flex: 1 1 auto;
  overflow-y: auto;
  padding: 0.25rem 0;
  min-height: 8rem;
}

.dm-docs-node {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  padding: 0.28rem 0.6rem;
  font-size: var(--dm-fs-sm);
  cursor: pointer;
  border-left: var(--dm-rail) solid transparent;
}

.dm-docs-node.child { padding-left: 1.9rem; }
.dm-docs-node:hover { background: var(--dm-surface-hover); }
.dm-docs-node:focus-visible { outline: 2px solid var(--dm-accent); outline-offset: -2px; }

.dm-docs-node.selected {
  background: var(--dm-accent-soft);
  border-left-color: var(--dm-accent);
  font-weight: 600;
}

.dm-docs-node.empty .dm-docs-node-label { color: var(--dm-text-muted); }
.dm-docs-node .bi { color: var(--dm-text-muted); flex: none; }

.dm-docs-node-label {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dm-docs-twisty,
.dm-docs-twisty-pad {
  flex: none;
  width: 1rem;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--dm-text-muted);
  font-size: var(--dm-fs-micro);
}

.dm-docs-count {
  flex: none;
  min-width: 1.5rem;
  text-align: right;
  font-size: var(--dm-fs-micro);
  color: var(--dm-text-muted);
  font-variant-numeric: tabular-nums;
}

.dm-docs-loading {
  padding: 0.75rem;
  color: var(--dm-text-muted);
  font-size: var(--dm-fs-sm);
}

.dm-docs-loading.child { padding-left: 1.9rem; }

.dm-docs-bin {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  border: 0;
  border-top: 1px solid var(--dm-line);
  background: var(--dm-surface-sunken);
  padding: 0.5rem 0.75rem;
  font-size: var(--dm-fs-sm);
  color: var(--dm-text-muted);
  text-align: left;
}

.dm-docs-bin .dm-docs-count { margin-left: auto; }
.dm-docs-bin:hover { color: var(--dm-text); }
.dm-docs-bin.active { color: var(--dm-danger); font-weight: 600; background: var(--dm-danger-soft); }

/* --- List --- */

.dm-docs-search {
  position: relative;
  flex: 1 1 18rem;
  max-width: 32rem;
}

.dm-docs-search .bi {
  position: absolute;
  left: 0.6rem;
  top: 50%;
  transform: translateY(-50%);
  color: var(--dm-text-muted);
  pointer-events: none;
}

.dm-docs-search input { padding-left: 1.9rem; }

/* Category, region and store filters above the table. The card normally clips its contents for the
   table's rounded corners; the combos' lists must drop out over the rows, so this card opts out. */
.dm-docs-list { overflow: visible; }

.dm-docs-filters {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1.2fr);
  gap: 0.5rem;
  padding: 0.5rem 0.65rem;
  border-bottom: 1px solid var(--dm-line);
}

.dm-docs-filter .form-label {
  font-size: var(--dm-fs-micro);
  color: var(--dm-text-muted);
  margin-bottom: 0.1rem;
}

@media (max-width: 991.98px) {
  .dm-docs-filters { grid-template-columns: minmax(0, 1fr); }
}

/* The dialog's three link finders: a label column, then the chosen chips above the box that adds more. */
.dm-docs-link {
  display: grid;
  grid-template-columns: 9rem minmax(0, 1fr);
  gap: 0.5rem;
  align-items: start;
  padding: 0.35rem 0;
}

.dm-docs-link + .dm-docs-link { border-top: 1px dashed var(--dm-line); }

.dm-docs-link-required {
  margin-left: 0.15rem;
  font-size: var(--dm-fs-micro);
  font-weight: 400;
  color: var(--dm-danger);
}

.dm-docs-link-label {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  min-height: calc(1.5em + 0.5rem + 2px);   /* the height of the sm input beside it */
  font-size: var(--dm-fs-sm);
  font-weight: 600;
}

.dm-docs-link-body .dm-docs-chips:empty { display: none; }
.dm-docs-link-body .dm-docs-picker { margin-top: 0; }
.dm-docs-link-body .dm-docs-chips:not(:empty) + .dm-docs-picker { margin-top: 0.3rem; }

@media (max-width: 575.98px) {
  .dm-docs-link { grid-template-columns: minmax(0, 1fr); gap: 0.2rem; }
}

.dm-docs-chip .bi { color: var(--dm-text-muted); }

.dm-docs-context {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding: 0.5rem 0.65rem 0;
}

.dm-docs-ctx {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  background: var(--dm-accent-soft);
  border-radius: 1rem;
  padding: 0.15rem 0.35rem 0.15rem 0.7rem;
  font-size: var(--dm-fs-sm);
  font-weight: 600;
}

.dm-docs-ctx .btn-close { font-size: 0.55rem; }

.dm-docs-types {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  padding: 0.5rem 0.65rem;
  border-bottom: 1px solid var(--dm-line);
}

.dm-docs-types:empty { display: none; }

.dm-docs-type {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  border: 1px solid var(--dm-line-strong);
  background: var(--dm-surface);
  border-radius: 1rem;
  padding: 0.1rem 0.6rem;
  font-size: var(--dm-fs-sm);
  color: var(--dm-text);
}

.dm-docs-type:hover { background: var(--dm-surface-hover); }

.dm-docs-type.active {
  border-color: var(--dm-accent);
  background: var(--dm-accent);
  color: #fff;
}

.dm-docs-sort {
  border: 0;
  background: transparent;
  padding: 0;
  font: inherit;
  color: inherit;
  text-transform: inherit;
  letter-spacing: inherit;
}

.dm-docs-sort.active { color: var(--dm-accent); }
.dm-docs-sort[data-dir="asc"]::after { content: " \2191"; }
.dm-docs-sort[data-dir="desc"]::after { content: " \2193"; }

.dm-docs-table tbody tr { cursor: pointer; }
.dm-docs-row:focus-visible { outline: 2px solid var(--dm-accent); outline-offset: -2px; }

.dm-docs-row.selected > td {
  background: var(--dm-accent-soft) !important;
  box-shadow: none;
}

.dm-docs-row.selected > td:first-child { box-shadow: inset var(--dm-rail) 0 0 var(--dm-accent); }

.dm-docs-title {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  font-weight: 600;
}

.dm-docs-title .bi { color: var(--dm-accent); flex: none; }

.dm-docs-sub {
  font-size: var(--dm-fs-micro);
  color: var(--dm-text-muted);
  padding-left: 1.45rem;
  word-break: break-all;
}

.dm-docs-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
}

.dm-docs-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.3rem;
  max-width: 16rem;
  border: 1px solid var(--dm-line);
  background: var(--dm-surface-sunken);
  border-radius: var(--dm-radius-sm);
  padding: 0 0.4rem;
  font-size: var(--dm-fs-micro);
  line-height: 1.6;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  color: var(--dm-text);
}

button.dm-docs-chip:hover { border-color: var(--dm-accent); color: var(--dm-accent); }
.dm-docs-chip.muted { color: var(--dm-text-muted); border-style: dashed; }
.dm-docs-chip.orphaned { color: var(--dm-warn); background: var(--dm-warn-soft); border-color: var(--dm-warn-solid); }
.dm-docs-chip.current { background: var(--dm-ok-soft); border-color: var(--dm-ok); color: var(--dm-ok); margin-left: 0.4rem; }
/* In the dialog the whole name matters — "FROZEN PERISHABLES - S…" could be any of several — so no ellipsis. */
.dm-docs-chip.removable { font-size: var(--dm-fs-sm); padding-right: 0.2rem; max-width: 100%; white-space: normal; }
.dm-docs-chip.removable .btn-close { font-size: 0.5rem; }

.dm-docs-pager {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.4rem 0.65rem;
  border-top: 1px solid var(--dm-line);
}

.dm-docs-pager:empty { display: none; }

.dm-docs-busy { opacity: .55; transition: opacity .15s; }

/* --- Detail --- */

.dm-docs-detail-head {
  position: relative;
  padding: 0.75rem 2.5rem 0.25rem 0.9rem;
}

.dm-docs-detail-type {
  display: flex;
  align-items: center;
  gap: 0.35rem;
  font-size: var(--dm-fs-micro);
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--dm-text-muted);
}

.dm-docs-detail-title {
  font-size: var(--dm-fs-lg);
  font-weight: 600;
  margin: 0.15rem 0 0;
  word-break: break-word;
}

.dm-docs-detail-close {
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
}

.dm-docs-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem;
  padding: 0.5rem 0.9rem;
}

.dm-docs-preview {
  margin: 0 0.9rem;
  border: 1px solid var(--dm-line);
  border-radius: var(--dm-radius-sm);
  background: var(--dm-surface-sunken);
  overflow: hidden;
}

.dm-docs-preview iframe {
  display: block;
  width: 100%;
  height: 26rem;
  border: 0;
  background: #fff;
}

.dm-docs-preview img {
  display: block;
  max-width: 100%;
  max-height: 26rem;
  margin: 0 auto;
}

.dm-docs-preview.none {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.25rem;
  padding: 1.5rem 1rem;
  color: var(--dm-text-muted);
  font-size: var(--dm-fs-sm);
  text-align: center;
}

.dm-docs-preview.none .bi { font-size: 2rem; opacity: .5; }

.dm-docs-facts {
  display: grid;
  grid-template-columns: 6.5rem minmax(0, 1fr);
  gap: 0.35rem 0.75rem;
  margin: 0.9rem;
  font-size: var(--dm-fs-sm);
}

.dm-docs-facts dt { font-weight: normal; color: var(--dm-text-muted); }
.dm-docs-facts dd { margin: 0; word-break: break-word; }

.dm-docs-versions {
  border-top: 1px solid var(--dm-line);
  padding: 0.6rem 0.9rem 0.9rem;
}

.dm-docs-versions h3 {
  font-size: var(--dm-fs-micro);
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--dm-text-muted);
  margin: 0 0 0.4rem;
}

.dm-docs-version {
  display: flex;
  gap: 0.6rem;
  padding: 0.4rem 0;
  font-size: var(--dm-fs-sm);
  border-bottom: 1px solid var(--dm-line);
}

.dm-docs-version:last-child { border-bottom: 0; }

.dm-docs-version-no {
  flex: none;
  width: 2rem;
  font-weight: 600;
  color: var(--dm-text-muted);
}

.dm-docs-version.current .dm-docs-version-no { color: var(--dm-ok); }
.dm-docs-version-body { flex: 1 1 auto; min-width: 0; word-break: break-word; }
.dm-docs-version-note { font-style: italic; color: var(--dm-text-muted); }
.dm-docs-version-actions { flex: none; display: flex; align-items: flex-start; }

/* --- Upload dialog --- */

.dm-docs-drop {
  border: 2px dashed var(--dm-line-strong);
  border-radius: var(--dm-radius);
  padding: 1rem;
  text-align: center;
  background: var(--dm-surface-sunken);
  transition: border-color .15s, background .15s;
}

.dm-docs-drop > .bi { font-size: 1.75rem; color: var(--dm-text-muted); }
.dm-docs-drop.over { border-color: var(--dm-accent); background: var(--dm-accent-soft); }

.dm-docs-filelist {
  list-style: none;
  padding: 0;
  margin: 0.6rem 0 0;
  text-align: left;
}

.dm-docs-filelist li {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.25rem 0.4rem;
  font-size: var(--dm-fs-sm);
  border-top: 1px solid var(--dm-line);
}

.dm-docs-filelist li > span:first-of-type { flex: 1 1 auto; min-width: 0; word-break: break-all; }
.dm-docs-filelist .btn-close { font-size: 0.55rem; }

.dm-docs-picker { position: relative; margin-top: 0.35rem; }

.dm-docs-picker-results {
  position: absolute;
  z-index: 5;
  left: 0;
  right: 0;
  max-height: 14rem;
  overflow-y: auto;
  background: var(--dm-surface);
  border: 1px solid var(--dm-line-strong);
  border-top: 0;
  border-radius: 0 0 var(--dm-radius-sm) var(--dm-radius-sm);
  box-shadow: var(--dm-shadow-lg);
}

.dm-docs-picker-results:empty { display: none; }

.dm-docs-picker-item {
  display: block;
  width: 100%;
  border: 0;
  background: transparent;
  text-align: left;
  padding: 0.35rem 0.6rem;
  font-size: var(--dm-fs-sm);
}

.dm-docs-picker-item:hover,
.dm-docs-picker-item:focus { background: var(--dm-accent-soft); outline: none; }

.dm-docs-picker-empty {
  padding: 0.5rem 0.6rem;
  font-size: var(--dm-fs-sm);
  color: var(--dm-text-muted);
}

/* Narrower: the rail keeps the left column for the full height and the preview stacks under the list.
   Spanning both rows matters — a sticky rail confined to the first row would slide over the preview. */
@media (max-width: 1399.98px) {
  .dm-docs { grid-template-columns: 16rem minmax(0, 1fr); }
  .dm-docs-rail { grid-row: 1 / span 2; }
  .dm-docs-detail { grid-column: 2; position: static; max-height: none; }
}

@media (max-width: 767.98px) {
  .dm-docs { grid-template-columns: minmax(0, 1fr); }
  .dm-docs-rail { grid-row: auto; position: static; max-height: 22rem; }
  .dm-docs-detail { grid-column: auto; }
}
