/*
 * MM Portal - shared UI primitives.
 *
 * The portal's baseline vocabulary for buttons, badges, search boxes and filter
 * pills, plus the component kits lifted out of view <style> blocks.
 *
 * Loaded by all three shells immediately after tokens.css and BEFORE custom.css
 * and partners.css, so either of those can still override a primitive without
 * reaching for !important.
 *
 * Two hard rules for this file:
 *
 *   1. No colour literal. Every background, color, border-color and shadow
 *      reads a token from tokens.css. A literal here is invisible today and
 *      becomes a light-coloured hole the moment dark mode lands.
 *
 *   2. .mm-btn must NEVER share an element with .btn. See the note on .mm-btn.
 *
 * Reserved names, already in use elsewhere, do not claim them here:
 *   .mm-tabs .mm-tab-pane .mm-lock*        app/Views/customers/view.php
 *   .mm-chip .mm-open*                     app/Views/customers/index.php
 *   .mm-navctl* .mm-notif-* .mm-bell-badge app/Views/layout/navbar.php
 *   .mm-content .mm-card .mm-pad .mm-body  app/Views/email/layouts/base.twig
 */

/* ── Button ─────────────────────────────────────────────────────────── */
/*
 * The portal's baseline button, deliberately NOT the theme's .btn.
 *
 * CRITICAL: never put .mm-btn and .btn on the same element.
 * public/assets/js/material-dashboard.js:847 runs querySelectorAll('.btn') once
 * at load and attaches a handler that appends a <span class="ripple"> into the
 * clicked node. Rewriting textContent on such a button detaches that span and
 * its cleanup then throws. Avoiding that is the whole reason .pn-card-action
 * was written as a non-.btn in the first place, and this primitive generalises
 * it rather than reintroducing the bug.
 *
 * Quieter than the theme's button on purpose: no uppercase, no drop shadow, no
 * gradient. Those read as loud beside a 0.85rem card title.
 */
.mm-btn {
    display: inline-flex;
    /*
     * Without this, two adjacent buttons sit on DIFFERENT baselines whenever their
     * first child differs. An inline-flex box takes its baseline from its first
     * flex item, so `[icon] Previous` aligns on the icon while `Next [icon]`
     * aligns on the text, and the pair steps down the page by a few pixels.
     *
     * Bootstrap's .btn carries `vertical-align: middle` for exactly this reason.
     * .mm-btn replaced .btn across the portal and did not inherit the guard.
     */
    vertical-align: middle;
    align-items: center;
    justify-content: center;
    gap: 0.35rem;
    padding: 0.4rem 0.8rem;
    font-family: inherit;
    font-size: 0.8rem;
    font-weight: 600;
    line-height: 1.2;
    border: 1px solid transparent;
    border-radius: 0.5rem;
    background: none;
    color: var(--mm-text-primary);
    cursor: pointer;
    white-space: nowrap;
    text-decoration: none;
    transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

.mm-btn:disabled,
.mm-btn[aria-disabled="true"] {
    opacity: 0.5;
    cursor: not-allowed;
}

/*
 * Disabled SOLID variants get a real muted pair, not the opacity wash above.
 *
 * The wash was written for the outline and ghost variants, where fading dark ink on a
 * light ground stays legible. On a solid it is the pastel-with-white-text failure this
 * sheet warns about everywhere else, arriving by a different route: --mm-fill-accent is
 * #576ce3, and half of that over white is #abb6f1, which carries --mm-text-inverse at
 * **1.96:1**. The label is not so much low contrast as absent.
 *
 * It went unnoticed because almost nothing in the portal renders a solid button disabled
 * at REST - they disable for a moment mid-submit and nobody reads them. A bulk-action
 * button that starts disabled until something is selected sits in that state permanently,
 * which is where it was finally seen.
 *
 * Fill and ink move together across themes, per the rule that a fill DARKENS in dark mode
 * while an ink LIGHTENS: 4.19:1 light, 6.10:1 dark. Disabled controls are exempt from the
 * WCAG minimum, so neither number is a requirement - they are here because "disabled"
 * should mean unavailable, never illegible.
 *
 * Deliberately listed rather than written as `.mm-btn[class*="--"]`: only the solid
 * variants may be caught, and --secondary, --ghost, --link and --ghost-danger must keep
 * the wash.
 */
.mm-btn--primary:disabled,
.mm-btn--danger:disabled,
.mm-btn--success:disabled,
.mm-btn--warning:disabled,
.mm-btn--dark:disabled,
.mm-btn--primary[aria-disabled="true"],
.mm-btn--danger[aria-disabled="true"],
.mm-btn--success[aria-disabled="true"],
.mm-btn--warning[aria-disabled="true"],
.mm-btn--dark[aria-disabled="true"] {
    opacity: 1;
    background: var(--mm-surface-sunken);
    border-color: var(--mm-border);
    color: var(--mm-text-muted);
}

.mm-btn:focus-visible {
    outline: 2px solid var(--mm-accent);
    outline-offset: 2px;
}

/*
 * The icon takes the button's colour, whatever that is. panels.css paints every
 * icon in a card head grey via `.set-card-head .material-symbols-rounded`
 * (0-2-0), and that selector also catches an icon inside a BUTTON in the head,
 * outranking any colour set on the button itself. Restated here at 0-2-0 so a
 * .mm-btn in a card head keeps its own colour instead of inheriting chrome grey.
 */
.mm-btn .material-symbols-rounded {
    font-size: 17px;
    line-height: 1;
    color: inherit;
}

/*
 * A label wrapped in a span must not make the button taller than its neighbour.
 *
 * .mm-btn is inline-flex, so an icon written as a DIRECT child is a flex item and
 * contributes its own 15-17px box. Wrap that same icon and its text in a <span> and the
 * span becomes the only flex item: it is blockified, an inline formatting context opens
 * inside it, and the line box now has to hold both the 1.2 strut of the 12px label and
 * the half-leading around the larger inline icon. The button grows three or four pixels
 * and sits visibly taller than the plain one beside it.
 *
 * That wrapper is not optional and cannot simply be removed. It is the portal's standard
 * way of relabelling a button: material-dashboard.js attaches a ripple on load and
 * rewriting textContent detaches it, so the house pattern is two sibling spans toggled
 * with .d-none. 30 buttons across 17 views are built that way, and every one of them was
 * carrying this.
 *
 * Restating the parent's own alignment on the span collapses the extra line box. `gap`
 * is inherited explicitly - it is not an inherited property by default - so an icon and
 * its label keep the same 0.35rem they would have had as direct children.
 *
 * .d-none still wins over this: Bootstrap's utility carries !important, so the hidden
 * half of a toggle pair stays hidden.
 */
.mm-btn > span {
    display: inline-flex;
    align-items: center;
    gap: inherit;
}

/*
 * Every variant's :hover is guarded against the disabled states, and that is not a
 * flourish. `.mm-btn:disabled` sits ABOVE these rules in this file, and a variant hover
 * scores the same (0,2,0) as the disabled rule, so source order alone handed hover the
 * win: a disabled button lit up under the cursor and looked clickable. It also meant a
 * solid disabled button flashed back to its full accent fill on hover, undoing the muted
 * treatment entirely.
 *
 * `[aria-disabled="true"]` is guarded alongside `:disabled` because an anchor cannot be
 * `:disabled` - a disabled `<a class="mm-btn">` is spelled with the attribute, and the
 * base disabled rule already covers both.
 *
 * .mm-btn--ghost-danger had this from the start; the other eight did not.
 */
.mm-btn--primary {
    background: var(--mm-fill-accent);
    border-color: var(--mm-fill-accent);
    color: var(--mm-text-inverse);
}

.mm-btn--primary:hover:not(:disabled):not([aria-disabled="true"]) {
    background: var(--mm-fill-accent-hover);
    border-color: var(--mm-fill-accent-hover);
    color: var(--mm-text-inverse);
}

.mm-btn--secondary {
    background: var(--mm-surface);
    border-color: var(--mm-border-strong);
    color: var(--mm-text-primary);
}

.mm-btn--secondary:hover:not(:disabled):not([aria-disabled="true"]) {
    background: var(--mm-surface-hover);
    border-color: var(--mm-text-faint);
    color: var(--mm-text-primary);
}

.mm-btn--danger {
    background: var(--mm-fill-danger);
    border-color: var(--mm-fill-danger);
    color: var(--mm-text-inverse);
}

.mm-btn--danger:hover:not(:disabled):not([aria-disabled="true"]) {
    background: var(--mm-fill-danger-hover);
    border-color: var(--mm-fill-danger-hover);
    color: var(--mm-text-inverse);
}

/*
 * Quiet destructive action: the missing middle between .mm-btn--ghost (neutral)
 * and .mm-btn--danger (solid fill). A row-level remove had no variant to use, so
 * every one of them was a .mm-btn--link recoloured by a utility class, which
 * reads as an underlined glyph rather than a control.
 *
 * Muted at REST, danger only on hover. In a list of 70 rows a column of red
 * glyphs reads as a column of errors and competes with the status the row is
 * actually trying to report. The intent belongs on hover, which is the moment
 * before the click. Hover is ink on tint, so both halves move together in dark
 * mode rather than one inverting against the other.
 */
.mm-btn--ghost-danger {
    background: none;
    border-color: transparent;
    color: var(--mm-text-muted);
}
.mm-btn--ghost-danger:hover:not(:disabled):not([aria-disabled="true"]) {
    background: var(--mm-danger-tint);
    border-color: transparent;
    color: var(--mm-danger-ink);
}
.mm-btn--ghost-danger:focus-visible {
    outline: 0;
    border-color: var(--mm-danger-border-soft);
    box-shadow: 0 0 0 3px var(--mm-danger-tint);
}

/* Square icon-only button, so a hover ground is a tidy rounded square rather than
   a wide slab around a single glyph. Pair with .mm-btn--sm. */
.mm-btn--icon {
    width: 28px;
    height: 28px;
    padding: 0;
}

.mm-btn--ghost {
    background: none;
    border-color: transparent;
    color: var(--mm-text-muted);
}

.mm-btn--ghost:hover:not(:disabled):not([aria-disabled="true"]) {
    background: var(--mm-surface-sunken);
    color: var(--mm-text-primary);
}

/*
 * The remaining variants, added when the portal-wide migration ran and the four
 * originals turned out not to cover what was actually in use. The theme's
 * buttons were a mix of `btn-*` and `bg-gradient-*` fills, so both map here.
 */
.mm-btn--dark {
    background: var(--mm-fill-dark);
    border-color: var(--mm-fill-dark);
    color: var(--mm-text-inverse);
}

.mm-btn--dark:hover:not(:disabled):not([aria-disabled="true"]) {
    background: var(--mm-fill-dark-hover);
    border-color: var(--mm-fill-dark-hover);
    color: var(--mm-text-inverse);
}

.mm-btn--success {
    background: var(--mm-fill-success);
    border-color: var(--mm-fill-success);
    color: var(--mm-text-inverse);
}

.mm-btn--success:hover:not(:disabled):not([aria-disabled="true"]) {
    background: var(--mm-fill-success-hover);
    border-color: var(--mm-fill-success-hover);
    color: var(--mm-text-inverse);
}

.mm-btn--warning {
    background: var(--mm-fill-warning);
    border-color: var(--mm-fill-warning);
    color: var(--mm-text-inverse);
}

.mm-btn--warning:hover:not(:disabled):not([aria-disabled="true"]) {
    background: var(--mm-fill-warning-hover);
    border-color: var(--mm-fill-warning-hover);
    color: var(--mm-text-inverse);
}

/*
 * A text-only action. Replaces `.btn-link`, which the theme renders uppercase
 * with a ripple. Underlines on hover rather than at rest, so a row of them does
 * not read as a paragraph of links.
 */
.mm-btn--link {
    background: none;
    border-color: transparent;
    color: var(--mm-accent);
    padding-left: 0.2rem;
    padding-right: 0.2rem;
}

.mm-btn--link:hover:not(:disabled):not([aria-disabled="true"]) {
    color: var(--mm-accent-strong);
    text-decoration: underline;
}

/*
 * Compact size, replacing `.btn-sm`. Matches .mm-field--sm so a small button
 * sitting beside a small input lines up.
 */
.mm-btn--sm {
    padding: 0.3rem 0.6rem;
    font-size: 0.75rem;
    border-radius: 0.4rem;
}

.mm-btn--sm .material-symbols-rounded { font-size: 15px; }

/*
 * A label wrapper INSIDE a button.
 *
 * .mm-btn is `display: inline-flex` with `align-items: center`, which is what puts an
 * icon on the same optical line as its text - but only for DIRECT children. The busy-state
 * pattern (a label span and a spinner span toggled with d-none, used because rewriting
 * textContent detaches the theme's ripple) nests the icon one level deeper, where the
 * button's flex no longer reaches it. The icon then falls back to sitting on the text
 * baseline, and a material symbol's baseline is not the text's, so it rides high.
 *
 * Put this on the label span and the alignment comes back. Gap matches .mm-btn's own, so a
 * button with a wrapped label is spaced identically to one without.
 */
.mm-btn-label {
    display: inline-flex;
    align-items: center;
    gap: 0.35rem;
}

/* Full width, replacing the w-100 utility on a button. */
.mm-btn--block { width: 100%; }

/* ── Activity timeline ────────────────────────────────────────────────────── */
/*
 * Built for a partner's page on 2026-09-03 and redesigned the same day: the first
 * cut was a flat run of rows with a solid coloured disc each, which is a list
 * wearing a timeline's name. What makes it readable is the day grouping and the
 * rail.
 *
 * Moved here from partners.css when the consignment page grew the same component.
 * That sheet is loaded only by the partner module, so a second consumer had no way
 * to reach it, and a copy would have drifted.
 */

.mm-tl-day + .mm-tl-day { margin-top:1.25rem; }

/* The day heading. Small and grey on purpose: it is a divider, and a heading that competes
   with the events under it makes the page read as a list of dates. */
.mm-tl-date {
  font-size:0.65rem; font-weight:700; text-transform:uppercase; letter-spacing:0.04em;
  color:var(--mm-text-muted); padding-bottom:0.5rem;
}

/* The rail. Drawn on the container rather than per item, so it runs continuously through
   a day instead of restarting under every marker, and it is inset to pass through the
   middle of the 26px markers. */
.mm-tl-items { position:relative; }
.mm-tl-items::before {
  content:""; position:absolute; left:12px; top:0.35rem; bottom:0.35rem;
  width:1px; background:var(--mm-border);
}

.mm-tl-item { position:relative; display:flex; gap:0.7rem; padding:0.4rem 0; }

/* The marker sits ON the rail, so it needs an opaque background of its own to break the
   line rather than letting it run behind the glyph. */
.mm-tl-mark {
  position:relative; z-index:1; flex-shrink:0;
  width:25px; height:25px; border-radius:50%;
  display:flex; align-items:center; justify-content:center;
  box-shadow:0 0 0 3px var(--mm-surface);
}
.mm-tl-mark .material-symbols-rounded { font-size:14px; }

/* Tinted, not solid. Nine solid discs down one column is the loudest thing on the page and
   says nothing, because every row has one. These are the .pn-badge pastels, so a timeline
   marker and the badge for the same idea are the same colour. */
.mm-tl-mark--neutral { background:var(--mm-surface-sunken); color:var(--mm-text-muted); }
.mm-tl-mark--blue    { background:var(--mm-info-tint); color:var(--mm-info-ink); }
.mm-tl-mark--green   { background:var(--mm-success-tint); color:var(--mm-success-ink); }
.mm-tl-mark--amber   { background:var(--mm-warning-tint); color:var(--mm-badge-amber-ink); }
.mm-tl-mark--red     { background:var(--mm-danger-tint); color:var(--mm-danger-ink); }

.mm-tl-body { min-width:0; flex:1; padding-top:0.15rem; }

/* Title and time on one line, the time pushed right and never allowed to wrap. A time that
   breaks onto its own line makes every row a different height. */
.mm-tl-head { display:flex; align-items:baseline; gap:0.5rem; }
.mm-tl-title { font-size:0.8rem; font-weight:600; color:var(--mm-text-primary); text-decoration:none; }
a.mm-tl-title:hover { color:var(--mm-accent); text-decoration:underline; }
.mm-tl-time { margin-left:auto; flex-shrink:0; font-size:0.68rem; color:var(--mm-text-faint); }

.mm-tl-detail { font-size:0.75rem; color:var(--mm-text-muted); line-height:1.45; margin-top:0.1rem; }
.mm-tl-actor {
  display:inline-block; margin-top:0.2rem;
  font-size:0.65rem; color:var(--mm-text-muted); background:var(--mm-hairline);
  border-radius:0.3rem; padding:0.05rem 0.35rem;
}

/* ── Consignment summary strip ───────────────────────────────────────────── */
/*
 * The consignor header, shared by all three consignment states. Eight fields
 * collapse to one line with the detail behind a disclosure, so the page opens on
 * the thing you came to do rather than on 350px of reference.
 */
.cc-strip {
    display: flex;
    align-items: center;
    gap: 0.85rem;
    padding: 0.75rem 1rem;
}
.cc-strip-main { min-width: 0; flex: 1; }
.cc-strip-name {
    font-size: 0.9rem;
    font-weight: 700;
    color: var(--mm-text-primary);
    line-height: 1.25;
}
.cc-strip-meta {
    font-size: 0.75rem;
    color: var(--mm-text-muted);
    line-height: 1.35;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
/* A figure carried on the strip itself, so the totals that matter are readable
   without opening the disclosure. */
.cc-strip-figure {
    font-size: 0.95rem;
    font-weight: 700;
    color: var(--mm-text-primary);
    line-height: 1.2;
    white-space: nowrap;
}
.cc-strip-figures { display: flex; align-items: center; gap: 1.25rem; }

/* ── Consignment item vocabulary ─────────────────────────────────────────── */
/*
 * One definition of what a consignment item looks like, shared by the scanner
 * rail on /consignments/create, the same rail in draft mode, and the items table
 * on a completed consignment. They had drifted into three different treatments of
 * the same thing.
 */
.cc-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
.cc-dot--ok { background: var(--mm-success-solid); }
.cc-dot--warn { background: var(--mm-warning-solid); }

/*
 * A fixed PORTRAIT frame with the image CONTAINED inside it at its own aspect
 * rather than cropped to fill. A slab or a raw single fills the frame; a sealed
 * box letterboxes and stays whole. Uniform row height either way.
 *
 * The items table used object-fit: cover with a per-type inline override to undo
 * it for sealed products, which is the shape of bug that hides in a PHP
 * expression where no style sweep looks.
 */
.cc-thumb {
    width: 38px;
    height: 52px;
    border-radius: 5px;
    flex-shrink: 0;
    background: var(--mm-surface-hover);
    border: 1px solid var(--mm-border-soft);
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
}
.cc-thumb img { max-width: 100%; max-height: 100%; width: auto; height: auto; display: block; }
.cc-thumb--empty { color: var(--mm-glyph-empty); }
.cc-thumb--empty .material-symbols-rounded { font-size: 18px; }
/* Row height on a record table affords a larger frame. 60x84 is a slab's 2.5:3.5. */
.cc-thumb--lg { width: 60px; height: 84px; border-radius: 6px; }
.cc-thumb--lg .material-symbols-rounded { font-size: 22px; }

.cc-title { font-size: 0.8rem; font-weight: 600; color: var(--mm-text-primary); line-height: 1.3; }
.cc-title.is-missing { color: var(--mm-warning-ink); font-style: italic; font-weight: 500; }
.cc-sub { font-size: 0.7rem; color: var(--mm-text-muted); line-height: 1.3; }
/* The "nothing here" panel a list shows in place of its rows. */
.cc-empty {
    padding: 2.5rem 1rem;
    text-align: center;
    color: var(--mm-text-muted);
}

.cc-missing-tag {
    display: inline-block;
    font-size: 0.63rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.3px;
    color: var(--mm-warning-ink);
    background: var(--mm-warning-tint);
    border-radius: 4px;
    padding: 0.05rem 0.3rem;
    white-space: nowrap;
}

/* ── Card head action ───────────────────────────────────────────────── */
/*
 * A quiet action sitting in a card head. Smaller and softer than .mm-btn--ghost
 * because it is chrome rather than a call to action, and it sits beside a
 * 0.85rem title.
 *
 * Shared with `.pn-card-action`, the incumbent name across 30 uses in the
 * partner module. One definition rather than two copies, so partner markup
 * needed no edit and the two can no longer drift.
 *
 * The values are .pn-card-action's, not the ones .mm-action shipped with: the
 * partner version is the proven one and is what this whole baseline was drawn
 * from, so it won the two places they disagreed (hover ground --mm-rule rather
 * than --mm-surface-hover, icon 15px rather than 16px). partners.css keeps only
 * the --inline / --confirm / --danger variants and the disabled state.
 */
.mm-action,
.pn-card-action {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    background: none;
    border: 0;
    border-radius: 0.375rem;
    padding: 0.2rem 0.4rem;
    font-size: 0.72rem;
    font-weight: 600;
    color: var(--mm-text-muted);
    cursor: pointer;
    white-space: nowrap;
    transition: color 0.15s ease, background 0.15s ease;
}

.mm-action:hover,
.pn-card-action:hover {
    color: var(--mm-accent);
    background: var(--mm-rule);
}

/*
 * Mirrored from partners.css, where only .pn-card-action carried it. Without
 * it a .mm-action held disabled through a slow job still lights up on hover
 * and reads as clickable for the whole run.
 */
.mm-action:disabled { opacity: 0.5; cursor: default; }
.mm-action:disabled:hover { color: var(--mm-text-muted); background: none; }

/*
 * The variants, moved here from partners.css on 2026-09-22 for the same reason the
 * base rule was: /admin/users needed --inline and --confirm, and a second copy under
 * an `mm-` name would be the two drifting apart. partners.css keeps only its
 * `.pn-portal`-scoped hover overrides, which are more specific and still win.
 *
 * --inline cancels the base rule's `margin-left:auto`. That auto is what pushes a
 * lone action to the right of a card head, but EVERY action carries it, so two
 * visible at once split the free space between them and drift apart instead of
 * sitting together. The FIRST visible action keeps the auto and the rest take
 * --inline. In an Edit / Cancel / Save head that means Save carries it and Cancel
 * does not, because Edit is hidden precisely when Cancel is shown.
 *
 * Green commits, red DESTROYS. Two narrow meanings, and the narrowness is the point.
 *
 * --confirm goes on the action that actually writes. --danger goes on the action that
 * destroys something: Remove, Delete, Void, and cancelling a payout request (which
 * releases its claimed ledger rows). It does NOT go on a form's Cancel button. That was
 * tried and reversed the same day: a dismissal and a deletion in the same red means the
 * colour stops saying "careful" and starts saying "this is the second button", and the
 * only thing left separating them is the confirm dialog.
 *
 * Everything else stays grey - openers (Add sign-in, Adjust balance), navigation
 * (Prev/Next, View, Back) and neutral verbs (Export, Copy, Suspend). An opener in green
 * would put the colour on half the page and spend the signal before the Confirm inside
 * it gets a chance to use it.
 *
 * Greens and reds are the ones .pn-badge--green / --red already use, so a saved state
 * and the button that saved it are the same colour.
 */
.mm-action--inline,
.pn-card-action--inline { margin-left: 0; }

.mm-action--confirm,
.pn-card-action--confirm { color: var(--mm-success-ink); }
.mm-action--confirm:hover,
.pn-card-action--confirm:hover { color: var(--mm-action-confirm-hover); background: var(--mm-success-tint); }

.mm-action--danger,
.pn-card-action--danger { color: var(--mm-action-danger); }
.mm-action--danger:hover,
.pn-card-action--danger:hover { color: var(--mm-action-danger-hover); background: var(--mm-callout-imp-tint); }

/*
 * Same panels.css problem as .mm-btn above. panels.css paints every icon in a
 * card head grey at 0-2-0, which also catches an icon inside a BUTTON in the
 * head and outranks any colour set on the button itself: a green Save arrived
 * with a grey save icon, and the plain grey action went blue on hover while its
 * icon stayed put. The second selector restates at 0-3-0 so the icon simply
 * inherits. The font-size is repeated rather than left to load order, because
 * panels.css sets 19px at the same specificity as the short selector and only
 * loses that tie by loading first.
 */
.mm-action .material-symbols-rounded,
.pn-card-action .material-symbols-rounded,
.set-card-head .mm-action .material-symbols-rounded,
.set-card-head .pn-card-action .material-symbols-rounded {
    font-size: 15px;
    line-height: 1;
    color: inherit;
}

/* ── Badge ──────────────────────────────────────────────────────────── */
/*
 * Geometry lifted verbatim from .pn-badge, the badge system that already proved
 * itself: 8 views, zero inline copies.
 *
 * Deliberately NOT `.badge`, which is Bootstrap's and carries different padding,
 * radius and weight. The portal's four existing badge families (.pn-badge,
 * .sup-*, .cond-*, .status-badge/.st-*) keep their own class names and are
 * retokenised in place, so no markup churns and they can no longer drift apart
 * on colour. .mm-badge is the canonical shape for new code.
 */
.mm-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
    padding: 0.2rem 0.55rem;
    border-radius: 999px;
    font-size: 0.65rem;
    font-weight: 700;
    line-height: 1.4;
    text-transform: uppercase;
    letter-spacing: 0.02em;
    white-space: nowrap;
    background: var(--mm-neutral-tint);
    color: var(--mm-neutral-ink);
}

.mm-badge--neutral { background: var(--mm-neutral-tint); color: var(--mm-neutral-ink); }
.mm-badge--success { background: var(--mm-success-tint); color: var(--mm-success-ink); }
.mm-badge--info    { background: var(--mm-info-tint);    color: var(--mm-info-ink); }
.mm-badge--warning { background: var(--mm-warning-tint); color: var(--mm-warning-ink); }
.mm-badge--danger  { background: var(--mm-danger-tint);  color: var(--mm-danger-ink); }
.mm-badge--special { background: var(--mm-special-tint); color: var(--mm-special-ink); }

/*
 * A badge whose ground is set per item rather than by a status: a game's brand
 * colour on a picking card, a budget state on the manager board. The fill stays
 * inline because it is data driven; this only states the ink, which the default
 * badge sets to a muted grey that is unreadable on a saturated ground.
 *
 * Needed because .badge used to inherit white text from the theme; .mm-badge
 * does not, so the 12 call sites with an inline background lost their ink when
 * they were migrated.
 */
.mm-badge--solid { color: var(--mm-text-inverse); }
/* Replaces the theme's bg-gradient-dark badge. Reuses the partner module's dark
   badge pair, which is the same treatment under a different name. */
.mm-badge--dark    { background: var(--mm-border);       color: var(--mm-badge-dark-ink); }

/*
 * Aliases for the two Bootstrap names the portal still generates dynamically.
 * A number of views build a badge as `bg-gradient-${expr}` where expr is a
 * severity computed in JS or PHP. Rather than rewrite every one of those value
 * maps, the markup becomes `mm-badge--${expr}` and these two make the leftover
 * Bootstrap vocabulary resolve. `secondary` is the portal's neutral and
 * `primary` is the portal ACCENT, deliberately NOT an alias of info.
 *
 * Eight status maps use `primary` and `info` for two DIFFERENT states each:
 * a PSA order is `assigned` then `submitted`, a doc is a `process` then an
 * `sop`. Aliasing the two rendered those states identically, which is worse
 * than the theme was, because it silently removed a distinction the screen
 * was relying on. The accent tint sits dE 19 from the info tint on ground
 * and dE 77 on ink, and measures 6.51:1.
 *
 * `secondary` and `light` do still alias onto neutral: those three were the
 * same grey in the theme too, so nothing is lost.
 */
.mm-badge--primary   { background: var(--mm-accent-tint);  color: var(--mm-accent-strong); }
.mm-badge--secondary { background: var(--mm-neutral-tint); color: var(--mm-neutral-ink); }
.mm-badge--light     { background: var(--mm-neutral-tint); color: var(--mm-neutral-ink); }

/* ── Substitutions badge ────────────────────────────────────────────── */
/*
 * The customer's checkout answer to "allow TCG singles substitutions", shown
 * wherever staff might swap a card they cannot find: the follow-up ticket modal,
 * the picking screen, and order lookup. Lifted out of ticket-modal.css once the
 * second surface needed it, so the four renderers cannot drift apart.
 *
 * Wider and taller than .mm-badge, and sentence case rather than uppercase: it
 * is an instruction to read, not a status to scan past. Which is also why it
 * carries an icon and a border where .mm-badge carries neither.
 *
 * Only ever rendered from ShopifyService::substitutionsBadge - see that method
 * for why a missing answer paints the red state and not nothing at all.
 */
.mm-subs-badge {
    display: inline-flex;
    align-items: center;
    gap: 0.15rem;
    font-size: 0.65rem;
    font-weight: 700;
    padding: 0.2rem 0.55rem 0.2rem 0.4rem;
    border-radius: 20px;
    white-space: nowrap;
    letter-spacing: 0.02em;
    border: 1px solid transparent;
}
.mm-subs-badge i { font-size: 14px; }

.mm-subs-badge--yes { background: var(--mm-success-tint); color: var(--mm-success-ink); border-color: var(--mm-success-border); }
/* Declined is the one nobody can afford to miss: substituting a card for a customer who said no is
   the expensive direction, so it takes the same red the customer-replied state uses. */
.mm-subs-badge--no  { background: var(--mm-danger-tint);  color: var(--mm-danger-ink);  border-color: var(--mm-danger-border); }
/* There is deliberately no third tone, and no "unknown" state. On an online-store order anything
   that is not an explicit yes - no answer, or a value the server could not map - paints RED, because
   the rule is "refund rather than swap" and a blank space is not an instruction. Only an order the
   checkout never asked (eBay, POS, draft) shows no badge, which the floor reads as "handle this one
   the normal way". */

/* ── Search box ─────────────────────────────────────────────────────── */
/*
 * A filled search well with a leading icon, which turns white on focus.
 *
 * Lifted from 12 views that each carried a byte identical copy, then restyled
 * to the muted language (Dom, 2026-09-04). `.search-wrap` / `.search-icon` /
 * `.search-field` are the incumbent names and keep working, so all 14 pages
 * pick this up with no markup edit. `.psa-search-*` was a second copy of the
 * same thing under a different prefix.
 *
 * The rest state was 1.5px solid #d2d6da on white. A heavy border on a control
 * that is present on every list page reads as chrome competing with the data,
 * so the box is now a soft well with a 1px edge and earns its border only when
 * focused. Same move as .mm-pill below.
 */
.mm-search,
.search-wrap,
.psa-search-wrap {
    display: flex;
    align-items: center;
    border: 1px solid var(--mm-border);
    border-radius: 0.5rem;
    background: var(--mm-surface-hover);
    transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
}

/* Lifts to white on focus, so the active field is the brightest thing in the
   row. The ring does the work the resting border used to. */
.mm-search:focus-within,
.search-wrap:focus-within,
.psa-search-wrap:focus-within {
    background: var(--mm-surface);
    border-color: var(--mm-accent);
    box-shadow: 0 0 0 3px var(--mm-accent-ring);
}

.mm-search-icon,
.search-icon,
.psa-search-icon {
    flex-shrink: 0;
    color: var(--mm-text-muted);
    font-size: 20px;
    line-height: 1;
    padding: 0 6px 0 12px;
    pointer-events: none;
}

/*
 * The six !important flags come from the source views and are load bearing: the
 * theme styles .form-control with its own border, background image and shadow,
 * and this input has to render as bare text inside the wrapper that draws the
 * box. Do not add a seventh without saying why.
 */
.mm-search-input,
.search-field {
    flex: 1;
    height: 38px;
    font-size: 0.875rem;
    border: none !important;
    background: transparent !important;
    background-image: none !important;
    box-shadow: none !important;
    border-radius: 0 8px 8px 0 !important;
    padding-left: 4px !important;
}

.mm-search-input:focus,
.search-field:focus { box-shadow: none !important; }

/* ── Filter pill ────────────────────────────────────────────────────── */
/*
 * A toggleable filter chip: a soft filled chip, tinted with the accent once
 * applied. Same language as .pn-seg-btn in partners.css.
 *
 * Lifted from 8 views, then restyled (Dom, 2026-09-04). It was a 1.5px DASHED
 * outline on white, flipping to solid when applied.
 *
 * What the dashed border was doing, and how it is replaced: the dash carried
 * "not applied yet" without relying on colour, which a plain fill loses. The
 * active state therefore does more work than it used to - it changes ground,
 * text colour AND gains a visible 1px border, where before it only changed
 * border-style. Do not drop that border thinking it is redundant with the
 * tint; it is the non-colour half of the signal.
 *
 * The resting label is --mm-text-body, NOT --mm-text-muted. Muted measures
 * 4.19:1 on this ground, which fails WCAG AA for 0.775rem text. Body measures
 * 7.29:1. All three states pass: rest 7.29, hover 8.35, active 6.51.
 */
.mm-pill,
.filter-pill {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    font-size: 0.775rem;
    font-weight: 600;
    padding: 0.3rem 0.7rem;
    border-radius: 999px;
    border: 1px solid transparent;
    background: var(--mm-surface-sunken);
    color: var(--mm-text-body);
    cursor: pointer;
    white-space: nowrap;
    transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
}

.mm-pill:hover,
.filter-pill:hover {
    background: var(--mm-neutral-tint);
    border-color: transparent;
    color: var(--mm-text-primary);
}

.mm-pill.is-active,
.filter-pill.pill-active {
    background: var(--mm-accent-tint);
    border-color: var(--mm-accent-soft);
    color: var(--mm-accent-strong);
}

.mm-pill.is-active::after,
.filter-pill.pill-active::after {
    display: none;
}

/*
 * The clear affordance inside an applied pill. It inherits the pill's colour
 * and is dimmed by opacity.
 *
 * Raised from 0.45 to 0.75 when the pill gained its fill. Against the old white
 * ground 0.45 was fine; against --mm-accent-tint it computed to 2.08:1, which
 * is under the 3:1 a control needs to be discernible. 0.75 gives 3.77:1.
 */
.pill-clear {
    font-size: 1rem;
    line-height: 1;
    opacity: 0.75;
    margin-left: 1px;
}

.pill-clear:hover {
    opacity: 1;
}

.pill-clear-all {
    font-size: 0.775rem;
    font-weight: 500;
    color: var(--mm-text-primary);
    background: none;
    border: none;
    padding: 4px 6px;
    cursor: pointer;
}

.pill-clear-all:hover {
    text-decoration: underline;
}

/* ── Announcement / message card kit ────────────────────────────────── */
/*
 * The card, form controls and picker widgets shared by the staff announcement
 * composer and the partner message composer.
 *
 * Lifted from four views that carried byte identical copies:
 *   app/Views/admin/announcements/form.php   app/Views/partners/messages/form.php
 *   app/Views/admin/announcements/index.php  app/Views/partners/messages/index.php
 *
 * NOT lifted, and deliberately still inline in those views: .an-head,
 * .an-head-title, .an-body and .an-hint. Those four genuinely differ, because
 * announcements/index.php reuses the names for a LIST card while the two
 * form.php views use them for a FORM card. Same names, different roles, so
 * merging them would be a redesign rather than a lift.
 */
.an-card {
    background: var(--mm-surface);
    border: 1px solid var(--mm-border-subtle);
    border-radius: 0.75rem;
    overflow: hidden;
    margin-bottom: 1rem;
}

.an-head .material-symbols-rounded { font-size: 19px; color: var(--mm-text-muted); }
.an-head-sub { font-size: 0.73rem; color: var(--mm-text-muted); }
.an-main     { flex: 1; min-width: 0; }
.an-accent   { width: 3px; align-self: stretch; border-radius: 2px; background: transparent; flex-shrink: 0; }
.an-row:last-child { border-bottom: 0; }

.an-label {
    display: block;
    font-size: 0.65rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--mm-text-muted);
    margin-bottom: 0.3rem;
}

.an-empty {
    text-align: center;
    padding: 2.25rem 1rem;
    color: var(--mm-text-muted);
}

.an-empty .material-symbols-rounded { font-size: 34px; color: var(--mm-glyph-empty); }

.an-input {
    width: 100%;
    padding: 0.5rem 0.7rem;
    border: 1px solid var(--mm-border-control);
    border-radius: 0.5rem;
    font-size: 0.85rem;
    color: var(--mm-text-primary);
    background: var(--mm-surface);
    transition: border-color 0.15s ease, box-shadow 0.15s ease;
}

.an-input:focus {
    outline: none;
    border-color: var(--mm-accent);
    box-shadow: 0 0 0 3px var(--mm-accent-ring);
}

/* Audience picker: a row of chips, each wrapping its own checkbox. */
.an-pick        { border-top: 1px solid var(--mm-rule); padding-top: 0.9rem; }
.an-pick-list   { display: flex; flex-wrap: wrap; gap: 0.4rem; }
.an-pick-scroll { max-height: 210px; overflow-y: auto; padding: 0.15rem; }

.an-pick-item {
    display: inline-flex;
    align-items: center;
    gap: 0.4rem;
    border: 1.5px solid var(--mm-border-subtle);
    border-radius: 0.5rem;
    padding: 0.35rem 0.6rem;
    margin: 0;
    font-size: 0.78rem;
    color: var(--mm-text-primary);
    cursor: pointer;
    transition: border-color 0.15s ease, background 0.15s ease;
}

.an-pick-item input { margin: 0; cursor: pointer; accent-color: var(--mm-accent); }
.an-pick-item:hover { border-color: var(--mm-border-soft); background: var(--mm-surface-subtle); }
.an-pick-item:has(input:checked) { border-color: var(--mm-accent); background: var(--mm-accent-wash); }

/* Type picker: a grid of cards, each hiding a radio behind a label box. */
.an-types {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
    gap: 0.6rem;
}

.an-type { position: relative; }

.an-type input {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    opacity: 0;
    margin: 0;
    cursor: pointer;
}

.an-type-box {
    display: block;
    height: 100%;
    border: 1.5px solid var(--mm-border-subtle);
    border-radius: 0.6rem;
    padding: 0.7rem 0.8rem;
    transition: border-color 0.15s ease, background 0.15s ease;
}

.an-type-hint { font-size: 0.68rem; color: var(--mm-text-muted); line-height: 1.4; }
.an-aud-icon  { font-size: 20px; color: var(--mm-text-muted); }

.an-type:hover .an-type-box { border-color: var(--mm-border-soft); background: var(--mm-surface-subtle); }
.an-type input:checked + .an-type-box { border-color: var(--mm-accent); background: var(--mm-accent-wash); }
.an-type input:checked + .an-type-box .an-aud-icon { color: var(--mm-accent); }
.an-type input:focus-visible + .an-type-box {
    border-color: var(--mm-accent);
    box-shadow: 0 0 0 3px rgba(94, 114, 228, 0.18);
}

/* ── Support ticket badges ──────────────────────────────────────────── */
/*
 * One badge shape carrying three axes: status (open, in progress, closed),
 * type (bug, feature request, other) and priority (low, medium, high).
 *
 * Lifted from app/Views/support/index.php and app/Views/support/view.php.
 * Same shape as .mm-badge above; kept under its own names because the markup
 * in both views names the axis value directly, which reads better at the call
 * site than a generic severity would.
 */
.sup-badge {
    display: inline-block;
    font-size: 0.65rem;
    font-weight: 700;
    padding: 0.2rem 0.55rem;
    border-radius: 20px;
    white-space: nowrap;
    text-transform: uppercase;
    letter-spacing: 0.02em;
}

.sup-open        { background: var(--mm-neutral-tint); color: var(--mm-neutral-ink); }
.sup-in_progress { background: var(--mm-info-tint);    color: var(--mm-info-ink); }
.sup-closed      { background: var(--mm-success-tint); color: var(--mm-success-ink); }

.sup-bug             { background: var(--mm-danger-tint);  color: var(--mm-danger-ink); }
.sup-feature_request { background: var(--mm-special-tint); color: var(--mm-type-feature-ink); }
.sup-other           { background: var(--mm-neutral-tint); color: var(--mm-type-other-ink); }

.sup-low    { background: var(--mm-success-tint); color: var(--mm-success-ink); }
.sup-medium { background: var(--mm-caution-tint); color: var(--mm-caution-ink); }
.sup-high   { background: var(--mm-danger-tint);  color: var(--mm-danger-ink); }

/* ── Settings row ───────────────────────────────────────────────────── */
/*
 * A labelled setting with its control on the right, its description under the
 * label, and a dirty state while unsaved.
 *
 * Lifted from app/Views/marketplace/settings.php and
 * app/Views/wishlist/settings.php. Note this is NOT the .set-* panel system in
 * panels.css: that is the tabbed card frame, this is one row inside it.
 */
.setting-row {
    display: flex;
    gap: 1.5rem;
    align-items: flex-start;
    padding: 1rem 0;
    border-bottom: 1px solid var(--mm-surface-sunken);
}

.setting-row:last-child { border-bottom: 0; }
.setting-row.dirty      { background: var(--mm-surface-hover); }

.setting-main    { flex: 1; min-width: 0; }
.setting-control { width: 260px; flex-shrink: 0; }
.setting-label   { font-size: 0.85rem; font-weight: 700; color: var(--mm-text-primary); }

.setting-desc {
    font-size: 0.78rem;
    line-height: 1.5;
    color: var(--mm-text-muted);
    margin-top: 0.35rem;
}

.setting-key {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size: 0.68rem;
    color: var(--mm-text-meta);
}

.setting-meta { font-size: 0.66rem; color: var(--mm-text-meta); margin-top: 0.4rem; }

.setting-input {
    width: 100%;
    height: 38px;
    border: 1.5px solid var(--mm-border-strong);
    border-radius: 8px;
    padding: 0 0.7rem;
    font-size: 0.85rem;
    background: var(--mm-surface);
}

.setting-input:focus {
    border-color: var(--mm-accent);
    box-shadow: 0 0 0 3px var(--mm-accent-ring);
    outline: none;
}

/* ── Dashboard stat card ────────────────────────────────────────────── */
/*
 * A headline figure with a caption, on a card whose left edge carries the
 * severity colour. Lifted from app/Views/dashboard.php and
 * app/Views/marketplace/dashboard.php.
 *
 * The border-left colour is set per instance in the view, since it is the
 * severity signal and varies per tile. Only the resting grey lives here.
 */
.dash-stat-card {
    border: none;
    border-left: 3px solid var(--mm-border);
    border-radius: 8px;
    transition: box-shadow 0.15s ease;
}

.dash-stat-card:hover { box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08) !important; }

.dash-stat-value {
    font-size: 1.9rem;
    font-weight: 700;
    color: var(--mm-text-primary);
    line-height: 1.1;
}

.dash-stat-sub { font-size: 0.75rem; color: var(--mm-text-muted); }

/* ── Sticky card head over a long table ─────────────────────────────── */
/*
 * Pins a card's header - its title and any bulk action bar - below the app
 * navbar while a long table scrolls underneath. Built for the consignment items
 * table, where a large consignment pushes the action buttons off screen, but it
 * is generic: put it on the .card-header of any card whose body holds a long
 * table, and add the sentinel described below directly before it.
 *
 * Two constraints worth knowing before reusing this:
 *
 *   1. Sticky needs an unbroken chain. No ancestor between this element and the
 *      scroll root may set `overflow` to anything but visible. `.card` sets none,
 *      which is what makes this work at all.
 *
 *   2. The table's own <thead> canNOT be made sticky the same way, because
 *      Bootstrap's `.table-responsive` sets `overflow-x: auto`, and a non-visible
 *      value on one axis computes the other to `auto` too. That makes the wrapper
 *      a scroll container, so a sticky thead inside it sticks to the wrapper - a
 *      box exactly as tall as its content - and never moves. Pinning the column
 *      headers as well needs the wrapper given a bounded height, which trades the
 *      page scrollbar for an inner one. That is a UX decision, not a CSS one.
 *
 * Do not raise the z-index past the navbar's 1020, or the head slides over it.
 */
.mm-sticky-head {
    position: sticky;
    top: var(--mm-sticky-head-top, 64px);
    z-index: 3;
    /*
     * !important because the dark theme block further down this file sets
     * `background-color: transparent` on every .card-header - reasonable for a
     * static header sitting on an already-dark card, fatal for a pinned one, which
     * needs to hide the rows passing under it. That selector scores (0,3,0) against
     * this rule's (0,1,0), so specificity alone cannot win.
     *
     * This is the same trap as the one on the header ROW below, and it bites in only
     * one theme, so it survives any amount of testing in the other.
     */
    background: var(--mm-surface) !important;
    border-bottom: 1px solid var(--mm-border);
    transition: box-shadow 0.15s ease;
}

/*
 * Lift the head off the rows travelling under it. Only once stuck: at rest it is
 * just a card head and a shadow there would read as a floating panel.
 *
 * The z-index of 3 above is what makes this land - table rows are unpositioned, so
 * the shadow paints over them rather than under.
 */
.mm-sticky-head.is-stuck {
    box-shadow: var(--mm-sticky-head-shadow);
}

/*
 * The strip between the top of the viewport and the pinned head. The navbar is
 * translucent once the theme adds .blur on scroll, so without this the table
 * rows travelling past show through it.
 *
 * Painted only while stuck. At rest the head sits partway down the page and this
 * band would cover whatever card happens to be above it - the gap between cards
 * is 24px, well inside the 64px offset - so it has to be gated rather than left
 * on. `.is-stuck` is toggled by an IntersectionObserver watching the sentinel;
 * a scroll handler would do the same job but runs on every frame of every scroll.
 *
 * The navbar's z-index of 1020 keeps it painted over this band, so the band hides
 * the rows without hiding the navbar.
 *
 * It bleeds PAST the card on both sides on purpose. An absolutely positioned
 * pseudo-element resolves left/right against its containing block's padding box,
 * which is the head - so `left: 0; right: 0` stops exactly at the card's edge and
 * leaves whatever the card paints outside that box still visible as two thin
 * vertical lines: the card's own box-shadow, and in dark mode the card surface
 * itself, which sits LIGHTER than the page behind it. Bleeding a gutter's width
 * each way covers both. The overspill lands on the column gutter, which is the
 * page ground the band is painted in, so it is invisible - but that is only true
 * for a full-width card. Reduce the bleed if you ever put this on a card sitting
 * beside another one.
 */
.mm-sticky-head.is-stuck::before {
    content: "";
    position: absolute;
    left: calc(var(--mm-sticky-head-bleed, 1rem) * -1);
    right: calc(var(--mm-sticky-head-bleed, 1rem) * -1);
    bottom: 100%;
    height: var(--mm-sticky-head-top, 64px);
    background: var(--mm-app-bg);
    pointer-events: none;
}

/*
 * A table whose column headers pin under the card head as the PAGE scrolls.
 * No inner scroll region, one scrollbar, the page behaves normally.
 *
 * The obstacle is that Bootstrap's `.table-responsive` sets `overflow-x: auto`,
 * and a non-visible value on one axis computes the other to `auto` too. That makes
 * the wrapper a scroll container, and anything sticky inside it then sticks to the
 * WRAPPER rather than the viewport - a box exactly as tall as its content, so it
 * never moves.
 *
 * The way out is to stop needing the wrapper. The theme sets `white-space: nowrap`
 * on every `.table` cell, which is the only reason a wide table overflows at all:
 * with wrapping allowed, auto table layout shrinks the columns to fit the card and
 * there is nothing to scroll horizontally. So above the breakpoint the overflow is
 * released and the header sticks to the viewport.
 *
 * Below it the columns would be too narrow to read whether they wrap or not, so the
 * wrapper keeps its horizontal scroll and the headers simply do not pin. Sticky
 * column headers matter least on the screen where you can see fewest columns.
 */
/*
 * A pinned header row has to be fully opaque or the rows travelling under it show
 * through. Two separate things would otherwise stop that happening:
 *
 *   - the cell needs a background at all, since the theme leaves table cells
 *     transparent, and
 *   - `.opacity-7`, which the item table puts on all nine header cells, fades the
 *     whole ELEMENT - background included - not just the label text. It carries
 *     !important, so the override has to as well.
 *
 * The muted look is not lost with the opacity: the header cells also carry
 * `.text-secondary`, which ui.css already restates under the dark scope, so the
 * quieter label survives in both themes through the colour where it belonged.
 * Nothing is set here for it - `.text-secondary` is !important and would win anyway.
 *
 * Both declarations need !important, for two different reasons:
 *
 *   opacity - `.opacity-7` is a utility and carries !important itself.
 *
 *   background - the dark-theme reset further down this file sets
 *     `background-color: transparent` on every table cell via
 *     `:root[data-mm-theme="dark"] .table > :not(caption) > * > *`. That selector
 *     scores (0,3,1) against this rule's (0,1,2), so it wins on specificity and no
 *     amount of extra nesting here would beat it. Without the flag the pinned header
 *     is solid in light mode and fully transparent in dark - the rows show straight
 *     through, and only in one theme, which is exactly the kind of split that gets
 *     shipped unnoticed.
 */
.mm-sticky-cols thead th,
.mm-scroll-table thead th {
    background: var(--mm-surface) !important;
    opacity: 1 !important;
}

@media (min-width: 992px) {
    /* Release the scroll container so sticky resolves against the viewport. */
    .mm-sticky-cols {
        overflow: visible;
    }

    /*
     * Let the text-heavy columns wrap. Without this the table stays wider than its
     * card and, with the overflow now gone, would push the PAGE sideways instead.
     * The theme applies nowrap to every cell, so this has to name both.
     */
    .mm-sticky-cols .table > :not(caption) > * > * {
        white-space: normal;
    }

    /*
     * Pin the header row directly beneath the card head. Its offset is the head's
     * own offset plus the head's measured height - measured, because the head grows
     * when the bulk action bar appears inside it, and a stale number would leave the
     * column row overlapping it or floating below.
     *
     * Sticky is set on the cells rather than on <thead>: support for a sticky thead
     * element is uneven, while sticky th has been reliable for years. The bottom rule
     * is an inset shadow rather than border-bottom, because a border on a sticky
     * table cell is painted with the cell box and disappears as it detaches.
     */
    .mm-sticky-cols thead th {
        position: sticky;
        top: calc(var(--mm-sticky-head-top, 64px) + var(--mm-sticky-head-height, 4.6rem));
        z-index: 2;
        box-shadow: inset 0 -1px 0 var(--mm-border);
    }

    /*
     * With both rows pinned they read as one block, so the shadow belongs under the
     * BOTTOM of that block. Left on the card head it would fall across the column
     * headers as a smudge rather than lifting the pair off the rows.
     */
    .mm-sticky-head.is-stuck {
        box-shadow: none;
    }

    /*
     * Drawn as a gradient on a pseudo-element, for two reasons that each rule out
     * the obvious approach:
     *
     *   1. The cell itself cannot carry it. The table is `border-collapse: collapse`,
     *      and an OUTER box-shadow on a collapsed table cell is unreliable - browsers
     *      clip or drop it. The inset rule above survives, which is the tell: inset
     *      renders, outer does not.
     *
     *   2. A box-shadow on the strip cannot carry it either. --mm-sticky-head-shadow
     *      uses a negative spread to keep the shadow from haloing upward into the
     *      header, and a spread of -6px on a 1px-tall strip gives the shadow a height
     *      of 1 - 12 = -11px. A shadow with a negative resulting box paints nothing
     *      at all, silently.
     *
     * A gradient has no such geometry to get wrong: it fills the strip and fades
     * downward by construction, so it can never bleed back up over the header. One
     * per header cell, tiling edge to edge into a continuous band. It inherits the
     * cell's stacking position, so it paints over the rows travelling underneath.
     */
    .mm-sticky-head.is-stuck ~ .card-body .mm-sticky-cols thead th::after {
        content: "";
        position: absolute;
        left: 0;
        right: 0;
        top: 100%;
        height: 0.5rem;
        background: linear-gradient(to bottom, var(--mm-sticky-head-veil), transparent);
        pointer-events: none;
    }
}

/*
 * The other shape of sticky header: a table given its own bounded scroll region,
 * with the header row pinned to the top of THAT region rather than to the page.
 *
 * This is the right pattern inside a modal, where the dialog already owns the
 * height and a page-level sticky has nothing to stick to. It is the wrong pattern
 * on a page, because it puts a second scrollbar inside the first - use
 * .mm-sticky-cols there instead.
 *
 * Callers set their own max-height inline; the region and the pinning come from here.
 */
.mm-scroll-table {
    overflow: auto;
}

.mm-scroll-table thead th {
    position: sticky;
    top: 0;
    z-index: 2;
    box-shadow: inset 0 -1px 0 var(--mm-border);
}

/*
 * Zero-height marker sitting immediately before the head. When it leaves the top
 * of the viewport the head has begun sticking. It must not affect layout, hence
 * the negative margin cancelling its own height, and it is decorative, hence
 * aria-hidden at the call site.
 */
.mm-sticky-sentinel {
    height: 1px;
    margin-bottom: -1px;
    pointer-events: none;
}

/* ── Sticky save bar and toggle switch ──────────────────────────────── */
/*
 * The bar that pins to the bottom of a settings card while there are unsaved
 * changes, and the switch used inside it.
 *
 * Lifted from app/Views/marketplace/settings.php and
 * app/Views/wishlist/settings.php.
 *
 * public/assets/js/save-bar.js drives the bar, but it toggles .sb-busy,
 * .sb-idle, .wl-savebar__*, .d-none and .is-shaking - none of which are
 * defined here. The classes below are pure CSS, so lifting them cannot affect
 * the script's behaviour.
 */
.save-bar {
    position: sticky;
    bottom: 0;
    z-index: 5;
    background: var(--mm-surface);
    border-top: 1px solid var(--mm-border);
    border-radius: 0 0 0.75rem 0.75rem;
    padding: 0.75rem 1.5rem;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}

.err-text { font-size: 0.72rem; color: var(--mm-danger-solid); margin-top: 0.35rem; }

.sw { display: inline-flex; align-items: center; gap: 0.6rem; cursor: pointer; }
.sw .sw-text { font-size: 0.8rem; font-weight: 600; color: var(--mm-text-primary); }
.sw input { position: absolute; opacity: 0; width: 0; height: 0; }

.sw .track {
    width: 44px;
    height: 24px;
    border-radius: 12px;
    background: var(--mm-toggle-off);
    position: relative;
    transition: background 0.15s ease;
    flex-shrink: 0;
}

.sw .track::after {
    content: '';
    position: absolute;
    top: 3px;
    left: 3px;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: var(--mm-surface);
    transition: transform 0.15s ease;
}

.sw input:checked + .track { background: var(--mm-toggle-on); }
.sw input:checked + .track::after { transform: translateX(20px); }

/* ── Order line item kit ────────────────────────────────────────────── */
/*
 * The condition badge, quantity badge and definition list shown against a card
 * line in picking and packing.
 *
 * Lifted from picking/active.php, picking/queue.php, packing/active.php and
 * packing/queue.php.
 *
 * NOT lifted, and still inline in those four views: the `.cond-badge` BASE
 * rule. ticket-modal.css defines `#tm-order-full .cond-badge` at 1-1-0 and
 * does not set letter-spacing, margin-right or vertical-align. A lifted bare
 * `.cond-badge` would newly reach the badges inside that modal and apply those
 * three, which is a small but real change to a screen this work is not
 * touching. The colour variants below are safe because ticket-modal.css sets
 * its own at 1-1-0, which outranks them there and leaves them free elsewhere.
 */
.cond-badge-nm  { background: var(--mm-solid-muted); }
.cond-badge-lp  { background: var(--mm-success-ink); }
.cond-badge-mp  { background: var(--mm-info-ink); }
.cond-badge-hp,
.cond-badge-dmg { background: var(--mm-danger-ink); }

.channel-badge { display: inline-flex; align-items: center; flex-shrink: 0; line-height: 1; }

/* Express-shipping marker for the two PICKING screens: the bolt glyph alone, with the meaning in
   the title + aria-label. The packing screens do not use this - there, express folds into the
   delivery label itself ("Express Delivery") rather than sitting beside it as a badge.

   colour: inherit rather than a named colour, so the bolt takes the grey of whatever meta line it
   sits in and matches the neighbouring delivery glyph exactly - by construction in both themes,
   rather than by naming a token here that could drift from the one .order-meta uses. Sized to that
   same glyph so the pair reads as one row of icons.

   Speed only: it says nothing about packaging, which the $pkg block owns. */
.express-badge {
    display: inline-flex;
    align-items: center;
    flex-shrink: 0;
    line-height: 1;
    color: inherit;
}
.express-badge .material-symbols-rounded { font-size: 16px; }

.qty-badge {
    position: absolute;
    bottom: -5px;
    right: -5px;
    z-index: 20;
    background: var(--mm-solid);
    color: var(--mm-solid-ink);
    font-size: 0.72rem;
    font-weight: 700;
    line-height: 1;
    padding: 3px 7px;
    border-radius: 20px;
    border: 2px solid var(--mm-surface);
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.25);
    pointer-events: none;
}

.item-meta     { display: flex; flex-direction: column; gap: 3px; margin: 0; padding: 0; }
.item-meta-row { display: flex; align-items: baseline; gap: 4px; }

.item-meta dt {
    font-size: 0.68rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--mm-text-faint);
    min-width: 0;
}

.item-meta dd { font-size: 0.75rem; color: var(--mm-text-primary); margin: 0; }

.item-meta-highlight dt { color: var(--mm-neutral-strong); }
.item-meta-highlight dd {
    font-weight: 600;
    color: var(--mm-info-solid);
    font-family: monospace;
    letter-spacing: 0.02em;
}

/* ── Form field ─────────────────────────────────────────────────────── */
/*
 * The portal's text input, select and textarea. Same well as .mm-search above:
 * soft ground, 1px edge, lifts to white with an accent ring on focus.
 *
 * WHY THIS EXISTS, beyond looks. The theme's bare `.form-control` is broken on
 * focus: material-dashboard.css sets
 *
 *     .form-control:focus { border-color: transparent; box-shadow: none; }
 *
 * and restores the border only through `.input-group.input-group-outline
 * .is-focused`, a class material-dashboard.js:827 puts on the WRAPPER. An input
 * without that wrapper therefore loses its border the moment you click into it.
 * 47 views used a bare `.form-control`; seven of them had independently hand
 * written the same patch to fix it. This primitive is that patch, done once.
 *
 * Focus is --mm-accent, not --mm-focus-border. Chosen deliberately (Dom,
 * 2026-09-04) so the portal has ONE focus colour across search, fields and
 * buttons, rather than search focusing accent while fields focus theme blue.
 *
 * Contrast, all measured on the resting ground: value 8.88:1, placeholder
 * 4.46:1, disabled 4.19:1, focused value 9.36:1. Placeholder and disabled use
 * --mm-text-muted rather than --mm-text-faint, which computes to 1.97:1 and
 * 1.85:1 here and is simply not readable.
 */
.mm-field {
    display: block;
    width: 100%;
    padding: 0.5rem 0.7rem;
    font-family: inherit;
    font-size: 0.85rem;
    line-height: 1.4;
    color: var(--mm-text-primary);
    background-color: var(--mm-surface-hover);
    border: 1px solid var(--mm-border);
    border-radius: 0.5rem;
    transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}

.mm-field::placeholder { color: var(--mm-text-muted); opacity: 1; }

.mm-field:focus {
    background-color: var(--mm-surface);
    border-color: var(--mm-accent);
    box-shadow: 0 0 0 3px var(--mm-accent-ring);
    outline: 0;
}

/* A disabled field still has to be readable: its value is information. */
.mm-field:disabled,
.mm-field[readonly] {
    background-color: var(--mm-surface-sunken);
    color: var(--mm-text-muted);
    cursor: not-allowed;
}

/*
 * Select: our own chevron rather than the browser's.
 *
 * The native arrow sits hard against the right edge and cannot be moved
 * reliably or recoloured, so appearance:none turns it off and the chevron is
 * painted as a background image with room made for it by padding-right.
 * appearance is also set unprefixed AND -webkit- because Safari still needs it.
 */
select.mm-field {
    -webkit-appearance: none;
    appearance: none;
    padding-right: 2.1rem;
    background-image: var(--mm-select-chevron);
    background-repeat: no-repeat;
    background-position: right 0.75rem center;
    background-size: 14px 14px;
}

/* A multiple or sized select is a list, not a dropdown, so it has no chevron. */
select.mm-field[multiple],
select.mm-field[size]:not([size="1"]) {
    background-image: none;
    padding-right: 0.7rem;
}

textarea.mm-field { height: auto; min-height: 4.5rem; resize: vertical; }

/*
 * The field's label. Same eyebrow treatment as .an-label, so a form built from
 * these reads the same as the announcement and message composers.
 */
.mm-label {
    display: block;
    font-size: 0.65rem;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    color: var(--mm-text-muted);
    margin-bottom: 0.3rem;
}

/* ── Checkbox ───────────────────────────────────────────────────────── */
/*
 * The one form control the system was missing.
 *
 * material-dashboard dresses no plain checkbox at all: it only styles
 * `.form-check.form-switch`. A bare `.form-check-input` therefore breaks out of
 * Bootstrap's negative-margin layout, lands on its own line and renders at switch
 * size. That is why every toggle in the portal had been built as a switch, which is
 * the wrong control for a one-off choice: a switch says "this setting persists", a
 * checkbox says "this applies to the thing you are about to do".
 *
 * Border, radius, focus ring and disabled treatment are lifted from .mm-field so a
 * checkbox sits beside an input without looking borrowed.
 *
 * FILL, not ink. The box is --mm-fill-accent carrying a --mm-check-tick. --mm-accent
 * is the obvious choice and the wrong one: it LIGHTENS to #8897f0 in dark mode, which
 * is the pastel-control bug this codebase has hit three times.
 */
.mm-check {
    -webkit-appearance: none;
    appearance: none;
    flex-shrink: 0;
    width: 1.05rem;
    height: 1.05rem;
    margin: 0;
    /* For inline use in a table cell, where `text-center` on the td does the
       centring and there is no flex parent to align against. Ignored inside
       .mm-check-row, which is a flex container. */
    vertical-align: middle;
    background-color: var(--mm-surface-hover);
    border: 1px solid var(--mm-control-border);
    border-radius: 0.3rem;
    cursor: pointer;
    transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease;
}

.mm-check:checked {
    background-color: var(--mm-fill-accent);
    border-color: var(--mm-fill-accent);
    background-image: var(--mm-check-tick);
    background-repeat: no-repeat;
    background-position: center;
    background-size: 0.72rem;
}

.mm-check:hover:not(:disabled) { border-color: var(--mm-fill-accent); }

/* Partial state for a select-all sitting over a list where only some rows are
   ticked. Without it such a checkbox renders as plain unchecked and claims
   nothing is selected. */
.mm-check:indeterminate {
    background-color: var(--mm-fill-accent);
    border-color: var(--mm-fill-accent);
    background-image: linear-gradient(var(--mm-text-inverse), var(--mm-text-inverse));
    background-repeat: no-repeat;
    background-position: center;
    background-size: 0.55rem 2px;
}

/*
 * :focus-visible, not :focus. A checkbox is operated by mouse as often as by
 * keyboard, and a ring that appears on every click reads as a stuck state.
 */
.mm-check:focus-visible {
    border-color: var(--mm-fill-accent);
    box-shadow: 0 0 0 3px var(--mm-accent-ring);
    outline: 0;
}

.mm-check:disabled {
    background-color: var(--mm-surface-sunken);
    border-color: var(--mm-border);
    cursor: not-allowed;
    opacity: 0.65;
}

/*
 * Checkbox, its label, and an optional line of explanation under the label.
 * align-items:flex-start so a wrapping label keeps the box against its first line
 * rather than centring it against a two-line block.
 */
.mm-check-row { display: flex; align-items: flex-start; gap: 0.6rem; }
.mm-check-row .mm-check { margin-top: 0.12rem; }

.mm-check-label {
    font-size: 0.85rem;
    line-height: 1.4;
    color: var(--mm-text-primary);
    cursor: pointer;
    margin: 0;
}

.mm-check:disabled ~ div .mm-check-label,
.mm-check:disabled + .mm-check-label { color: var(--mm-text-muted); cursor: not-allowed; }

.mm-check-hint { font-size: 0.72rem; line-height: 1.45; color: var(--mm-text-muted); margin: 0.2rem 0 0; }

/* A line of help or error text under a field. */
.mm-field-hint { font-size: 0.7rem; color: var(--mm-text-muted); margin-top: 0.25rem; }
.mm-field-error { font-size: 0.72rem; color: var(--mm-danger-solid); margin-top: 0.25rem; }

.mm-field.is-invalid { border-color: var(--mm-danger-solid); }
.mm-field.is-invalid:focus { box-shadow: 0 0 0 3px var(--mm-danger-tint); }

/*
 * Compact field, replacing Bootstrap's .form-control-sm / .form-select-sm.
 * For dense contexts: table row editors, sidebar filters, modal sub-forms.
 */
.mm-field--sm {
    padding: 0.3rem 0.55rem;
    font-size: 0.78rem;
    border-radius: 0.4rem;
}

select.mm-field--sm {
    padding-right: 1.8rem;
    background-position: right 0.55rem center;
    background-size: 12px 12px;
}

/* ── Segmented control ──────────────────────────────────────────────── */
/*
 * A small set of mutually exclusive options, sitting inline with a field:
 * the g/kg unit picker on /retail/create, the pie/bar switch on the tournament
 * report.
 *
 * Both were the same older treatment - a grey track holding a white active pill
 * with a drop shadow - and both sat at a different height from the input beside
 * them, so nothing lined up. The box model here is deliberately the SAME
 * arithmetic as .mm-field, so the two are exactly equal height:
 *
 *   .mm-field  1px border + 0.5rem  padding + line + 0.5rem  padding + 1px
 *   .mm-seg    1px border + 3px pad + (0.5rem - 3px) + line + ... + 3px + 1px
 *
 * The active state is the accent tint used by .mm-pill and .pn-seg-btn rather
 * than a white pill, so a selected segment reads the same as a selected filter.
 *
 * Both `.is-active` and `.active` are honoured: the existing scripts set
 * `.active` and one of them reads it back with querySelector('.active'), so
 * renaming the state class would be a JS change for no gain.
 */
.mm-seg {
    display: inline-flex;
    align-items: stretch;
    /*
     * Wraps rather than overflowing. Not scoped to a breakpoint, because the
     * control fits on one line whenever there is room and this only takes
     * effect when it would otherwise run past its card. The range presets in
     * analytics/_filters.php measured 469.6px against 364px of card body, and
     * with nothing in the chain to <body> setting `overflow` that pushed all
     * six /analytics pages sideways at rest. The `gap` below applies to both
     * axes, so a wrapped row is already spaced.
     */
    flex-wrap: wrap;
    gap: 2px;
    padding: 3px;
    background: var(--mm-surface-hover);
    border: 1px solid var(--mm-border);
    border-radius: 0.5rem;
}

.mm-seg button,
.mm-seg > a {
    border: 0;
    background: none;
    cursor: pointer;
    font-family: inherit;
    font-size: 0.85rem;
    font-weight: 600;
    line-height: 1.4;
    padding: calc(0.5rem - 3px) 0.65rem;
    color: var(--mm-text-muted);
    border-radius: 0.35rem;
    white-space: nowrap;
    transition: background-color 0.15s ease, color 0.15s ease;
}

.mm-seg button:hover:not(.is-active):not(.active),
.mm-seg > a:hover:not(.is-active):not(.active) {
    color: var(--mm-text-primary);
}

.mm-seg button.is-active,
.mm-seg button.active,
.mm-seg > a.is-active,
.mm-seg > a.active {
    background: var(--mm-accent-tint);
    color: var(--mm-accent-strong);
}

/* Sits beside a labelled field. The label sits ABOVE the input, so centring the
   control against the whole column pushes it above the input it belongs to;
   aligning to the end lines it up with the field itself. */
.mm-seg > a { text-decoration: none; display: inline-flex; align-items: center; }

.mm-seg--field-end { align-self: flex-end; }

/* ── Switch ─────────────────────────────────────────────────────────── */
/*
 * Restyles the theme's `.form-switch`, used 29 times across 19 views. Done as
 * an override rather than a new class so every switch in the portal moves at
 * once with no markup change.
 *
 * What was dated about it: material-dashboard.css draws a 30x15 track and then
 * puts a 20x20 knob on top of it, positioned at top:-2.5px / left:-5px so it
 * OVERFLOWS the track on every side, with a 1px border and a heavy drop shadow.
 * The knob now fits inside the track, loses the border, and the shadow is a
 * hairline.
 *
 * The track width is deliberately unchanged at 1.875rem. Bootstrap positions
 * the label with a negative margin keyed to that width, so widening the switch
 * would shift the label on all 29 instances.
 *
 * On is --mm-toggle-on, the same green as the save bar's switch, so the portal
 * has one switch rather than two.
 */
.form-switch .form-check-input {
    height: 1.125rem;
    border: 0;
    border-radius: 999px;
    background-color: var(--mm-toggle-off);
    background-image: none;
    box-shadow: none;
    transition: background-color 0.2s ease;
}

.form-switch .form-check-input:after {
    width: 0.875rem;
    height: 0.875rem;
    top: 2px;
    left: 2px;
    border: 0;
    background-color: var(--mm-surface);
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
    transform: translateX(0);
    transition: transform 0.2s ease;
}

.form-switch .form-check-input:checked {
    background-color: var(--mm-toggle-on);
    border-color: transparent;
}

.form-switch .form-check-input:checked:after {
    transform: translateX(12px);
    border-color: transparent;
}

/* The theme fires a 15px ring on :active. Too loud now the control is smaller. */
.form-switch .form-check-input:active:after,
.form-switch .form-check-input:checked:active:after {
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
}

.form-switch .form-check-input:focus {
    border-color: transparent;
    box-shadow: none;
}

.form-switch .form-check-input:focus-visible {
    outline: 2px solid var(--mm-accent);
    outline-offset: 2px;
}

.form-switch .form-check-input:disabled {
    opacity: 0.5;
    cursor: not-allowed;
}

/* ── Dropdown menu ──────────────────────────────────────────────────── */
/*
 * Restyles the theme's dropdown, used by 22 menus and 86 items. Driven through
 * Bootstrap's own --bs-dropdown-* custom properties rather than by fighting
 * individual rules, so the show/hide animation and positioning are untouched.
 *
 * What was dated: a 1.625rem gap that floated the menu away from its button, a
 * large radius, generous item padding, and a white pointer triangle notched
 * into the top edge. The notch is removed in custom.css, where it was drawn.
 *
 * The item inset is the PANEL's padding-x, not a margin on the item. The menu
 * is position:absolute and so shrink-wraps to its widest child; with a margin
 * and width:auto a short item sized to its own text, so its hover background
 * stopped short of the panel edge while a long one filled it. Panel padding
 * plus width:100% makes every row the same width whatever its label.
 */
.dropdown-menu,
.dropup .dropdown-menu {
    --bs-dropdown-min-width: 12rem;
    --bs-dropdown-padding-y: 0.35rem;
    --bs-dropdown-padding-x: 0.35rem;
    --bs-dropdown-spacer: 0.35rem;
    --bs-dropdown-font-size: 0.8rem;
    --bs-dropdown-color: var(--mm-text-primary);
    --bs-dropdown-bg: var(--mm-surface);
    --bs-dropdown-border-color: var(--mm-border);
    --bs-dropdown-border-width: 1px;
    --bs-dropdown-border-radius: 0.5rem;
    --bs-dropdown-inner-border-radius: 0.5rem;
    --bs-dropdown-item-padding-y: 0.45rem;
    --bs-dropdown-item-padding-x: 0.85rem;
    --bs-dropdown-link-color: var(--mm-text-primary);
    --bs-dropdown-link-hover-color: var(--mm-text-primary);
    --bs-dropdown-link-hover-bg: var(--mm-surface-hover);
    --bs-dropdown-link-active-color: var(--mm-accent-strong);
    --bs-dropdown-link-active-bg: var(--mm-accent-tint);
    --bs-dropdown-link-disabled-color: var(--mm-text-faint);
    --bs-dropdown-divider-bg: var(--mm-rule);
    --bs-dropdown-divider-margin-y: 0.35rem;
    box-shadow: 0 8px 28px -10px rgba(20, 20, 20, 0.28), 0 2px 8px -4px rgba(20, 20, 20, 0.12);
}

.dropdown .dropdown-menu .dropdown-item {
    display: flex;
    align-items: center;
    gap: 0.55rem;
    font-size: 0.8rem;
    font-weight: 500;
    border-radius: 0.35rem;
    width: 100%;
}

/* The icon reads as chrome beside the label, and takes the item's colour on
   hover so the whole row moves together. */
.dropdown .dropdown-menu .dropdown-item .material-symbols-rounded {
    font-size: 17px;
    line-height: 1;
    color: var(--mm-text-muted);
    margin: 0;
}

.dropdown .dropdown-menu .dropdown-item:hover .material-symbols-rounded { color: inherit; }

/* Full bleed. The panel now carries horizontal padding so the items can inset,
   which would otherwise leave the rule floating short of both edges. */
.dropdown .dropdown-menu .dropdown-divider {
    margin-left: calc(var(--bs-dropdown-padding-x) * -1);
    margin-right: calc(var(--bs-dropdown-padding-x) * -1);
}

/*
 * An item that states itself in colour takes its icon with it. The rule above
 * paints every dropdown icon grey so it reads as chrome beside the label, but
 * that also caught the icon on a coloured item and left, for example, a red
 * "Archive Consignment" sitting behind a grey archive glyph.
 *
 * `inherit` rather than a named colour, so this covers text-success and
 * text-warning too, and any future variant, without listing them.
 */
.dropdown .dropdown-menu .dropdown-item.text-danger .material-symbols-rounded,
.dropdown .dropdown-menu .dropdown-item.text-success .material-symbols-rounded,
.dropdown .dropdown-menu .dropdown-item.text-warning .material-symbols-rounded,
.dropdown .dropdown-menu .dropdown-item.text-info .material-symbols-rounded {
    color: inherit;
}

/* Destructive hover reads in its own colour rather than the neutral grey. */
.dropdown .dropdown-menu .dropdown-item.text-danger:hover {
    background: var(--mm-danger-tint);
    color: var(--mm-danger-ink);
}

.dropdown .dropdown-menu .dropdown-item.text-success:hover {
    background: var(--mm-success-tint);
    color: var(--mm-success-ink);
}

.dropdown .dropdown-menu .dropdown-item.text-warning:hover {
    background: var(--mm-warning-tint);
    color: var(--mm-warning-ink);
}

.dropdown .dropdown-menu .dropdown-item.disabled,
.dropdown .dropdown-menu .dropdown-item.disabled .material-symbols-rounded {
    color: var(--mm-text-faint);
}

/*
 * A control that sits in a filter row beside labelled fields but carries no
 * label of its own: a switch, a segmented control, a lone button.
 *
 * Without this the row cannot line up. Its columns are different heights (a
 * labelled field is the label plus the control, a bare switch is ~20px), so
 * centring floats the short ones and bottom-aligning drops them below the
 * field's centre line. Matching .mm-field's height and centring inside it puts
 * every control on one centre line whatever its own size.
 *
 * The height is .mm-field's box arithmetic, not a magic number: two 1px
 * borders, 0.5rem padding top and bottom, and one 0.85rem line at 1.4.
 */
.mm-field-inline {
    display: flex;
    align-items: center;
    min-height: calc(1.4 * 0.85rem + 1rem + 2px);
}

/*
 * A button sitting inline with a labelled field: a barcode lookup beside its
 * input, a Go beside a search.
 *
 * Matches .mm-field's height by the same arithmetic .mm-seg and
 * .mm-field-inline use, and aligns to the end of its row so the label above the
 * field does not push it out of line. Without this a --sm button is 28px next
 * to a 37px input, and a default one is 30px.
 */
.mm-btn--field {
    min-height: calc(1.4 * 0.85rem + 1rem + 2px);
    align-self: flex-end;
}

/* ── Required marker ────────────────────────────────────────────────── */
/*
 * The asterisk on a required field's label. Its own class rather than
 * Bootstrap's .text-danger, so it reads from the token scale and moves with
 * dark mode. Half the labels used .text-danger and half a bare grey asterisk,
 * which meant the marker said "required" on some fields and nothing on others.
 */
.mm-req {
    color: var(--mm-danger-ink);
    font-weight: 700;
    margin-left: 0.15rem;
}

/* ── Invalid field ──────────────────────────────────────────────────── */
/*
 * Set by the form's own validation on submit, not by :invalid. The browser
 * pseudo-class matches an empty required field from first paint, which would
 * paint a blank form red before anyone had touched it.
 */
.mm-field.is-invalid,
.mm-seg.is-invalid {
    border-color: var(--mm-danger-solid);
    background-color: var(--mm-danger-tint);
}

.mm-field.is-invalid:focus {
    border-color: var(--mm-danger-solid);
    box-shadow: 0 0 0 3px var(--mm-danger-tint);
}

.mm-label.is-invalid { color: var(--mm-danger-ink); }

/* ── Notice ─────────────────────────────────────────────────────────── */
/*
 * An inline message: a validation error, a status note, a confirmation. 85 of
 * them across 44 views, all previously Bootstrap `.alert` wearing Material
 * Dashboard's gradient fills with white text.
 *
 * A tint with matching ink instead, in the same language as .mm-badge and the
 * documentation callouts. `text-white` is dropped at every call site when this
 * is applied: white on a light tint is unreadable, and it was only there to sit
 * on the gradient.
 *
 * `.alert` is KEPT on dismissible notices. Bootstrap's close button resolves
 * its target with closest('.alert'), so removing the class would leave the X
 * doing nothing. The rules below win on load order, being equal specificity in
 * a sheet that loads after the theme, and the `background` shorthand is what
 * clears the theme's gradient.
 */
.mm-notice {
    display: flex;
    align-items: flex-start;
    gap: 0.55rem;
    padding: 0.65rem 0.9rem;
    border: 1px solid transparent;
    border-radius: 0.5rem;
    font-size: 0.8rem;
    font-weight: 500;
    line-height: 1.5;
    background: var(--mm-neutral-tint);
    color: var(--mm-text-primary);
    margin-bottom: 1rem;
}

.mm-notice .material-symbols-rounded { font-size: 18px; line-height: 1.3; flex-shrink: 0; }

.mm-notice--info    { background: var(--mm-info-tint);    color: var(--mm-info-ink);    border-color: var(--mm-info-border); }
.mm-notice--success { background: var(--mm-success-tint); color: var(--mm-success-ink); border-color: var(--mm-success-border); }
.mm-notice--warning { background: var(--mm-warning-tint); color: var(--mm-warning-ink); border-color: var(--mm-warning-border); }
.mm-notice--danger  { background: var(--mm-danger-tint);  color: var(--mm-danger-ink);  border-color: var(--mm-danger-border); }
.mm-notice--neutral { background: var(--mm-neutral-tint); color: var(--mm-text-primary); border-color: var(--mm-border); }

/*
 * The dismiss X, when the notice keeps Bootstrap's .alert-dismissible.
 *
 * The markup is an empty <button class="btn-close">, and the theme sets
 * `.alert-dismissible .btn-close { background-image: none }`, so the glyph was
 * not drawn at all: the control has been invisible, and you had to know it was
 * there. Painted here as a mask so it takes the notice's own ink through
 * currentColor and stays right in every variant.
 */
.mm-notice.alert-dismissible {
    position: relative;
    padding-right: 2.4rem;
}

.mm-notice.alert-dismissible .btn-close {
    position: absolute;
    top: 0.55rem;
    right: 0.6rem;
    width: 0.85rem;
    height: 0.85rem;
    padding: 0;
    opacity: 0.55;
    background-color: currentColor;
    background-image: none;
    border: 0;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 4l8 8M12 4l-8 8' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E") center / contain no-repeat;
    mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 4l8 8M12 4l-8 8' stroke='black' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E") center / contain no-repeat;
}

.mm-notice.alert-dismissible .btn-close:hover { opacity: 1; }

/* ══ DARK THEME CHROME ═════════════════════════════════════════════════
   The theme's own dark rules cover a lot (sidenav, cards, dropdowns, tables)
   but they get three things wrong for this portal, and cover two things not at
   all. Tokens cannot reach any of it, so it is handled here rather than in
   tokens.css, which stays a pure :root block.
   ════════════════════════════════════════════════════════════════════════ */

/*
 * The sidenav. material-dashboard.css darkens `.dark-version .sidenav` and then
 * immediately forces it BACK to white for `.dark-version .sidenav.bg-white`,
 * which is its "keep a white sidenav in dark mode" option. Our sidenav carries
 * bg-white, so it opted in by accident and stayed white while the page went
 * dark. Both declarations are !important, so this has to be too.
 */
:root[data-mm-theme="dark"] .sidenav.bg-white {
    background: var(--mm-surface) !important;
    border: 1px solid var(--mm-border) !important;
}

:root[data-mm-theme="dark"] .sidenav.bg-white .navbar-nav .nav-link,
:root[data-mm-theme="dark"] .sidenav.bg-white .collapse .nav-item .nav-link:not(.active),
:root[data-mm-theme="dark"] .sidenav.bg-white .collapse .nav-item .nav-link:not(.active) i,
:root[data-mm-theme="dark"] .sidenav.bg-white .collapse .nav-item h6 {
    color: var(--mm-text-muted) !important;
}

:root[data-mm-theme="dark"] .sidenav.bg-white .navbar-nav .nav-link.active,
:root[data-mm-theme="dark"] .sidenav.bg-white .navbar-nav .nav-link.active i {
    color: var(--mm-text-primary) !important;
}

:root[data-mm-theme="dark"] .sidenav.bg-white .navbar-nav .nav-link.active {
    background-color: var(--mm-surface-sunken) !important;
}

:root[data-mm-theme="dark"] .sidenav.bg-white .navbar-nav .nav-link:hover {
    background-color: var(--mm-surface-hover) !important;
}

/*
 * The section headings (ECOMMERCE, SERVICES, TOOLS). Colour only: they were
 * grouped with the rule below and inherited its background, which painted a
 * band behind every heading.
 */
:root[data-mm-theme="dark"] .sidenav .navbar-nav > .nav-item > h6 {
    color: var(--mm-text-faint) !important;
    background: none !important;
}

/* The rules. The theme draws these as a gradient image, not a border. */
:root[data-mm-theme="dark"] .sidenav-header + hr,
:root[data-mm-theme="dark"] .sidenav hr.horizontal {
    background-image: none;
    background-color: var(--mm-border);
}

/*
 * Sidenav text, dark theme.
 *
 * These selectors MIRROR the theme's own, prefix included, because the theme is
 * playing at 0-8-0 with !important and anything shorter loses. The whole nav is
 * wrapped in `<div class="collapse navbar-collapse" id="sidenav-collapse-main">`,
 * so `.collapse .nav-item .nav-link` matches EVERY item, not just the children
 * of an open group. That is what made two earlier attempts fail: they read as
 * specific enough, and were not.
 *
 * The rule being beaten is:
 *   .dark-version .sidenav.bg-white .collapse .nav-item
 *     .nav-link.active.text-dark .nav-link-text { color: #262626 !important }
 *
 * which paints the OPEN PARENT of the page you are on near-black, on a dark
 * pill. Adding the scope attribute takes ours to 0-9-1.
 */
:root[data-mm-theme="dark"] .sidenav.bg-white .collapse .nav-item .nav-link,
:root[data-mm-theme="dark"] .sidenav.bg-white .collapse .nav-item .nav-link .nav-link-text,
:root[data-mm-theme="dark"] .sidenav.bg-white .collapse .nav-item .nav-link .sidenav-normal,
:root[data-mm-theme="dark"] .sidenav.bg-white .collapse .nav-item .nav-link:not(.active) i,
:root[data-mm-theme="dark"] .sidenav.bg-white .collapse .nav-item h6,
:root[data-mm-theme="dark"] .sidenav .nav-link.text-dark,
:root[data-mm-theme="dark"] .sidenav h6.text-dark {
    color: var(--mm-text-muted) !important;
}

/* The open parent and the current page read brighter than their siblings. */
:root[data-mm-theme="dark"] .sidenav.bg-white .collapse .nav-item .nav-link.active,
:root[data-mm-theme="dark"] .sidenav.bg-white .collapse .nav-item .nav-link.active.text-dark,
:root[data-mm-theme="dark"] .sidenav.bg-white .collapse .nav-item .nav-link.active.text-dark i,
:root[data-mm-theme="dark"] .sidenav.bg-white .collapse .nav-item .nav-link.active.text-dark .nav-link-text,
:root[data-mm-theme="dark"] .sidenav.bg-white .collapse .nav-item .nav-link.active.text-dark .sidenav-normal,
:root[data-mm-theme="dark"] .sidenav.bg-white .collapse .nav-item .nav-link[aria-expanded="true"],
:root[data-mm-theme="dark"] .sidenav.bg-white .collapse .nav-item .nav-link[aria-expanded="true"] .nav-link-text,
:root[data-mm-theme="dark"] .sidenav.bg-white .collapse .nav-item .nav-link[aria-expanded="true"] i {
    color: var(--mm-text-primary) !important;
}

/* The theme dims every sidenav icon with .opacity-5, which on a dark ground
   leaves it barely visible. */
:root[data-mm-theme="dark"] .sidenav .nav-link i.opacity-5 {
    opacity: 0.85;
}


/* The brand name beside the logo. */
:root[data-mm-theme="dark"] .sidenav-header .navbar-brand span,
:root[data-mm-theme="dark"] .sidenav-header .navbar-brand {
    color: var(--mm-text-primary) !important;
}

/*
 * The page ground. `bg-gray-100` on <body> is a Bootstrap utility with
 * !important, and the theme's one body-scoped dark rule does not beat it.
 */
:root[data-mm-theme="dark"] body,
:root[data-mm-theme="dark"] body.bg-gray-100,
:root[data-mm-theme="dark"] body.bg-gray-200 {
    background-color: var(--mm-app-bg) !important;
    color: var(--mm-text-primary);
}

/* The top navbar, which .dark-version does not touch at all. */
:root[data-mm-theme="dark"] .navbar,
:root[data-mm-theme="dark"] .navbar .navbar-brand,
:root[data-mm-theme="dark"] .navbar .nav-link {
    color: var(--mm-text-primary);
}

/* Modals, also untouched by .dark-version. */
:root[data-mm-theme="dark"] .modal-content {
    background-color: var(--mm-surface);
    color: var(--mm-text-primary);
    border: 1px solid var(--mm-border);
}

:root[data-mm-theme="dark"] .modal-header,
:root[data-mm-theme="dark"] .modal-footer {
    border-color: var(--mm-rule);
}

/* Bootstrap's close X is a black SVG. Invert it rather than ship a second asset. */
:root[data-mm-theme="dark"] .btn-close {
    filter: invert(1) grayscale(1) brightness(1.8);
}

/* Cards: the theme darkens them, but not their header rule or muted copy. */
:root[data-mm-theme="dark"] .card {
    background-color: var(--mm-surface);
    border: 1px solid var(--mm-border);
}

:root[data-mm-theme="dark"] .card-header {
    background-color: transparent;
    border-color: var(--mm-rule);
}

/* Tables. The theme sets a dark cell colour but leaves the head and the borders. */
:root[data-mm-theme="dark"] .table > :not(caption) > * > * {
    background-color: transparent;
    color: var(--mm-text-primary);
    border-color: var(--mm-rule);
}

:root[data-mm-theme="dark"] .table thead th {
    color: var(--mm-text-muted);
    border-color: var(--mm-border);
}

/*
 * Bootstrap utilities, dark theme.
 *
 * material-dashboard.css compiles these with HARDCODED values, not Bootstrap
 * 5.3's `var(--bs-border-color)` form:
 *
 *     .border-top { border-top: 1px solid #d4d4d4 !important; }
 *
 * So redefining --bs-* does nothing for them, which is why the divider under the
 * follow-ups table stayed a light hairline through two attempted fixes. 58
 * utilities carry a literal like this; the ones the portal actually uses are
 * restated below. Each needs !important, because each original has it.
 *
 * The --bs-* block is kept as well: components (not utilities) do read those.
 */
:root[data-mm-theme="dark"] {
    --bs-body-bg: var(--mm-app-bg);
    --bs-body-color: var(--mm-text-primary);
    --bs-border-color: var(--mm-border);
    --bs-border-color-translucent: var(--mm-border);
    --bs-secondary-bg: var(--mm-surface-sunken);
    --bs-tertiary-bg: var(--mm-surface-hover);
    --bs-emphasis-color: var(--mm-text-primary);
    --bs-secondary-color: var(--mm-text-muted);
    --bs-heading-color: var(--mm-text-primary);
    --bs-link-color: var(--mm-accent);
    --bs-link-hover-color: var(--mm-accent-strong);
}

/* Borders. `.border-top` alone is the follow-ups divider. */
:root[data-mm-theme="dark"] .border        { border-color: var(--mm-border) !important; }
:root[data-mm-theme="dark"] .border-top    { border-top-color: var(--mm-border) !important; }
:root[data-mm-theme="dark"] .border-bottom { border-bottom-color: var(--mm-border) !important; }
:root[data-mm-theme="dark"] .border-start  { border-left-color: var(--mm-border) !important; }
:root[data-mm-theme="dark"] .border-end    { border-right-color: var(--mm-border) !important; }
:root[data-mm-theme="dark"] .border-light  { border-color: var(--mm-border) !important; }
:root[data-mm-theme="dark"] .border-dark,
:root[data-mm-theme="dark"] .border-secondary { border-color: var(--mm-border-strong) !important; }
:root[data-mm-theme="dark"] .border-white  { border-color: var(--mm-border-strong) !important; }

/* Grounds. */
:root[data-mm-theme="dark"] .bg-white,
:root[data-mm-theme="dark"] .bg-body       { background-color: var(--mm-surface) !important; }
:root[data-mm-theme="dark"] .bg-light,
:root[data-mm-theme="dark"] .bg-gray-100,
:root[data-mm-theme="dark"] .bg-gray-200   { background-color: var(--mm-app-bg) !important; }
:root[data-mm-theme="dark"] .bg-gray-300   { background-color: var(--mm-surface-sunken) !important; }
:root[data-mm-theme="dark"] .bg-secondary  { background-color: var(--mm-solid-muted) !important; }
:root[data-mm-theme="dark"] .bg-dark       { background-color: var(--mm-solid) !important; }

/* Ink. `.text-dark` is near-black by definition and unreadable on a dark ground;
   outside the sidenav it is used to mean "the strong text colour". */
:root[data-mm-theme="dark"] .text-dark     { color: var(--mm-text-primary) !important; }
:root[data-mm-theme="dark"] .text-secondary,
:root[data-mm-theme="dark"] .text-muted,
:root[data-mm-theme="dark"] .text-body     { color: var(--mm-text-muted) !important; }
:root[data-mm-theme="dark"] .text-light    { color: var(--mm-text-faint) !important; }

/* Semantic ink, lifted so it holds against a dark ground. */
:root[data-mm-theme="dark"] .text-primary  { color: var(--mm-accent) !important; }
:root[data-mm-theme="dark"] .text-success  { color: var(--mm-success-ink) !important; }
:root[data-mm-theme="dark"] .text-info     { color: var(--mm-info-ink) !important; }
:root[data-mm-theme="dark"] .text-warning  { color: var(--mm-warning-ink) !important; }
:root[data-mm-theme="dark"] .text-danger   { color: var(--mm-danger-ink) !important; }

/*
 * Sticky navbar.
 *
 * material-dashboard.js:419 adds `blur` and `shadow-blur` to #navbarBlur as soon as
 * you scroll, and .blur is `background-color: rgba(255,255,255,.9) !important` plus a
 * white inset ring. On a light page that is the intended frosted bar; in dark mode it
 * is a white slab across the top of the screen. Both original rules carry !important,
 * so these need it too, and the theme scope supplies the specificity.
 */
:root[data-mm-theme="dark"] .navbar-main.blur {
    background-color: var(--mm-navbar-blur-bg) !important;
    box-shadow: inset 0 0 2px var(--mm-navbar-blur-ring) !important;
}

:root[data-mm-theme="dark"] .navbar-main.blur.shadow-blur {
    box-shadow: inset 0 0 1px 1px var(--mm-navbar-blur-ring),
                0 3px 12px 0 var(--mm-navbar-blur-shadow) !important;
}

/*
 * The same .blur trap, one level out.
 *
 * Three read-only context cards inside modals carry `card card-body blur shadow-blur`
 * with an inline `background-color: var(--mm-surface-hover)`: the item context panel in
 * the consignment edit and cancel modals, and the equivalent in the PSA order view. The
 * inline token never applied in ANY theme, because .blur is
 * `background-color: rgba(255,255,255,.9) !important` and !important beats a plain
 * inline style. In light mode that is invisible, both values being near white. In dark
 * mode it is a white slab carrying light text at roughly 1.2:1.
 *
 * The fix above was scoped to .navbar-main, so it never reached these. Scoped to
 * .card here for the same reason: .blur alone would also catch anything the vendor JS
 * decorates.
 *
 * No new token: --mm-surface-hover is the ground these cards already asked for and
 * --mm-border is the hairline role. The drop shadow is dropped rather than re-tinted;
 * a small panel inside an open modal has nothing to lift off.
 */
:root[data-mm-theme="dark"] .card.blur,
:root[data-mm-theme="dark"] .card.blur.shadow-blur {
    background-color: var(--mm-surface-hover) !important;
    box-shadow: inset 0 0 0 1px var(--mm-border) !important;
}

/*
 * The bar carries `mt-2 top-0`, so it sits 0.5rem down the page but sticks flush
 * against the viewport edge, and the gap it had while scrolling just vanishes. Match
 * the stuck offset to the margin it already has so the transition is continuous.
 */
.navbar-main.position-sticky {
    top: 0.5rem !important;
}

/*
 * Sidenav search trigger.
 *
 * Geometry follows `.mm-navctl` in layout/navbar.php, which the bell and the account
 * menu still use, so the control does not look like it arrived from a different system
 * after the move out of the header. It differs in being full width and left aligned:
 * it sits alone under the brand rather than in a row, so it can read as a field.
 */
.mm-sidenav-search {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    width: 100%;
    height: 34px;
    padding: 0 0.6rem;
    border: 1px solid var(--mm-border-control);
    border-radius: 0.5rem;
    background: transparent;
    color: var(--mm-text-muted);
    font-size: 0.75rem;
    line-height: 1;
    text-align: left;
    transition: all 0.15s ease;
}

.mm-sidenav-search:hover {
    background: var(--mm-surface-hover);
    border-color: var(--mm-text-faint);
    color: var(--mm-text-primary);
}

.mm-sidenav-search .material-symbols-rounded {
    font-size: 18px;
    flex-shrink: 0;
}

/* The label takes the slack, so the shortcut chip stays pinned to the right edge
   whatever width the sidenav is at. */
.mm-sidenav-search-label {
    flex: 1 1 auto;
}

/*
 * Dark baseline for form controls.
 *
 * An <input> with no explicit `background` falls back to the user agent's white,
 * whatever the theme is doing. That is what made the "Other printings" box in the
 * Find alternatives drawer a white slab, and every hand rolled input in the portal
 * is one missing declaration away from the same bug.
 *
 * The :where() wrapper is load bearing, not decoration. Written plainly,
 * `:root[data-mm-theme="dark"] input` computes to 0-2-1, which BEATS a plain
 * `.mm-field` at 0-1-0 and would repaint every properly styled field in the portal.
 * :where() contributes zero specificity, so the whole selector weighs 0-0-1 and
 * loses to any class rule at all. It paints only controls that nothing else paints.
 *
 * Checkboxes, radios and ranges are excluded for the same reason: they draw their
 * own control, and a surface fill behind one just boxes it in. That :not() is also
 * inside :where(), or it would drag the specificity back up to 0-1-1.
 */
:where(:root[data-mm-theme="dark"]) input:where(:not([type="checkbox"]):not([type="radio"]):not([type="range"]):not([type="color"])),
:where(:root[data-mm-theme="dark"]) textarea,
:where(:root[data-mm-theme="dark"]) select {
    /* Sunken, not --mm-surface: a field that matches the card it sits on reads as a
       gap rather than an input. This rule is dark-only, so light mode is untouched. */
    background-color: var(--mm-surface-sunken);
    color: var(--mm-text-primary);
    border-color: var(--mm-border-strong);
}

:where(:root[data-mm-theme="dark"]) input::placeholder,
:where(:root[data-mm-theme="dark"]) textarea::placeholder {
    color: var(--mm-text-muted);
    opacity: 1;
}

/*
 * Tab strip. THE canonical tab system for the portal.
 *
 * It began life in panels.css as the /admin/settings tab strip, and stayed there while
 * only two pages used it. It is here now because converting the Summary/Timeline pills
 * moved four more pages onto it - consignments, PSA orders, graded purchases and the
 * changelog - and not one of them loads panels.css, so every one of those tabs rendered
 * as a raw browser button. A primitive this widely used belongs beside the other
 * primitives, in the sheet every page already has.
 *
 * The class names keep the `.set-` prefix rather than becoming `.mm-tab*`: renaming
 * would touch settings/index.php and partners/view.php for no behavioural gain, and
 * `.mm-tab*` is the namespace customers/view.php still occupies.
 */
/* Top level tab strip: an underline, one row, scrolls rather than wraps awkwardly. */
.set-tabs { display:flex; flex-wrap:wrap; gap:0.25rem; border-bottom:1px solid var(--mm-border); }
.set-tab {
  display:inline-flex; align-items:center; gap:0.35rem;
  background:none; border:0; border-bottom:2px solid transparent; margin-bottom:-1px;
  padding:0.6rem 0.9rem; font-size:0.8rem; font-weight:600; color:var(--mm-text-muted);
  text-decoration:none; white-space:nowrap; cursor:pointer;
  transition:color .15s ease, border-color .15s ease;
}
.set-tab:hover { color:var(--mm-text-primary); }
.set-tab.active { color:var(--mm-accent); border-bottom-color:var(--mm-accent); }
.set-tab .material-symbols-rounded { font-size:17px; }

/* A count sitting beside a tab label. Muted, so it never competes with the label. */
.set-tab-count {
  font-size:0.68rem; font-weight:700; color:var(--mm-text-muted);
  background:var(--mm-surface-sunken); border-radius:999px; padding:0.05rem 0.4rem;
}
.set-tab.active .set-tab-count { background:var(--mm-accent-tint); color:var(--mm-accent); }

/* Bootstrap's `.nav` rides along on three of these strips, because Chart-less pages let
   the Tab plugin find its group with closest('.nav, .list-group'). `.nav` sets its own
   display and flex-wrap at the same specificity, so ui.css loading after the theme is
   what keeps the strip laid out by the rules above. */

/*
 * Scrollbars, and native controls generally.
 *
 * `color-scheme` is the actual fix here, not the scrollbar rules below it. Until it is
 * set, the browser has no idea the page is dark, so it paints its LIGHT scrollbar: a
 * white track with a grey thumb, which is what showed up against the dark timeline panel.
 * material-dashboard.css does set `color-scheme: dark`, but only under `[data-bs-theme]`,
 * an attribute nothing in this portal has ever set, so that rule has never once applied.
 *
 * Setting it fixes more than the scrollbar. Date pickers, select popups, the spinner on a
 * number input and Chrome's autofill highlight are all drawn by the browser, and all of
 * them follow color-scheme rather than any stylesheet we can write.
 *
 * The explicit `light` on :root is not redundant: without it a browser in a dark OS theme
 * may infer dark and paint dark scrollbars onto our light pages.
 */
:root {
    color-scheme: light;
}

:root[data-mm-theme="dark"] {
    color-scheme: dark;
}

/* Past that, brand the scrollbar to our own surfaces rather than the UA's dark grey.
   Dark only, deliberately: light keeps the native scrollbar it has always had.
   `scrollbar-color` is the standard property (Firefox, and Chrome 121+). */
:root[data-mm-theme="dark"] {
    scrollbar-color: var(--mm-scroll-thumb) var(--mm-scroll-track);
    scrollbar-width: thin;
}

/* WebKit needs the older pseudo-elements: Safari only learned `scrollbar-color` in 18.2,
   so without these the scrollbar in Safari falls back to the plain color-scheme default. */
:root[data-mm-theme="dark"] ::-webkit-scrollbar {
    width: 10px;
    height: 10px;
}

:root[data-mm-theme="dark"] ::-webkit-scrollbar-track {
    background: var(--mm-scroll-track);
}

:root[data-mm-theme="dark"] ::-webkit-scrollbar-thumb {
    background: var(--mm-scroll-thumb);
    border-radius: 999px;
    /* A transparent border with background-clip is what insets the thumb from the track
       edge; padding does not apply to a scrollbar pseudo-element. */
    border: 2px solid transparent;
    background-clip: content-box;
}

:root[data-mm-theme="dark"] ::-webkit-scrollbar-thumb:hover {
    background: var(--mm-scroll-thumb-hover);
    background-clip: content-box;
}

:root[data-mm-theme="dark"] ::-webkit-scrollbar-corner {
    background: var(--mm-scroll-track);
}

/*
 * Toasts.
 *
 * Top right, 3s, click anywhere to dismiss. Styling comes from the token layer, so this
 * needed no dark-mode block of its own.
 *
 * The stack sits BELOW the sticky navbar rather than at the very top of the viewport,
 * because the navbar is `position:sticky; top:.5rem` and a toast at top:0 would slide
 * under it. z-index clears Bootstrap's modal (1055) so a save inside a modal is still
 * visible, and stays under the command palette (30000), which owns Escape.
 */
.mm-toasts {
    position: fixed;
    top: 4.75rem;
    right: 1.25rem;
    z-index: 20000;
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    /* The container spans no more than it needs, so the page stays clickable around it. */
    pointer-events: none;
    max-width: min(24rem, calc(100vw - 2.5rem));
}

.mm-toast {
    pointer-events: auto;
    display: flex;
    align-items: flex-start;
    gap: 0.55rem;
    padding: 0.7rem 0.85rem;
    border: 1px solid var(--mm-border);
    border-left: 3px solid var(--mm-text-faint);
    border-radius: 0.6rem;
    background: var(--mm-surface);
    box-shadow: var(--mm-toast-shadow);
    color: var(--mm-text-primary);
    font-size: 0.8rem;
    line-height: 1.4;
    cursor: pointer;
    /* Start off-screen right; .is-in brings it in. */
    opacity: 0;
    transform: translateX(0.75rem);
    transition: opacity 0.18s ease, transform 0.18s ease;
}

.mm-toast.is-in {
    opacity: 1;
    transform: translateX(0);
}

.mm-toast.is-leaving {
    opacity: 0;
    transform: translateX(0.75rem);
}

.mm-toast-icon {
    font-size: 18px;
    flex-shrink: 0;
    line-height: 1.2;
}

.mm-toast-text {
    flex: 1 1 auto;
    min-width: 0;
    overflow-wrap: anywhere;
}

.mm-toast-close {
    flex-shrink: 0;
    border: 0;
    background: none;
    padding: 0 0 0 0.25rem;
    margin: -0.1rem 0 0 0;
    font-size: 1.05rem;
    line-height: 1;
    color: var(--mm-text-faint);
    cursor: pointer;
}

.mm-toast-close:hover {
    color: var(--mm-text-primary);
}

/* The accent bar and the icon carry the type. The label stays in body ink rather than
   the status colour: coloured text on a surface is the pattern that keeps failing
   contrast, and the bar already says which kind of toast this is. */
.mm-toast--success { border-left-color: var(--mm-success-ink); }
.mm-toast--success .mm-toast-icon { color: var(--mm-success-ink); }
.mm-toast--error   { border-left-color: var(--mm-danger-ink); }
.mm-toast--error   .mm-toast-icon { color: var(--mm-danger-ink); }
.mm-toast--warning { border-left-color: var(--mm-warning-ink); }
.mm-toast--warning .mm-toast-icon { color: var(--mm-warning-ink); }
.mm-toast--info    { border-left-color: var(--mm-info-ink); }
.mm-toast--info    .mm-toast-icon { color: var(--mm-info-ink); }

@media (prefers-reduced-motion: reduce) {
    .mm-toast {
        transition: opacity 0.18s ease;
        transform: none;
    }
    .mm-toast.is-leaving { transform: none; }
}

@media (max-width: 575.98px) {
    .mm-toasts {
        top: auto;
        bottom: 1rem;
        left: 1rem;
        right: 1rem;
        max-width: none;
    }
}

/* ============================================================================
   PAY TABLE
   A payee list with a breakdown row folded under each entry. Used by /payouts
   Approvals and /payouts Needs attention, which are the same table with a
   different reason for being on it.

   Lifted out of approvals.php on 2026-09-10 when the second page needed it.
   Scoped to `.pay-table` rather than to either page's own class, so neither
   owns it and a third consumer needs no further move.
   ============================================================================ */

/* One measurement, used by every cell. Alignment between a row and its breakdown is
   structural - both end on the Amount column's boundary - so nothing here has to track
   the chevron's width. */
.pay-table { --pay-edge: 1rem; }

/* The standard list-page override: the theme's default cell padding is built for sparser
   tables than this one. Same rule /payouts carries.
   Scoped with `>` so it stops at the outer table and leaves the nested breakdown alone -
   unscoped it beat `.table-sm` on source order and made the detail rows just as roomy. */
.pay-table > thead > tr > th { padding: 0.75rem 1rem; }
.pay-table > tbody > tr > td { padding: 0.75rem 1rem; vertical-align: middle; }

/*
 * OPT-IN, for a table whose rows come in PAIRS - a payee row plus its hidden detail row.
 *
 * The theme clears the bottom border on `.table tbody tr:last-child td`. Where every payee
 * row is followed by a hidden detail row, that rule lands on the hidden one and the last
 * VISIBLE row keeps its line, drawing a rule just above the card's own bottom edge, which
 * reads as a doubled border. The last TWO children fixes it: `:last-child` counts hidden
 * elements, which is the same trap in both directions.
 *
 * It must NOT be part of the base kit. On a table of single rows the same selector strips
 * the border from the last two real rows, and they run into each other.
 */
.pay-table--paired > tbody > tr:nth-last-child(-n+2) > td { border-width: 0; }

/*
 * The details disclosure.
 *
 * Sunken so a nested panel looks nested, and its own padding rather than the row padding
 * above, which is sized for a two-line payee row and far too airy for a list of items.
 *
 * Selected through the full row path, not as a bare `.pay-items-cell`. The theme's
 * `.table > :not(caption) > * > *` sets `background-color` and scores (0,1,1), which beats
 * a lone class at (0,1,0) - so the sunken ground was silently being painted back to the
 * table background and the panel looked white. This selector scores (0,2,3) and wins
 * honestly, which is also why the padding no longer needs !important.
 */
.pay-table > tbody > tr > td.pay-items-cell,
.pay-table > tbody > tr > td.pay-items-gutter {
    background: var(--mm-surface-sunken);
}

/* The gutter carries the ground only. It sits in the checkbox column, so it is already
   exactly as wide as it needs to be. */
.pay-table > tbody > tr > td.pay-items-gutter { padding: 0; }

.pay-table > tbody > tr > td.pay-items-cell {
    padding-top: 0.4rem;
    padding-bottom: 0.55rem;
    /* The same left padding every other cell has, so item names sit on the same x as the
       payee name above them. */
    padding-left: var(--pay-edge);
    /* Just the cell edge. The chevron has its own column and the panel ends on the Amount
       column's boundary, so the amounts line up structurally rather than by a measurement
       someone has to keep in step with `.mm-btn--icon`. */
    padding-right: var(--pay-edge);
}

/* One line per item. The theme sets `white-space: nowrap` on every table cell, so the
   truncation has to happen on the flex child that is allowed to shrink. */
.pay-item {
    display: flex;
    align-items: baseline;
    gap: 0.5rem;
    padding: 0.22rem 0;
    font-size: 0.75rem;
    line-height: 1.35;
    border-top: 1px solid var(--mm-border-soft);
}
.pay-item:first-child { border-top: 0; }

.pay-item-name { color: var(--mm-text-primary); flex-shrink: 0; }

/* Takes the slack and gives it back first: the consignment number is the least important
   thing on the line and the only part that may be clipped. */
.pay-item-meta {
    flex: 1;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: 0.7rem;
    color: var(--mm-text-muted);
}

/* Fixed width so the amounts form a column without a table to align them. */
.pay-item-amount {
    flex-shrink: 0;
    min-width: 5.5rem;
    text-align: right;
    font-weight: 700;
    color: var(--mm-text-primary);
}

/* Ink, not a fill: this is text on the sunken panel and has to lighten in dark mode. */
.pay-item-amount--negative { color: var(--mm-danger-ink); }

/* A row that opens its payee. The whole row is the target, so the pointer and the hover
   have to say so - a `role="button"` that does not look clickable is one nobody clicks. */
.pay-row { cursor: pointer; }
.pay-row:hover > td { background: var(--mm-surface-hover); }
/* `--mm-accent` and inset, matching `.mm-btn:focus-visible`. Not `--mm-focus-ring`, which
   is a low-alpha colour built for a box-shadow halo and nearly invisible as a solid line.
   The offset is negative so the ring sits inside the row rather than over its neighbour. */
.pay-row:focus-visible {
    outline: 2px solid var(--mm-accent);
    outline-offset: -2px;
}

/* A small caps heading inside the review dialog, marking off its three parts. */
.pay-review-label {
    font-size: 0.68rem;
    font-weight: 700;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--mm-text-muted);
    margin-bottom: 0.5rem;
}

/* What is wrong, at the top of the dialog and unmissable: it is why the dialog is open.
   Tint and ink as independent tokens - in dark mode the ground darkens while the text
   lightens, which one shared token could never do. */
.pay-review-blockers:not(:empty) {
    background: var(--mm-warning-tint);
    border-radius: 0.5rem;
    padding: 0.6rem 0.75rem;
    margin-bottom: 1.25rem;
}

.pay-review-blocker {
    display: flex;
    align-items: flex-start;
    gap: 0.4rem;
    font-size: 0.8rem;
    line-height: 1.45;
    color: var(--mm-warning-ink);
}

.pay-review-blocker + .pay-review-blocker { margin-top: 0.3rem; }
.pay-review-blocker .material-symbols-rounded { font-size: 17px; flex-shrink: 0; }

/* The summing line under an item list. Ruled off above and heavier, so the total reads as
   a total rather than as one more item. */
.pay-item--total {
    border-top-width: 2px;
    padding-top: 0.45rem;
    margin-top: 0.15rem;
    font-weight: 700;
}

/* A blocker, on a row that cannot be paid. Tint and ink as independent tokens so it stays
   legible when the theme flips: in dark mode the tint darkens and the ink lightens, which
   one shared token could never do. */
.pay-blocker {
    display: inline-block;
    padding: 0.1rem 0.45rem;
    margin: 0.1rem 0.2rem 0.1rem 0;
    border-radius: 0.35rem;
    font-size: 0.68rem;
    line-height: 1.5;
    white-space: normal;
    background: var(--mm-warning-tint);
    color: var(--mm-warning-ink);
}

/* The one blocker nobody can act on. Grey, so it does not sit in a list of things to fix
   wearing the same colour as the things that are. */
.pay-blocker--inert {
    background: var(--mm-surface-sunken);
    color: var(--mm-text-muted);
}

/* ============================================================================
   MOBILE LAYER

   Everything above this point was written for a desktop viewport and shipped to
   a phone unchanged. This is the small-screen half, kept in one block rather
   than tacked onto each primitive so the whole mobile contract reads in one
   sitting.

   Scoped to `max-width: 991.98px`: the exact complement of the
   `min-width: 992px` block above, and the breakpoint partners.css and
   ticket-modal.css already use. It covers a phone at 390px and an iPad in
   portrait at 768px.

   Desktop is untouched deliberately. The density above this block is the point
   of it, and CLAUDE.md's rule against per-page overrides is why this lands here
   rather than in a view.
   ============================================================================ */

@media (max-width: 991.98px) {

    /*
     * 1. Every text input at 16px.
     *
     * iOS Safari zooms the viewport when a focused input resolves under 16px
     * and does NOT zoom back, so one tap on a search box leaves the page wider
     * than the screen for the rest of the session. `shrink-to-fit=no` in the
     * viewport meta does not prevent it.
     *
     * Written against the ELEMENTS, not a list of classes. The list was tried
     * first and missed four families in production: `.set-input` (10 fields on
     * the email settings panel, which I had listed as `.setting-input`),
     * `.ev-input` (16 on the calendar forms), `.filter-select`, and every
     * control carrying `.text-xs` or `.text-sm`. A browser sweep of 110 routes
     * found 57 survivors. Enumerating classes cannot be made reliable here:
     * any page is free to invent another one.
     *
     * The `!important` is load bearing. It has to outrank three things that all
     * beat a plain class selector: the theme's
     * `.text-xs` / `.text-sm` utilities (which carry their own !important), page
     * local id rules such as `#searchInput`, and inline styles. A focusable
     * control under 16px on a phone is always a defect and never a design
     * choice, so there is nothing legitimate for this to override.
     *
     * The `:root` prefix is not decoration. When two !important declarations
     * collide, SPECIFICITY decides, and `.text-xs` is (0,1,0) while a bare
     * `select` is (0,0,1) - so the utility won and four selects stayed at 12px
     * even with !important. `:root select` is (0,1,1) and takes it. The `input`
     * selector already clears the bar on its own, because each `:not([type=...])`
     * contributes a class-level unit.
     *
     * Checkbox, radio, hidden, range, file and color are excluded: none of them
     * takes text, so none triggers the zoom, and several would be resized by it.
     */
    :root input:not([type="checkbox"]):not([type="radio"]):not([type="hidden"]):not([type="range"]):not([type="file"]):not([type="color"]),
    :root select,
    :root textarea {
        font-size: 1rem !important;
    }

    /*
     * 2. A 44px floor on anything you tap.
     *
     * Apple HIG and WCAG 2.5.5 both ask for 44px. Measured in Chrome against
     * this sheet, the primitives came in at 24.8px (.mm-action), 26.0px
     * (.mm-btn--sm), 28.0px (.mm-btn--icon), 30.1px (.mm-btn) and 37.0px
     * (.mm-field), and not one of them set a `min-height` - so there was no
     * floor to raise, only one to add.
     *
     * `min-height` rather than `height` on purpose: the used height is clamped
     * to max(min-height, min(height, max-height)), so this lifts the controls
     * that set an explicit height (.setting-input and .mm-search-input at 38px,
     * .mm-sidenav-search at 34px) without having to restate each one.
     *
     * .mm-field-inline and .mm-btn--field are in this list because their
     * heights are .mm-field's box arithmetic, spelled out as a calc() with a
     * comment saying so. All four move together or a button lands off the
     * centre line of the input beside it.
     */
    .mm-btn,
    .mm-action,
    .pn-card-action,
    .mm-pill,
    .mm-seg button,
    .mm-seg > a,
    .mm-sidenav-search,
    .an-pick-item,
    .mm-field,
    .mm-field--sm,
    .an-input,
    .setting-input,
    .mm-search-input,
    .search-field,
    .mm-field-inline,
    .mm-btn--field {
        min-height: 44px;
    }

    /*
     * .mm-notice is `display: flex`, which turns every inline element and every
     * run of text inside it into a SEPARATE flex item sharing one non-wrapping
     * line. 27 of the portal's 84 notices put bare prose inside one, and that
     * prose therefore cannot wrap as prose: /admin/settings/consignment-fees
     * measured 444px of content inside a 364px box, which is what was still
     * pushing that page sideways after the table was ruled out.
     *
     * `flow-root` rather than `block` because the icon is floated to keep the
     * text beside it, and flow-root contains the float without clipping. Both
     * shapes work under it: bare prose flows around the icon, and the notices
     * that wrap their text in a div flow beside it just as they did under flex.
     */
    .mm-notice {
        display: flow-root;
    }

    .mm-notice > i:first-child {
        float: left;
        margin-right: 0.55rem;
    }

    /*
     * Inline code is a single unbreakable token under the default rules, so one
     * long identifier drags the page sideways: /admin/settings/consignment-fees
     * measured 465px against 390 on the strength of one <code>.
     */
    code, kbd, samp { overflow-wrap: anywhere; }

    /*
     * An icon-only control has no label to widen it, so it needs the floor on
     * both axes. .mm-btn--icon is a hard 28x28 above, and min-width clamps a
     * fixed width the same way min-height clamps a fixed height.
     */
    .mm-btn--icon {
        min-width: 44px;
        min-height: 44px;
    }
}

/* ============================================================================
   HEAD ACTIONS

   The action cluster in a page header: the `col-auto` that sits opposite the
   title and carries a back link, an Actions menu, or both.

   On a desktop row it sits to the right of the title and needs nothing. On a
   phone the `col-auto` wraps underneath, and on a detail page that puts it
   directly below the record's meta line, where it reads as one more line of
   that meta rather than as the page's actions. Margin does not fix it, because
   the problem is the order rather than the gap.

   Lifted out of psa/orders/view.php on 2026-09-18, which is where the shape was
   worked out. Add the class to the header's `col-auto`; there is nothing to
   configure.
   ============================================================================ */

@media (max-width: 767.98px) {
    .mm-head-actions {
        /*
         * Above the title. `order` works because Bootstrap's .row is a flex
         * container, and keeping the title first in the MARKUP is what leaves
         * desktop untouched. The cost is that the visual order and the focus
         * order differ for these controls on a phone; reversing the markup
         * instead would move that problem onto desktop, where the header is the
         * more heavily used of the two.
         */
        order: -1;
        width: 100%;
        margin-bottom: 0.9rem;

        /*
         * Set here rather than assumed, because the consumers are not
         * consistent: some carry `d-flex align-items-center`, some are a bare
         * `col-auto`. Wrapping matters on the headers with three controls,
         * which at 44px tall do not fit one row of a 390px screen.
         */
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 0.5rem;

        /*
         * Back on the left, the menu on the right. With a single control it
         * sits left, which is what a lone back link wants.
         */
        justify-content: space-between;
    }

    /* .btn-group is inline-flex, so its toggle needs telling to fill the space
       the flex row has given it. */
    .mm-head-actions .btn-group { display: flex; }
}

/* ============================================================================
   HEAD META

   The line of facts under a page title: a consignor and their contact details,
   or a service plus a created date plus a couple of status badges. Written as
   inline runs separated by middots, which is right on a desktop row and wrong
   on a phone, where it wraps mid-run and leaves a separator stranded at the
   start of the next line.

   Stacked one fact per line on a phone, separators dropped. Mark each separator
   with `.mm-head-meta-sep` so it can be taken out; a bare text run between two
   spans becomes an anonymous flex item and gets its own line for free.

   Lifted out of psa/orders/view.php on 2026-09-18 when the submission view
   needed the same shape.
   ============================================================================ */

@media (max-width: 767.98px) {
    .mm-head-meta {
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        gap: 0.15rem;
    }

    .mm-head-meta-sep { display: none; }

    /* An email address is the run that would otherwise push the header sideways:
       it offers no break opportunity under the default rules. */
    .mm-head-meta > * { overflow-wrap: anywhere; }
}

/* ============================================================================
   HEAD FILTERS

   The pill row in a list-page header: the `col-auto` opposite the title that
   carries Retail / Preorders / channel / delivery / range / sort. Picking has
   six of them, packing three, and both are styled by the same hand-rolled pill
   rule in their own views.

   Every pill is `white-space: nowrap` and the row does not wrap, so on picking
   the six measured 722px against a 390px screen. Nothing in the chain to
   <body> sets `overflow`, so that dragged the whole document sideways by 332px,
   taking the navbar and every card with it.

   They WRAP rather than scroll, and that is the deliberate half of this. Their
   labels are the filter state - "All methods", "Last 7 days", "Oldest first" -
   so a strip that puts half of them off-screen hides what the queue is
   currently showing. A chip strip of counts can scroll; a row of live filter
   state has to be readable at a glance.
   ============================================================================ */

@media (max-width: 767.98px) {
    .mm-head-filters {
        width: 100%;
        flex-wrap: wrap;
    }
}
