/* Cruze — first-party stylesheet (no framework, no build step).
   Organized as: tokens -> reset -> layout/grid -> components -> utilities.
   Class names intentionally match what templates already use, so this file
   is the single place that controls the whole app's look. */

/* ---------- Tokens ---------- */
/* Palette per the client's B2C CRM review ("Brand Color System"): Cruze
   Deep Blue for structure/actions, Lubricant Gold for brand accents, and
   Charcoal Slate for text/nav chrome. The four status colors (success/
   warning/danger/neutral) are the review's exact hex values, including its
   choice to reuse Lubricant Gold as the Warning color — accent and warning
   are still two separate tokens, they just happen to share one value by
   the client's own design. -hover/-light shades for primary/accent are
   mechanically derived (~15% darker / ~90% toward white) since the review
   only specifies one shade of each. */
:root {
  --color-primary: #1e3a8a;
  --color-primary-hover: #16295e;
  --color-primary-light: #e9edf7;

  --color-accent: #d97706;
  --color-accent-hover: #b96505;
  --color-accent-light: #fbf1e6;

  --color-success: #16a34a;
  --color-success-bg: #dcfce7;
  --color-success-text: #166534;

  --color-danger: #dc2626;
  --color-danger-bg: #fee2e2;
  --color-danger-text: #991b1b;

  --color-warning: #d97706;
  --color-warning-bg: #fef3c7;
  --color-warning-text: #92400e;

  --color-info: #0891b2;
  --color-info-bg: #cffafe;
  --color-info-text: #155e75;

  --color-secondary-bg: #f1f5f9;
  --color-secondary-text: #64748b;

  /* Order status "Returned"/"Partially returned" badges only (client ask:
     black and dark gray specifically, not one of the tinted-pastel tokens
     above) -- see templates/orders/_status_badge.html. */
  --color-black-bg: #1f2937;
  --color-black-text: #ffffff;
  --color-dark-gray-bg: #6b7280;
  --color-dark-gray-text: #ffffff;

  --color-text: #1e293b;
  --color-text-muted: #5c6b82;
  --color-border: #dde2ea;
  --color-surface: #ffffff;
  --color-bg: #f8fafc;
  --color-nav-bg: #1e293b;

  --radius-sm: 0.375rem;
  --radius-md: 0.5rem;
  --radius-lg: 1rem;
  --shadow-sm: 0 1px 2px rgba(20, 33, 59, 0.06);
  --shadow-md: 0 4px 12px rgba(20, 33, 59, 0.08);
  --shadow-card: 0 1px 2px rgba(30, 41, 59, 0.04), 0 10px 26px -10px rgba(30, 58, 138, 0.14);

  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /* Custom <select> chevron -- used by .field-input select (every list
     view's/the dashboard's filter toolbar). */
  --chevron-icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235c6b82' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
}

/* ---------- Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
body {
  margin: 0;
  font-family: var(--font-sans);
  font-size: 0.9375rem;
  color: var(--color-text);
  background: var(--color-bg);
  line-height: 1.5;
}
h1, h2, h3, h4, h5, h6 { margin: 0; font-weight: 600; line-height: 1.25; }
a { color: var(--color-primary); text-decoration: none; }
a:hover { text-decoration: underline; }
table { border-collapse: collapse; width: 100%; }
/* Real bug (Playwright-verified): a <button>'s UA stylesheet resets
   line-height to "normal" as part of its implicit `font` shorthand --
   font-family alone was never enough. Every button matched every other
   button anyway (all "normal"), so this stayed invisible until a <button>
   (Export to Excel/PDF) sat directly next to an <a>-based .btn (New Order)
   inheriting the real line-height: 1.5, and the two visibly differed in
   height at the same padding/font-size. */
button { font-family: inherit; line-height: inherit; }
:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }

/* ---------- Layout: topbar + persistent sidebar ---------- */
/* Client review: "Replace or complement the hamburger menu with a
   collapsible persistent sidebar." A slim topbar (brand, global search,
   user/logout) sits above an app-body flex row of sidebar + main content,
   rather than the old single horizontal navbar. Light chrome (white topbar
   + white sidebar with a grouped, icon-led nav) per the dashboard.html
   design — a deliberate reversal of the original dark Charcoal Slate nav
   chrome; --color-nav-bg is unused now but left defined in case a future
   pass wants it back for e.g. a dark-mode toggle. */
.topbar {
  display: flex;
  align-items: center;
  gap: 1rem;
  min-height: 64px;
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
  padding: 0.65rem 1.25rem;
  box-shadow: var(--shadow-sm);
  position: sticky;
  top: 0;
  /* Real bug (Playwright-verified): the off-canvas mobile sidebar below is
     a fixed panel starting at top:0 with z-index 50, so it was rendering
     ON TOP of the topbar once open -- including the hamburger button
     itself, silently swallowing the very click meant to close it again.
     The topbar has to out-rank the sidebar (and its backdrop) so the
     toggle stays clickable no matter what's open underneath it. */
  z-index: 60;
}
.sidebar-toggle {
  flex-shrink: 0;
  background: transparent;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-text);
  padding: 0.4rem 0.6rem;
  cursor: pointer;
}
.sidebar-toggle:hover { background: var(--color-bg); }
.navbar-brand { display: inline-flex; align-items: center; font-size: 1.125rem; font-weight: 700; flex-shrink: 0; }
.brand-logo { display: inline-flex; align-items: center; }
.brand-logo img { display: block; height: 30px; width: auto; }
.navbar-text { color: var(--color-text); font-size: 0.875rem; white-space: nowrap; }

.app-body { display: flex; min-height: calc(100vh - 64px); }

.sidebar {
  width: 220px;
  flex-shrink: 0;
  background: var(--color-surface);
  border-right: 1px solid var(--color-border);
  /* Deliberately NOT position:sticky/max-height/align-self here — this box
     stretches to match main's full height (the flex default), so its
     background spans the whole page length regardless of content length.
     The nav *links* stick to the viewport while scrolling via
     .sidebar-nav below; the background box itself just needs to be tall. */
  transition: width 0.15s ease;
}
.sidebar-nav {
  margin: 0;
  padding-bottom: 0.75rem;
  position: sticky;
  /* .sidebar itself already starts right below the topbar in normal flow
     (it's a sibling of .topbar's own space, not stacked under it) -- top:0
     is "stick right where it naturally sits." A real bug, caught via
     Playwright: this used to be top: 64px (meant as "topbar height"
     clearance) with overflow:hidden on .sidebar (below) -- but overflow
     other than visible on an ancestor makes IT the sticky containing
     block/scrollport instead of the real viewport, per spec. That broke
     two things at once: the resting position rendered 64px too low (double
     counting clearance .sidebar already had), and -- worse -- actual
     sticky-while-scrolling stopped working at all (confirmed: the nav
     scrolled away with the page instead of sticking, scrollTo(0, 2000)
     left it far off-screen). Moving overflow:hidden onto this element
     itself (below) instead of the .sidebar ancestor keeps the width:0
     collapse-clipping fix intact (it clips its own overflowing children
     the same way) while no longer breaking its own sticky positioning.
     Never put overflow:hidden back on .sidebar without re-checking this. */
  top: 0;
  /* Real bug: collapsing .sidebar to width:0 (see width:0 rule below)
     shrinks this element the same way (a plain block child), but
     .nav-item's white-space:nowrap gives every link its own intrinsic
     width wider than 0 -- with no overflow clipping here, the whole nav
     list spilled out past the collapsed container and sat there fully
     visible and still clickable instead of actually disappearing. */
  overflow: hidden;
}
.nav-section-label {
  font-size: 0.65625rem;
  font-weight: 700;
  letter-spacing: 0.07em;
  color: var(--color-secondary-text);
  text-transform: uppercase;
  padding: 1.1rem 1.25rem 0.5rem;
}
.nav-item {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  padding: 0.6rem 1.25rem;
  color: var(--color-text-muted);
  font-size: 0.84375rem;
  font-weight: 500;
  white-space: nowrap;
}
.nav-item svg { flex-shrink: 0; }
.nav-item:hover { color: var(--color-text); background: var(--color-bg); text-decoration: none; }
.nav-item.active { color: var(--color-text); font-weight: 700; background: var(--color-accent-light); box-shadow: inset 3px 0 0 var(--color-accent); }

/* Desktop collapse (review: "collapsible to preserve screen space") -- the
   sidebar itself shrinks to nothing rather than becoming an icon rail,
   since this app has no icon set to fall back to. */
.app-body.sidebar-collapsed .sidebar { width: 0; }
.sidebar-backdrop { display: none; }

/* Global search -- htmx-driven (see base.html), first real use of the
   vendored htmx.min.js in this codebase. */
.global-search { position: relative; flex: 1; max-width: 420px; min-width: 160px; }
.global-search-input {
  width: 100%;
  padding: 0.45rem 0.75rem;
  border-radius: var(--radius-sm);
  border: 1px solid var(--color-border);
  background: var(--color-secondary-bg);
  color: var(--color-text);
  font-size: 0.875rem;
}
.global-search-input::placeholder { color: var(--color-text-muted); }
.global-search-input:focus { outline: none; background: var(--color-surface); border-color: var(--color-primary); box-shadow: 0 0 0 3px var(--color-primary-light); }
.global-search-results {
  position: absolute;
  top: calc(100% + 0.35rem);
  left: 0;
  right: 0;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  z-index: 40;
  max-height: 320px;
  overflow-y: auto;
}
.global-search-results:empty { display: none; }
.search-result { display: flex; align-items: center; gap: 0.5rem; }
.search-result-label { font-weight: 600; }

.container { width: 100%; max-width: 1320px; margin: 0 auto; padding: 0 1.5rem; }
main.container { padding-top: 1.5rem; padding-bottom: 3rem; flex: 1; min-width: 0; }

/* Grid (only the subset of columns actually used across templates) */
.row { display: flex; flex-wrap: wrap; margin: 0 -0.5rem; }
.row > [class*="col"] { padding: 0 0.5rem; }
.row.g-2 { margin: 0 -0.375rem; row-gap: 0.5rem; }
.row.g-2 > [class*="col"] { padding: 0 0.375rem; }
.col-auto { flex: 0 0 auto; width: auto; }
.col-sm-3 { flex: 0 0 25%; max-width: 25%; }
.col-sm-6 { flex: 0 0 50%; max-width: 50%; }
.col-sm-9 { flex: 0 0 75%; max-width: 75%; }
@media (max-width: 640px) {
  .col-sm-3, .col-sm-6, .col-sm-9 { flex: 0 0 100%; max-width: 100%; }
}
.col-md-4 { flex: 0 0 33.3333%; max-width: 33.3333%; }
.col-md-5 { flex: 0 0 41.6667%; max-width: 41.6667%; }
.col-md-7 { flex: 0 0 58.3333%; max-width: 58.3333%; }
.col-md-8 { flex: 0 0 66.6667%; max-width: 66.6667%; }
@media (max-width: 768px) {
  .col-md-4, .col-md-5, .col-md-7, .col-md-8 { flex: 0 0 100%; max-width: 100%; }
}

/* dl/dt/dd detail rows (customer/order detail pages) */
dl.row { margin-bottom: 0; }
/* min-height (border-box, per the global reset above, so it includes this
   row's own padding rather than stacking on top of it) -- without this, a
   row whose dd holds a .badge (shorter, its own tighter padding) ends up a
   couple pixels shorter than an ordinary text row, and that small per-row
   difference compounds down the table. Harmless everywhere there's only
   ever one such dl on a page, but order_detail.html puts two independent
   dl.row's side by side (order info / customer info) -- their rows are
   expected to line up visually across the gap, and that drift was real
   and visible there (a badge in the order table's very first row,
   "Status"). Pinning every row to the same minimum height fixes it for
   any current or future two-dl layout, not just this one. Deliberately no
   flex/align-items here -- dt/dd stay plain blocks, top-aligned exactly
   as before (some dd's render multi-line content via <br>, e.g. Notes'
   |linebreaksbr and the two-address customer row below; flex would turn
   each text run/<br> into its own flex item and silently break that
   line-breaking). */
dl.row dt, dl.row dd { padding: 0.4rem 0.5rem; border-bottom: 1px solid var(--color-border); min-height: 2.25rem; }
dl.row dt { color: var(--color-text-muted); font-weight: 500; }
dl.row dd { margin: 0; }

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 0.5rem 1rem;
  font-size: 0.875rem;
  font-weight: 500;
  cursor: pointer;
  background: transparent;
  color: var(--color-text);
  transition: background-color 0.12s ease, border-color 0.12s ease, color 0.12s ease;
}
.btn:hover { text-decoration: none; }
.btn:disabled { opacity: 0.6; cursor: not-allowed; }
.btn-sm { padding: 0.3rem 0.65rem; font-size: 0.8125rem; }
.btn-primary { background: var(--color-primary); border-color: var(--color-primary); color: #fff; }
.btn-primary:hover { background: var(--color-primary-hover); border-color: var(--color-primary-hover); }
.btn-outline-primary { border-color: var(--color-primary); color: var(--color-primary); }
.btn-outline-primary:hover { background: var(--color-primary-light); }
.btn-outline-secondary { border-color: var(--color-border); color: var(--color-text); }
.btn-outline-secondary:hover { background: var(--color-bg); }
.btn-outline-danger { border-color: var(--color-danger); color: var(--color-danger); }
.btn-outline-danger:hover { background: var(--color-danger-bg); }
.btn-danger { background: var(--color-danger); border-color: var(--color-danger); color: #fff; }
.btn-danger:hover { background: #b91c1c; border-color: #b91c1c; }
.btn-outline-light { border-color: rgba(255, 255, 255, 0.5); color: #fff; }
.btn-outline-light:hover { background: rgba(255, 255, 255, 0.12); }
/* Export buttons (orders.html design): tinted, not just outlined, so
   Excel/PDF read as distinct at a glance rather than two identical gray
   buttons -- same success/danger tokens every badge/alert already uses,
   not new one-off colors. */
.btn-excel { background: var(--color-success-bg); border-color: var(--color-success); color: var(--color-success-text); }
.btn-excel:hover { background: #bbf7d0; }
.btn-pdf { background: var(--color-danger-bg); border-color: var(--color-danger); color: var(--color-danger-text); }
.btn-pdf:hover { background: #fecaca; }
/* Solid-fill, same idiom as .btn-primary (not the lighter tinted-outline
   style Export uses) -- accent (gold) for tag-related actions, info (teal)
   for report/reference pages, both existing tokens already used elsewhere
   for those meanings (accent = Promotions' sidebar icon color; info =
   Confirmed order badges). */
.btn-tag { background: var(--color-accent); border-color: var(--color-accent); color: #fff; }
.btn-tag:hover { background: var(--color-accent-hover); border-color: var(--color-accent-hover); }
.btn-report { background: var(--color-info); border-color: var(--color-info); color: #fff; }
.btn-report:hover { background: #0e7490; border-color: #0e7490; }
/* Solid-fill, same idiom as .btn-tag/.btn-report -- neutral secondary token
   for "back to X" navigation buttons, since that action isn't tag/report/
   primary-create but still reads as a deliberate button, not a plain link. */
.btn-back { background: var(--color-secondary-text); border-color: var(--color-secondary-text); color: #fff; }
.btn-back:hover { background: #475569; border-color: #475569; }
/* Order-status-transition buttons (order_detail.html's Confirm/Mark out for
   delivery/Mark fulfilled) -- same solid-fill idiom as .btn-tag/.btn-report/
   .btn-back, but each one's color previews the status badge
   (orders/_status_badge.html) pressing it leads to, rather than all three
   reading as identical generic .btn-primary actions: Confirm -> the
   Confirmed badge's info/teal, Mark out for delivery -> the Out for
   delivery badge's warning/gold, Mark fulfilled -> the Fulfilled badge's
   success/green. */
.btn-confirm { background: var(--color-info); border-color: var(--color-info); color: #fff; }
.btn-confirm:hover { background: #0e7490; border-color: #0e7490; }
.btn-dispatch { background: var(--color-warning); border-color: var(--color-warning); color: #fff; }
.btn-dispatch:hover { background: var(--color-accent-hover); border-color: var(--color-accent-hover); }
.btn-fulfill { background: var(--color-success); border-color: var(--color-success); color: #fff; }
.btn-fulfill:hover { background: #15803d; border-color: #15803d; }
/* Same "previews the status badge it leads to" idiom as .btn-confirm/
   .btn-dispatch/.btn-fulfill above, for the one transition that goes
   backwards -- Deconfirm returns a Confirmed order to Draft, so it reuses
   the Draft badge's own gray/secondary-text token rather than any of the
   "forward progress" colors those three use. */
.btn-deconfirm { background: var(--color-secondary-text); border-color: var(--color-secondary-text); color: #fff; }
.btn-deconfirm:hover { background: #475569; border-color: #475569; }
/* Solid-fill, same idiom as .btn-tag/.btn-report/.btn-back/.btn-confirm/
   .btn-dispatch/.btn-fulfill -- the generic "undo a deactivation" action,
   reused for every DeactivatableModel's Reactivate button (Supplier,
   Customer, CustomerTag, CustomerType, Employee, Product, Promotion), not
   tied to one specific status transition the way .btn-fulfill is. */
.btn-success { background: var(--color-success); border-color: var(--color-success); color: #fff; }
.btn-success:hover { background: #15803d; border-color: #15803d; }
/* Inventory detail page's action row (Receive stock/Count stock/Adjust
   stock/Set low-stock alert) -- same solid-fill idiom as every button
   above, used to all be plain .btn-outline-primary/-secondary with no
   icon. Receive stock reuses .btn-success (incoming stock, same "positive"
   green as Reactivate/Mark fulfilled) and Count stock reuses .btn-report
   (a stock count is a reconciliation/reference action) rather than
   inventing new classes for those two. Adjust stock (a manual correction,
   warrants a "use with care" warning/gold) and Set low-stock alert (a
   config/threshold action, neutral) are genuinely new action types, so
   they get their own names below. */
.btn-adjust { background: var(--color-warning); border-color: var(--color-warning); color: #fff; }
.btn-adjust:hover { background: #b45309; border-color: #b45309; }
.btn-settings { background: var(--color-secondary-text); border-color: var(--color-secondary-text); color: #fff; }
.btn-settings:hover { background: #475569; border-color: #475569; }
.d-inline { display: inline; }
.d-inline form, form.d-inline { display: inline-block; }

/* ---------- Cards ---------- */
.card {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-card);
}
.card-body { padding: 1.25rem; }
.card-header {
  padding: 0.85rem 1.25rem;
  border-bottom: 1px solid var(--color-border);
  font-weight: 600;
  font-size: 0.9375rem;
}
.card-header .h6 { margin: 0; }
.stat-card { border-top: 3px solid var(--color-accent); }
.stat-card .fs-3 { color: var(--color-primary-hover); }
/* A row of stat-cards (dashboard KPI tiles) — CSS grid instead of the
   fixed .col-sm-N / .col-md-N set above, since the number of tiles varies by
   page. Fixed at 3 per row (rather than auto-fit) so tiles stay a sensible
   width instead of stretching to fill the row when there are only one or
   two left over; narrower screens step down via the media queries below. */
/* minmax(0, 1fr), not a bare 1fr -- a grid track's automatic minimum size
   defaults to its content's min-content size (not 0) unless overridden, so
   a wide tile (e.g. Top-Selling Product's nowrap/ellipsis value line, or a
   long translated label) silently floors every 1fr column at that content
   width instead of letting the grid shrink to fit. Without minmax(0, ...),
   this makes the whole grid overflow its container as the viewport
   narrows -- tiles never reflow/wrap, they just get pushed off-screen --
   right up until a media query below finally drops the column count.
   min-width: 0 alone on .stat-grid > .card is not enough on its own: the
   child that actually needs to shrink is the grid item occupying the
   track, and Grid's own automatic-minimum-size rule looks at the item's
   content regardless of a min-width set on some ancestor further in --
   minmax() on the track itself is what actually caps it. Real,
   screenshot-reported bug, not a hypothetical -- confirm both this and the
   media-query breakpoints below still look right after any future change
   to a tile's own content. */
.stat-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 1rem; }
.stat-grid > .card { margin: 0; min-width: 0; }
@media (max-width: 768px) {
  .stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 640px) {
  .stat-grid { grid-template-columns: minmax(0, 1fr); }
}
.h-100 { height: 100%; }
.d-flex-col-center { display: flex; flex-direction: column; }
.d-flex-col-center .card-body { flex: 1; display: flex; align-items: center; justify-content: center; }

/* ---------- Tables ---------- */
.table {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
}
.table thead th {
  background: var(--color-bg);
  color: var(--color-text-muted);
  text-transform: uppercase;
  font-size: 0.6875rem;
  letter-spacing: 0.04em;
  font-weight: 600;
  text-align: left;
  padding: 0.65rem 0.85rem;
  border-bottom: 1px solid var(--color-border);
}
.table tbody td, .table tfoot th {
  padding: 0.65rem 0.85rem;
  border-bottom: 1px solid var(--color-border);
  vertical-align: middle;
}
.table tbody tr:last-child td { border-bottom: none; }
.table-sm thead th, .table-sm tbody td, .table-sm tfoot th { padding: 0.45rem 0.7rem; }
.table-hover tbody tr:hover { background: var(--color-bg); }
.table-flush { border: none; border-radius: 0; box-shadow: none; }
.table-flush thead th { background: transparent; }
.table tfoot th { font-weight: 600; }
.text-end { text-align: right; }
/* Real bug: .table thead th's own text-align: left (above) has higher
   specificity (.table + thead + th) than the plain single-class .text-end,
   so a right-aligned header (e.g. a Total column) silently stayed left-
   aligned despite the class -- only ever noticeable on a header whose text
   didn't already look at-home on the left. Matches .text-end's specificity
   back up for the header case specifically, rather than reaching for
   !important. */
.table thead th.text-end { text-align: right; }

/* Sortable column headers (core.sorting.SortableListMixin, {% sortable_header %}) --
   the <a> fills its <th> and inherits the header's own uppercase/muted
   styling rather than looking like a normal blue underlined link; the
   little up/down arrow sits inline after the label, muted at rest and
   accent-colored once that column is the active sort. */
.sortable-header {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}
.sortable-header:hover { color: var(--color-primary); }
.sortable-header .sort-arrow { display: inline-flex; color: var(--color-text-muted); opacity: 0.6; }
.sortable-header:hover .sort-arrow { opacity: 1; }
.sortable-header.active { color: var(--color-primary); }
.sortable-header.active .sort-arrow { color: var(--color-primary); opacity: 1; }

/* Table isn't a real border-radius container by default because <table> can't
   clip its own border the way a div can in every browser reliably; wrap via
   the border on the element itself instead of a container. */

/* ---------- Formset "ghost row" add-button ---------- */
/* Replaces a plain "+ Add row" text button below a formset table (promotion
   requirement/reward-item rows) with a blurred preview of what the next row
   will look like, plus a big solid button overlaid on top of it -- reads as
   "click to reveal a new row" rather than a disconnected utility link.
   The outer wrapper supplies the one shared bordered box; both inner
   <table>s go .table-flush so there's no double border. The ghost row's own
   <table> holds exactly one disabled preview row, and its wrapping div is
   sized to that single row -- so a plain `inset: 0` centers the button
   exactly over it with no JS measurement needed. Reusable for any future
   formset that needs the same "add another row" affordance. */
.formset-table {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  overflow: hidden;
  box-shadow: var(--shadow-sm);
}
.formset-table .table { margin: 0; }
.formset-ghost-row { position: relative; border-top: 1px solid var(--color-border); }
.formset-ghost-row .table {
  filter: blur(3px);
  opacity: 0.6;
  pointer-events: none;
  user-select: none;
}
.formset-ghost-btn {
  position: absolute;
  inset: 0;
  margin: auto;
  width: fit-content;
  height: fit-content;
  padding: 0.6rem 1.5rem;
  font-size: 0.9375rem;
  box-shadow: var(--shadow-md);
}

/* ---------- Forms ----------
   Most forms in this app are plain ModelForms with no widget classes set,
   so they render bare <input>/<select>/<textarea> tags with no .form-control
   class to hook into. Styling the raw elements (not just the classed ones)
   means every form gets consistent styling with zero changes to Python/forms
   or template markup. */
.form-label { display: block; font-weight: 500; font-size: 0.8125rem; margin-bottom: 0.3rem; }
/* Required-field asterisk, app-wide, with zero per-template changes: Django
   already renders the `required` HTML attribute on every required field's
   widget (ModelForm required=True -> Widget.use_required_attribute()), so a
   relational selector on that attribute is enough -- no Python/template
   changes needed in any of this app's many form templates, now or later.
   `~` (general sibling), not `+` (adjacent), since not every form wraps its
   label immediately against the field with nothing else between them. */
label:has(~ input[required])::after,
label:has(~ select[required])::after,
label:has(~ textarea[required])::after,
/* core.widgets.TableCheckboxSelectMultiple's mini table never gets a real
   [required] attribute (CheckboxSelectMultiple always omits it -- browser
   validation would otherwise demand every checkbox be checked, not just
   one), so its wrapper carries data-required as this rule's stand-in --
   see templates/core/widgets/table_checkbox_select.html. */
label:has(~ .multiselect-table-wrap[data-required])::after {
  content: " *";
  color: var(--color-danger);
}
.form-control, .form-select,
input[type="text"], input[type="email"], input[type="tel"], input[type="url"],
input[type="number"], input[type="password"], input[type="search"], input[type="date"],
select, textarea {
  display: block;
  width: 100%;
  max-width: 480px;
  padding: 0.5rem 0.7rem;
  font-family: inherit;
  /* Same UA-default line-height reset as the bare `button` rule above --
     fixed proactively here too since a <select> now sits inline with plain
     text/icons inside .field-input's flex row (Orders' filter toolbar). */
  line-height: inherit;
  font-size: 0.875rem;
  color: var(--color-text);
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
}
textarea { max-width: 640px; min-height: 6rem; }
.form-control:focus, .form-select:focus,
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--color-primary);
  box-shadow: 0 0 0 3px var(--color-primary-light);
}
.form-control-sm, .form-select-sm { padding: 0.35rem 0.6rem; font-size: 0.8125rem; max-width: none; }
.form-text { color: var(--color-text-muted); font-size: 0.8125rem; margin-top: 0.25rem; }
.field-errors, .text-danger.small { color: var(--color-danger); font-size: 0.8125rem; }
input[type="date"].form-control { line-height: 1.4; }
input[type="checkbox"], input[type="radio"] { width: auto; max-width: none; }

/* core.widgets.TableCheckboxSelectMultiple — used for every genuinely
   multi-valued form field (Promotion.products/customers/customer_types,
   Customer.tags) in place of Django's default <select multiple> listbox
   (an unobvious Ctrl/Cmd+click to pick more than one) or a plain scrolling
   checkbox list (client ask: "a mini table with search and an X clear all
   and a checkmark in the table header for select all"). See
   templates/core/widgets/table_checkbox_select.html and
   Cruze.setCheckboxGroup()/filterMultiselectTable() in base.html. */
.multiselect-table-wrap {
  max-width: 480px;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  background: var(--color-surface);
  overflow: hidden;
}
.multiselect-table-toolbar { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem; border-bottom: 1px solid var(--color-border); }
.multiselect-table-toolbar .multiselect-select-all-toggle { flex-shrink: 0; }
.multiselect-search { flex: 1; margin: 0; }
.multiselect-clear-btn {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2rem; height: 2rem; flex-shrink: 0;
  border: 1px solid var(--color-border); border-radius: var(--radius-sm);
  background: var(--color-bg); color: var(--color-text-muted);
  font-size: 0.75rem; line-height: 1; cursor: pointer;
}
.multiselect-clear-btn:hover { border-color: var(--color-danger); background: var(--color-danger-bg); color: var(--color-danger); }
.multiselect-table-scroll { max-height: 12rem; overflow-y: auto; }
.multiselect-table { width: 100%; border-collapse: collapse; margin: 0; }
.multiselect-table tbody td { padding: 0.3rem 0.6rem; border-bottom: 1px solid var(--color-border); }
.multiselect-table tbody tr:last-child td { border-bottom: none; }
.multiselect-table tbody tr:hover td { background: var(--color-bg); }
.multiselect-check-col { width: 2rem; }
.multiselect-table label { font-weight: 400; margin: 0; cursor: pointer; display: block; }
.multiselect-no-matches { text-align: center; padding: 0.75rem; margin: 0; font-size: 0.875rem; }

/* ---------- Combobox (searchable single-select) ---------- */
/* Turns a plain product <select> (promotion requirement/reward-item rows)
   into one field that's both the search box and the dropdown -- see
   enhanceProductCombobox() in templates/promotions/promotion_form.html.
   The real <select> is hidden (still submitted with the form; hiding via
   `display:none` doesn't stop that, only `disabled` would) and a text input
   sits in its place, with the filtered option list opening below it. */
.combobox { position: relative; }
.combobox-select { display: none; }
/* position: fixed + appended straight to <body> (see enhanceCombobox() in
   base.html), not nested inside .combobox -- a plain position:absolute
   listbox here got clipped by any ancestor with overflow:hidden (e.g.
   .formset-table, a real, screenshot-caught bug), since overflow clipping
   applies to positioned descendants regardless of position:fixed/absolute
   unless the element is moved out of that ancestor's DOM subtree entirely.
   top/left/width are set in JS from the input's own getBoundingClientRect()
   on every open/scroll/resize. z-index 100 matches .row-actions-menu, the
   only other floating-above-everything element in the app. */
.combobox-listbox {
  position: fixed;
  z-index: 100;
  max-height: 12rem;
  overflow-y: auto;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
}
.combobox-option { padding: 0.4rem 0.7rem; font-size: 0.8125rem; cursor: pointer; }
.combobox-option:hover, .combobox-option.active { background: var(--color-primary-light); }
.combobox-no-matches { padding: 0.5rem 0.7rem; font-size: 0.8125rem; color: var(--color-text-muted); }

/* ---------- Filter toolbar fields (orders.html design) ---------- */
/* A labeled, icon-led input/select "field" -- the box itself carries the
   border/background, so the real control nested inside is stripped down
   to a bare, transparent input (overriding the generic input/select rules
   above; same specificity, but declared later so it wins on cascade order,
   same idea as .form-control-sm overriding .form-control near line 300). */
.field-label { font-size: 0.65625rem; font-weight: 700; color: var(--color-secondary-text); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 0.3rem; }
.field-input {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  background: var(--color-bg);
  border: 1px solid var(--color-border);
  border-radius: 0.5625rem;
  padding: 0.5rem 0.75rem;
}
.field-input:focus-within { border-color: var(--color-primary); box-shadow: 0 0 0 3px var(--color-primary-light); }
.field-input svg { flex-shrink: 0; }
.field-input input, .field-input select {
  border: none;
  background: transparent;
  padding: 0;
  margin: 0;
  max-width: none;
  width: 100%;
  font-size: 0.8125rem;
  color: var(--color-text);
}
.field-input input:focus, .field-input select:focus { outline: none; box-shadow: none; }
.field-input select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 1.1rem;
  background-image: var(--chevron-icon);
  background-repeat: no-repeat;
  background-position: right center;
  background-size: 12px;
}

/* Clear-filters button: always rendered as a fixed slot at the end of the
   toolbar row (never conditionally added/removed), so toggling a filter
   never reflows the labeled fields next to it -- grayed out and inert at
   rest, only turning red/clickable once a filter is actually active. A
   `title` attribute supplies the "Clear filters" tooltip since there's no
   visible label at rest. */
.clear-filters-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.5rem;
  height: 2.5rem;
  flex-shrink: 0;
  border-radius: 0.5625rem;
  border: 1px solid var(--color-border);
  background: var(--color-bg);
  color: var(--color-text-muted);
  cursor: default;
}
.clear-filters-btn.active { border-color: var(--color-danger); background: var(--color-danger-bg); color: var(--color-danger); cursor: pointer; }
.clear-filters-btn.active:hover { background: #fecaca; text-decoration: none; }

/* Compact icon-only danger button -- a solid red square with a white X,
   no visible label (a `title` attribute supplies the tooltip). Client ask:
   order_detail.html's "Remove product line" action should read as this
   instead of a full text .btn-outline-danger button. */
.btn-icon-danger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.75rem;
  height: 1.75rem;
  flex-shrink: 0;
  border-radius: var(--radius-sm);
  border: none;
  background: var(--color-danger);
  color: #fff;
  cursor: pointer;
}
.btn-icon-danger:hover { background: #b91c1c; }

/* Set by the base.html 'invalid' listener when the browser's own HTML5
   constraint validation rejects a field (missing required value, wrong
   type/format — e.g. a date field's native picker only ever submits a real
   date or empty, so this mainly catches "left blank"/out-of-range cases).
   Gives a visible cue beyond the browser's own tooltip; cleared automatically
   once the field's value passes validation again. */
.is-invalid, .is-invalid:focus {
  border-color: var(--color-danger);
  box-shadow: 0 0 0 3px var(--color-danger-bg);
}

/* ---------- Badges ---------- */
.badge {
  display: inline-flex;
  align-items: center;
  padding: 0.25rem 0.55rem;
  border-radius: 999px;
  font-size: 0.75rem;
  font-weight: 600;
}
.text-bg-primary { background: var(--color-primary-light); color: var(--color-primary-hover); }
.text-bg-success { background: var(--color-success-bg); color: var(--color-success-text); }
.text-bg-danger { background: var(--color-danger-bg); color: var(--color-danger-text); }
.text-bg-warning { background: var(--color-warning-bg); color: var(--color-warning-text); }
.text-bg-info { background: var(--color-info-bg); color: var(--color-info-text); }
.text-bg-secondary { background: var(--color-secondary-bg); color: var(--color-secondary-text); }
.text-bg-black { background: var(--color-black-bg); color: var(--color-black-text); }
.text-bg-dark-gray { background: var(--color-dark-gray-bg); color: var(--color-dark-gray-text); }

/* ---------- Alerts ---------- */
.alert { padding: 0.75rem 1rem; border-radius: var(--radius-sm); border: 1px solid transparent; margin-bottom: 1rem; font-size: 0.875rem; }
.alert-success { background: var(--color-success-bg); border-color: #bbf7d0; color: var(--color-success-text); }
.alert-danger, .alert-error { background: var(--color-danger-bg); border-color: #fecaca; color: var(--color-danger-text); }
.alert-warning { background: var(--color-warning-bg); border-color: #fde68a; color: var(--color-warning-text); }
.alert-info, .alert-debug { background: var(--color-info-bg); border-color: #a5f3fc; color: var(--color-info-text); }

/* ---------- Toasts (flash messages) ---------- */
/* Floating, auto-dismissing -- same .alert/.alert-* coloring as a plain
   inline banner, just repositioned/stacked/animated instead of sitting in
   the document flow at the top of <main>. z-index 70 clears both .topbar
   (60) and the mobile off-canvas sidebar/backdrop (50/40). */
.toast-container {
  position: fixed;
  top: 76px;
  right: 1.25rem;
  z-index: 70;
  display: flex;
  flex-direction: column;
  gap: 0.625rem;
  width: min(360px, calc(100vw - 2.5rem));
}
.toast {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.75rem;
  margin-bottom: 0;
  box-shadow: var(--shadow-md);
  animation: toast-in 0.2s ease-out;
}
.toast-message { flex: 1; }
.toast-close {
  background: none;
  border: none;
  padding: 0;
  line-height: 1;
  font-size: 1.125rem;
  color: inherit;
  opacity: 0.6;
  cursor: pointer;
}
.toast-close:hover { opacity: 1; }
.toast-hide { animation: toast-out 0.2s ease-in forwards; }
@keyframes toast-in { from { opacity: 0; transform: translateX(16px); } to { opacity: 1; transform: translateX(0); } }
@keyframes toast-out { from { opacity: 1; transform: translateX(0); } to { opacity: 0; transform: translateX(16px); } }

/* ---------- Confirm toast (interactive Yes/No "are you sure?" prompt) ---------- */
/* Replaces the browser's native confirm() for every destructive/stateful
   action app-wide (Cancel order, Deactivate/Reactivate, Delete draft order,
   Deconfirm, Remove line) -- same fixed corner position as the flash-message
   .toast-container above and the same .card visual language, but a single
   reusable element (not one per message) that stays open until answered
   instead of auto-dismissing. z-index 80 -- above regular toasts (70) so a
   confirm prompt is never hidden behind one. See Cruze.confirmAction() in
   this template's own <script> for the JS half. */
.confirm-toast-container {
  position: fixed;
  top: 76px;
  right: 1.25rem;
  z-index: 80;
  width: min(360px, calc(100vw - 2.5rem));
}
.confirm-toast {
  display: none;
  flex-direction: column;
  gap: 0.75rem;
  padding: 0.875rem 1rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  animation: toast-in 0.2s ease-out;
}
.confirm-toast.show { display: flex; }
.confirm-toast-message { font-size: 0.875rem; color: var(--color-text); }
.confirm-toast-actions { display: flex; justify-content: flex-end; gap: 0.5rem; }

/* ---------- Pagination ---------- */
.pagination { display: flex; list-style: none; gap: 0.25rem; margin: 1rem 0 0; padding: 0; }
.page-item .page-link {
  display: inline-block;
  padding: 0.4rem 0.75rem;
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  color: var(--color-text);
  background: var(--color-surface);
  font-size: 0.8125rem;
}
.page-item .page-link:hover { background: var(--color-bg); text-decoration: none; }
.page-item.disabled .page-link { color: var(--color-text-muted); background: var(--color-bg); cursor: default; }
.page-item.active .page-link { background: var(--color-primary); border-color: var(--color-primary); color: #fff; }
.page-item.active .page-link:hover { background: var(--color-primary); }
.pagination-sm .page-link { padding: 0.3rem 0.6rem; }

/* ---------- List group / progress (dashboard leaderboard) ---------- */
.list-group { display: flex; flex-direction: column; gap: 0.5rem; }
.list-group-item {
  border: 1px solid var(--color-border);
  border-radius: var(--radius-sm);
  padding: 0.65rem 0.85rem;
  background: var(--color-surface);
}
.progress { height: 6px; background: var(--color-border); border-radius: 999px; overflow: hidden; }
.progress-bar { height: 100%; background: var(--color-primary); }

/* ---------- Row action menus ---------- */
/* Client review: "Where applicable, tables should include an easily
   accessible action menu for each record rather than requiring the user
   to open the full record for every action." One small "⋮" toggle per
   row opens a dropdown of that row's actions — see base.html's
   Cruze.toggleRowActions() for the open/close behavior. */
.row-actions { position: relative; text-align: right; white-space: nowrap; }
.row-actions-toggle {
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  color: var(--color-text-muted);
  font-size: 1rem;
  line-height: 1;
  padding: 0.3rem 0.55rem;
  cursor: pointer;
}
.row-actions-toggle:hover { background: var(--color-bg); color: var(--color-text); }
.row-actions-menu {
  display: none;
  /* Base position: absolute/top/right is only a fallback for the instant
     before JS measures and switches this to position: fixed (see
     positionRowActionsMenu() in base.html) -- fixed positioning is what
     lets the menu escape the table's .card overflow:hidden clipping and
     the viewport edge, which absolute positioning inside the table cell
     can't do near the bottom of a page/table. */
  position: absolute;
  top: 100%;
  right: 0;
  min-width: 160px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  z-index: 100;
  padding: 0.3rem;
}
.row-actions-menu.show { display: flex; flex-direction: column; }
.row-actions-menu a,
.row-actions-menu button {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  width: 100%;
  text-align: left;
  background: transparent;
  border: none;
  border-radius: var(--radius-sm);
  padding: 0.45rem 0.6rem;
  font-size: 0.8125rem;
  color: var(--color-text);
  cursor: pointer;
}
.row-actions-menu svg { flex-shrink: 0; }
.row-actions-menu a:hover,
.row-actions-menu button:hover { background: var(--color-bg); text-decoration: none; }
.row-actions-menu .text-danger:hover { background: var(--color-danger-bg); }
.row-actions-menu .text-success:hover { background: var(--color-success-bg); }
.row-actions-menu form { margin: 0; }

/* ---------- Topbar user menu ---------- */
/* Same open/close idiom as .row-actions above (a toggle button + its very
   next sibling as the dropdown, shown/hidden via Cruze.toggleUserMenu()) —
   the avatar+name+chevron block in dashboard.html implies a dropdown, and
   this reuses the pattern already established for row action menus instead
   of a bespoke one just for the topbar. */
.user-menu { position: relative; }
.user-menu-toggle {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding-left: 0.875rem;
  border-left: 1px solid var(--color-border);
  background: transparent;
  border-top: none;
  border-right: none;
  border-bottom: none;
  cursor: pointer;
  color: inherit;
  font: inherit;
  text-align: left;
}
.user-menu-name { font-size: 0.8125rem; font-weight: 600; color: var(--color-text); }
.user-menu-role { font-size: 0.6875rem; color: var(--color-text-muted); }
.user-menu-dropdown {
  display: none;
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  min-width: 180px;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  z-index: 40;
  padding: 0.5rem;
}
.user-menu-dropdown.show { display: block; }
.user-menu-dropdown form { margin: 0; }
.user-menu-dropdown .btn { width: 100%; justify-content: center; }

/* ---------- Topbar notification bell ---------- */
/* Same open/close idiom as .user-menu above (Cruze.toggleNotifMenu()) --
   fed by core.context_processors.notifications on every authenticated
   request (see core.notifications.topbar_notifications). A live snapshot
   of current state (pending Stock Approvals for Admins, Low Stock Alerts
   for everyone), not a persisted/dismissible inbox -- nothing here is ever
   marked read, it just reflects what's true right now. */
.notif-menu { position: relative; }
/* background moved here from an inline style= in base.html -- an inline
   style's specificity always beats a stylesheet rule (hover or not), so
   the :hover rule below could never actually override it while it stayed
   inline. */
.notif-menu-toggle { position: relative; border: none; cursor: pointer; background: var(--color-secondary-bg); }
/* Darken slightly on hover (not --color-bg -- that's actually *lighter*
   than --color-secondary-bg, the wrong direction for hover feedback). */
.notif-menu-toggle:hover { background: var(--color-border); }
.notif-badge {
  position: absolute;
  top: -2px;
  right: -2px;
  min-width: 16px;
  height: 16px;
  padding: 0 3px;
  border-radius: 999px;
  background: var(--color-danger);
  color: #fff;
  font-size: 0.625rem;
  font-weight: 700;
  line-height: 16px;
  text-align: center;
}
.notif-dropdown {
  display: none;
  position: absolute;
  top: calc(100% + 0.5rem);
  right: 0;
  width: 300px;
  max-height: 360px;
  overflow-y: auto;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  z-index: 40;
  padding: 0.5rem;
}
.notif-dropdown.show { display: block; }
.notif-dropdown-header { font-size: 0.8125rem; font-weight: 700; color: var(--color-text); padding: 0.35rem 0.5rem 0.5rem; }
.notif-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0.2rem; }
.notif-item {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  padding: 0.5rem 0.6rem;
  border-radius: var(--radius-sm);
  font-size: 0.8125rem;
  color: var(--color-text);
}
.notif-item:hover { background: var(--color-bg); text-decoration: none; }
.notif-item .icon-chip { flex-shrink: 0; }
.notif-empty { margin: 0; padding: 0.75rem 0.5rem; text-align: center; font-size: 0.8125rem; color: var(--color-text-muted); }

/* ---------- Dashboard (dashboard.html design) ---------- */
.card-header-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; padding: 1.25rem 1.375rem 0; flex-wrap: wrap; }
.card-title { font-size: 0.96875rem; font-weight: 700; color: var(--color-text); margin: 0; }
.card-subtitle { font-size: 0.78125rem; color: var(--color-text-muted); margin-top: 0.2rem; }
.see-all { font-size: 0.78125rem; font-weight: 600; color: var(--color-primary); }

.icon-chip {
  width: 38px;
  height: 38px;
  border-radius: 0.6875rem;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.icon-chip-sm { width: 30px; height: 30px; border-radius: 0.5rem; }
.icon-chip-xs { width: 24px; height: 24px; border-radius: 0.4rem; }

/* Topbar EN/AR language switcher (next to the notification bell) -- an
   .icon-chip reused on a <button> instead of a <div>, so it needs the same
   border/cursor reset as .sidebar-toggle/.user-menu-toggle above. */
.lang-switch-form { display: contents; }
/* background moved here from an inline style= in base.html -- same
   inline-beats-stylesheet reason as .notif-menu-toggle above. */
.lang-switch-btn { border: none; cursor: pointer; color: var(--color-text); background: var(--color-secondary-bg); }
/* Same darken-on-hover fix/rationale as .notif-menu-toggle above. */
.lang-switch-btn:hover { background: var(--color-border); }

.avatar { width: 34px; height: 34px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #fff; font-weight: 700; font-size: 0.78125rem; flex-shrink: 0; }
.avatar-sm { width: 30px; height: 30px; font-size: 0.71875rem; }
.avatar-primary { background: var(--color-primary); }
.avatar-info { background: var(--color-info); }
.avatar-accent { background: var(--color-accent); }
.avatar-muted { background: var(--color-secondary-text); }

/* Rep Leaderboard rank badge -- a small circular chip, same size/shape for
   every rank, rather than a big emoji medal for #1-3 next to bare unstyled
   text for #4+ (the "broken 4" look). #1-3 hold the shared .rank-medal SVG
   (a generic award-ribbon shape, drawn in this app's own stroke style
   rather than reusing emoji or importing an icon font); #4+ hold the plain
   rank number, styled identically otherwise. */
.rank-badge {
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 22px; border-radius: 50%; flex-shrink: 0;
  font-size: 0.6875rem; font-weight: 700;
  background: var(--color-secondary-bg); color: var(--color-secondary-text);
}
.rank-badge-gold { background: var(--color-accent-light); color: var(--color-accent); }
.rank-badge-silver { background: var(--color-secondary-bg); color: #64748b; }
.rank-badge-bronze { background: #fdece0; color: #b45309; }
/* 4th place -- deliberately NOT another filled gray chip (that read as
   identical to silver at a glance): an outline instead of a fill reads as
   its own, clearly-lower tier rather than a shade of #2. */
.rank-badge-plain { background: transparent; border: 1.5px solid var(--color-border); color: var(--color-text-muted); }

.mini-table { width: 100%; border-collapse: collapse; font-size: 0.8125rem; }
.mini-table thead th { text-align: left; color: var(--color-secondary-text); font-size: 0.65625rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 700; padding: 0 0.25rem 0.5rem 0; }
.mini-table tbody td { padding: 0.5625rem 0.25rem; border-top: 1px solid var(--color-bg); color: var(--color-text); }
.table-row-link { position: relative; }
.table-row-link:hover { background: var(--color-bg); }
/* Makes the whole row a click target for a real <a> in one of its cells
   (middle-click/ctrl-click "open in new tab" needs an actual anchor, not a
   row onclick) without wrapping every cell in its own link. */
.table-row-link .stretched-link { color: inherit; text-decoration: none; }
.table-row-link .stretched-link::after { content: ""; position: absolute; inset: 0; }

.pill-tabs-wrap { display: inline-flex; background: var(--color-secondary-bg); padding: 0.25rem; border-radius: var(--radius-md); gap: 0.125rem; flex-wrap: wrap; }
/* Reset border/background/font/cursor explicitly (not just relying on the
   browser's own UA defaults) -- .pill-tab started out as an <a>-only
   pattern (Recent Orders' tabs originally navigated the server via a
   ?recent_status= querystring), which needed none of this since anchors
   have no default chrome. Recent Orders' tabs are real <button>s now too
   (a pure client-side show/hide, same idea as the Sales Overview chart's
   Bar/Line toggle, which was <button>-based from the start) -- these
   resets are what stop a bare <button>'s own boxy outline showing through. */
.pill-tab { appearance: none; -webkit-appearance: none; padding: 0.4375rem 0.875rem; border: none; border-radius: var(--radius-sm); background: transparent; font-family: inherit; font-size: 0.78125rem; font-weight: 700; color: var(--color-text-muted); cursor: pointer; }
.pill-tab:hover { text-decoration: none; color: var(--color-text); }
.pill-tab.active { background: var(--color-surface); color: var(--color-text); box-shadow: 0 1px 3px rgba(30, 41, 59, 0.12); }

/* Compact 2-line stat tile (client review: dashboard cards were much
   taller than they needed to be) -- an icon+label(+trend) header line,
   then one bold value line below. Kept as the single definition for these
   classes (no separate "compact" modifier) since Inventory's stat cards
   are meant to adopt this exact same look next. */
.stat-tile-head { display: flex; align-items: center; gap: 0.5rem; }
.stat-tile-label { flex: 1; min-width: 0; font-size: 0.75rem; color: var(--color-text-muted); font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.stat-tile-value { margin-top: 0.3rem; font-size: 1.3125rem; font-weight: 700; color: var(--color-text); line-height: 1.2; }
.stat-tile-note { font-size: 0.75rem; font-weight: 500; color: var(--color-text-muted); }

.legend-dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
.legend-swatch { width: 8px; height: 8px; border-radius: 2px; display: inline-block; flex-shrink: 0; }

/* Recent Orders card rows */
.order-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.75rem 0; border-top: 1px solid var(--color-bg); }
.order-row:first-child { border-top: none; }
.order-row-title { font-size: 0.84375rem; font-weight: 700; color: var(--color-text); }
.order-row-meta { font-size: 0.75rem; color: var(--color-text-muted); margin-top: 0.1rem; }
.order-row-amount { font-size: 0.84375rem; font-weight: 700; color: var(--color-text); margin-right: 0.375rem; white-space: nowrap; }

/* Low Stock Alerts card rows */
.stock-alert-row { display: flex; align-items: center; justify-content: space-between; gap: 0.75rem; }
.stock-alert-name { font-size: 0.78125rem; font-weight: 600; color: var(--color-text); }

/* ---------- Utilities (only what templates use) ---------- */
.d-flex { display: flex; }
.d-inline-flex { display: inline-flex; }
.d-none { display: none !important; }
.justify-content-between { justify-content: space-between; }
.justify-content-center { justify-content: center; }
.align-items-center { align-items: center; }
.align-items-end { align-items: flex-end; }
.align-items-start { align-items: flex-start; }
.flex-wrap { flex-wrap: wrap; }
.text-muted { color: var(--color-text-muted); }
.text-white { color: #fff; }
.text-danger { color: var(--color-danger); }
.text-success { color: var(--color-success); }
.small { font-size: 0.8125rem; }
.fs-3 { font-size: 1.5rem; font-weight: 600; }
.h4 { font-size: 1.25rem; }
.h6 { font-size: 0.9375rem; }
.mb-0 { margin-bottom: 0; }
.mb-1 { margin-bottom: 0.25rem; }
.mb-2 { margin-bottom: 0.5rem; }
.mb-3 { margin-bottom: 1rem; }
.mb-4 { margin-bottom: 1.5rem; }
.mt-4 { margin-top: 1.5rem; }
.me-3 { margin-right: 1rem; }
.me-auto { margin-right: auto; }
.ps-3 { padding-left: 1rem; }

/* ---------- Responsive sidebar (off-canvas below 992px) ---------- */
@media (max-width: 991px) {
  .sidebar {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    height: 100vh;
    max-height: none;
    z-index: 50;
    box-shadow: var(--shadow-md);
    transform: translateX(-100%);
    transition: transform 0.2s ease;
  }
  .app-body.sidebar-open .sidebar { transform: translateX(0); }
  .app-body.sidebar-collapsed .sidebar { width: 220px; } /* the desktop "collapse to nothing" doesn't apply here */
  .app-body.sidebar-open .sidebar-backdrop {
    display: block;
    position: fixed;
    inset: 0;
    background: rgba(15, 23, 42, 0.45);
    z-index: 40;
  }
}
@media (max-width: 767px) {
  .topbar { flex-wrap: wrap; row-gap: 0.5rem; }
  .user-menu-name, .user-menu-role { display: none; } /* the avatar alone is enough on a phone-width topbar */
  .global-search { order: 3; flex-basis: 100%; max-width: none; }
}

/* ---------- Order receipt (print-only, orders/order_detail.html) ---------- */
/* "Print receipt" used to navigate to its own standalone page; it's now a
   hidden block embedded directly in the order detail page instead, shown
   only when printing -- clicking the button just calls window.print()
   in-place, no navigation. .receipt-print-only stays display:none on
   screen; @media print hides everything else on the page (topbar, sidebar,
   the normal order detail content) and reveals only this block, positioned
   to fill the printed page on its own. */
.receipt-print-only { display: none; }
.receipt-header { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; margin-bottom: 1rem; }
.receipt-header img { height: 40px; width: auto; display: block; margin-bottom: 0.5rem; }
.receipt-header h1 { margin: 0 0 0.25rem 0; }
.receipt-divider { border: none; border-top: 2px solid var(--color-primary); margin: 0 0 1.5rem 0; }
.receipt-party-block { margin-bottom: 1.5rem; }
.receipt-party-block h2 { margin-bottom: 0.5rem; }
.receipt-party-table { width: 100%; border-collapse: collapse; }
.receipt-party-table th, .receipt-party-table td { text-align: left; padding: 0.3rem 0.5rem 0.3rem 0; vertical-align: top; border-bottom: 1px solid var(--color-border); font-size: 0.875rem; }
.receipt-party-table tr:last-child th, .receipt-party-table tr:last-child td { border-bottom: none; }
.receipt-party-table th { width: 40%; font-weight: 600; color: var(--color-text-muted); white-space: nowrap; }
@media print {
  /* Chrome/Firefox draw their own page title/URL/date header-footer inside
     the page's print margin area -- there's no page-content way to turn
     that off directly (it's the browser's own "Headers and footers" print
     option), but a zero @page margin leaves no room for the browser to
     draw it into, which suppresses it in practice. Padding on
     .receipt-print-only itself replaces the lost page margin so the
     receipt doesn't print edge-to-edge. */
  @page { margin: 0; }
  .topbar, .sidebar, .sidebar-backdrop { display: none !important; }
  body * { visibility: hidden; }
  .receipt-print-only, .receipt-print-only * { visibility: visible; }
  .receipt-print-only {
    display: block;
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    max-width: 800px;
    margin: 0 auto;
    padding: 0.5in 0.75in;
  }
}
