/* Bootstrap Dashboard Example - Exact Styling */

body {
  font-size: .875rem;
}

.feather {
  width: 16px;
  height: 16px;
}

/*
 * Sidebar
 */

@media (min-width: 768px) {
  .text-bg-dark {
    position: sticky;
    top: 48px;
    height: calc(100vh - 48px);
  }
}

/*
 * Navbar
 */

.navbar-brand {
  height: 100%;
  background-color: rgba(0, 0, 0, .25);
  
  padding: 1rem 1rem;
}

.navbar-brand a {
  height: 100%;
}


.navbar .form-control {
  padding: .5rem 1rem;
}

/* Search bar styling */
.search-form-container {
  width: 60%;
  margin: 0 0;
  padding: 5px 10px;
}

.form-control-dark {
  color: #fff;
  background-color: rgba(255, 255, 255, .1);
  border-color: rgba(255, 255, 255, .1);
}

.form-control-dark:focus {
  border-color: transparent;
  box-shadow: 0 0 0 3px rgba(255, 255, 255, .25);
}

@media (max-width: 767.98px) {
  .search-form-container {
    width: 100%;
  }
}

/*
 * Sidebar styling
 */

.sidebar {
  background-color: #2b3035 !important;
}

/* The collapsible Settings group. Bootstrap puts .collapsed on the trigger
   while the group is closed; the chevron is the only thing that changes. */
.nav-group-toggle .bi {
  transition: transform .15s ease;
}
.nav-group-toggle.collapsed .bi {
  transform: rotate(-90deg);
}



/*
 * Content
 */

[role="main"] {
  padding-top: 25px;
}

@media (min-width: 768px) {
  [role="main"] {
    padding-top: 25px;
  }
}

/*
 * Icon styling
 */

.bi {
  display: inline-block;
  width: 1rem;
  height: 1rem;
  fill: currentColor;
}

/*
 * Nav pills styling
 */

.nav-pills .nav-link {
  font-size: 1rem;
}

.nav-pills .nav-link svg {
  fill: white;
}

/* HTMX loading spinner indicator */
.htmx-indicator {
  display: none;
}

.htmx-request .htmx-indicator {
  display: inline-block;
}

.htmx-request .button-text {
  opacity: 0.6;
}

/* Status badge styling */
.status-badge {
  background-color: #d4edda;
  color: #155724;
  border: none;
  font-weight: bold;
}

/* Inventory list: deposit stock type — light orange, distinct from warning/yellow badges */
.badge.inventory-stock-type-deposit {
  background-color: #ffe8cc;
  color: #9a5b00;
  border: 1px solid #ffc978;
}

/* Country flag icon styling */
.country-flag {
  width: 24px;
  height: 24px;
  border-radius: 4px;
  object-fit: cover;
  display: inline-block;
}

/* Shipments table vertical alignment */
.table tbody td,
.table tbody th {
  vertical-align: middle;
}

/*
 * Hub app shell — grey main canvas, white cards (Shopify Admin–like)
 */
body.hub-app {
  --hub-page-bg: #f1f2f4;
  --hub-border: #e3e5e8;
  --hub-muted: #5c6165;
  --hub-surface: #fff;
}

body.hub-app main[role="main"] {
  background-color: var(--hub-page-bg);
}

body.hub-app main[role="main"] > footer.border-top {
  background-color: var(--hub-page-bg);
}

.hub-app .hub-card,
.hub-app .card.hub-card {
  background: var(--hub-surface);
  border: 1px solid var(--hub-border);
  border-radius: 0.5rem;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}

.hub-app .card.hub-card .card-header {
  background: var(--hub-surface);
  border-bottom-color: var(--hub-border);
}

.hub-app .card.hub-card.border-primary {
  border-color: var(--bs-primary) !important;
}

/* —— Global detail drawer (60% viewport width) —— */
.hub-drawer {
  position: fixed;
  inset: 0;
  z-index: 2000;
  pointer-events: none;
}

.hub-drawer.is-open {
  pointer-events: auto;
}

.hub-drawer-backdrop {
  position: absolute;
  inset: 0;
  background: rgba(32, 34, 35, 0.45);
  opacity: 0;
  transition: opacity 0.2s ease;
}

.hub-drawer.is-open .hub-drawer-backdrop {
  opacity: 1;
}

.hub-drawer-panel {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  width: 60vw;
  min-width: min(60vw, 100%);
  max-width: 100%;
  /* The panel is a query container so its contents respond to the PANEL's
     width, not the window's. Bootstrap's col-lg-* measures the viewport, so
     inside a 60vw drawer the lg split still fires and gets crammed: col-lg-4
     resolves to 296px at 1440 and 264px at 1280, against 380px+ for the same
     markup on the full page. */
  container-type: inline-size;
  container-name: drawer;
  background: #fff;
  box-shadow: -8px 0 32px rgba(0, 0, 0, 0.12);
  display: flex;
  flex-direction: column;
  transform: translateX(100%);
  transition: transform 0.25s ease;
}

.hub-drawer.is-open .hub-drawer-panel {
  transform: translateX(0);
}

.hub-drawer-close {
  position: absolute;
  top: 0.75rem;
  left: 0.75rem;
  z-index: 2;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  border: 1px solid #e3e5e8;
  border-radius: 0.375rem;
  background: #fff;
  color: #202223;
  line-height: 1;
}

.hub-drawer-close:hover {
  background: #f6f6f7;
  color: #000;
}

.hub-drawer-body {
  flex: 1 1 auto;
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  padding: 1.25rem 1.25rem 1.25rem;
  /* Match resource detail <main> grey — injected HTML has no <body> classes */
  background-color: #f1f2f4;
}

@media (max-width: 575.98px) {
  .hub-drawer-panel {
    width: 100%;
  }
}

tr.hub-drawer-row {
  cursor: pointer;
}

/*
 * ERP resource detail pages (orders, inventory, future drawer targets).
 * Lives in styles.css (not <head> blocks) so HubDrawer can inject only body
 * markup and still match the full-page look — bootstrap + this file load on the host shell.
 */

/* Order detail — grey canvas on <main> */
body.order-detail-page {
  --resource-page-bg: #f1f2f4;
  --resource-main-padding-x: 1.25rem;
}

@media (min-width: 768px) {
  body.order-detail-page {
    --resource-main-padding-x: 2.25rem;
  }
}

body.order-detail-page main[role="main"] {
  background-color: var(--resource-page-bg);
  padding-left: var(--resource-main-padding-x) !important;
  padding-right: var(--resource-main-padding-x) !important;
}

body.order-detail-page main[role="main"] > footer.border-top {
  background-color: var(--resource-page-bg);
}

/* Inventory detail — same layout tokens + embed body (drawer has no <main>) */
body.inventory-detail-page {
  --resource-page-bg: #f1f2f4;
  --resource-main-padding-x: 1.25rem;
}

@media (min-width: 768px) {
  body.inventory-detail-page {
    --resource-main-padding-x: 2.25rem;
  }
}

body.inventory-detail-page main[role="main"] {
  background-color: var(--resource-page-bg);
  padding-left: var(--resource-main-padding-x) !important;
  padding-right: var(--resource-main-padding-x) !important;
}

body.inventory-detail-page main[role="main"] > footer.border-top {
  background-color: var(--resource-page-bg);
}

body.inventory-detail-page.hub-min-embed {
  margin: 0;
  min-height: 100%;
  background-color: var(--resource-page-bg);
}

/* Shopify Admin–style shared layout (order + inventory single) */
.order-resource-page {
  --order-border: #e3e5e8;
  /* #6d7175 was 4.39:1 on the #f1f2f4 canvas — under AA. #5c6165 clears it
     on every surface the app uses (4.76:1 worst case, on .hub-selbar). */
  --order-muted: #5c6165;
  --order-surface: #fff;
  width: 100%;
  max-width: 100%;
}

.order-resource-page .order-card {
  background: var(--order-surface);
  border: 1px solid var(--order-border);
  border-radius: 0.5rem;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
}

.order-resource-page .order-card-header {
  padding: 1rem 1.25rem;
  border-bottom: 1px solid var(--order-border);
  background: var(--order-surface);
  border-radius: 0.5rem 0.5rem 0 0;
}

.order-resource-page .order-section-label {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--order-muted);
  margin-bottom: 0.75rem;
}

.order-resource-page .order-field-label {
  font-size: 0.8125rem;
  color: var(--order-muted);
  margin-bottom: 0.25rem;
  line-height: 1.3;
}

.order-resource-page .order-field-value {
  font-size: 0.9375rem;
  color: #202223;
  line-height: 1.4;
  word-break: break-word;
}

.order-resource-page .order-field-value code {
  font-size: 0.875rem;
  background: #f1f2f4;
  padding: 0.125rem 0.375rem;
  border-radius: 0.25rem;
  color: #202223;
}

.order-resource-page .order-table-wrap {
  border: 1px solid var(--order-border);
  border-radius: 0.375rem;
  overflow: hidden;
}

.order-resource-page .order-table-wrap table thead th {
  font-size: 0.75rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--order-muted);
  background: #fafbfb;
  border-bottom: 1px solid var(--order-border);
  padding: 0.625rem 0.75rem;
  white-space: nowrap;
}

.order-resource-page .order-table-wrap table tbody td {
  padding: 0.625rem 0.75rem;
  vertical-align: middle;
  border-color: var(--order-border);
}

.order-resource-page .order-json-pre {
  font-size: 0.75rem;
  max-height: 420px;
  overflow: auto;
  background: #fafbfb;
  border: 1px solid var(--order-border);
  border-radius: 0.375rem;
  padding: 1rem;
  margin: 0;
}

.order-resource-page .order-header-flag {
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 0.25rem;
  object-fit: cover;
  flex-shrink: 0;
  vertical-align: middle;
}

.order-resource-page .order-status-block {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.375rem;
}

.order-resource-page .order-status-label {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--order-muted);
  line-height: 1;
}

.order-resource-page .order-status-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  line-height: 1.3;
  border-radius: 0.5rem;
  border: 1px solid rgba(32, 34, 35, 0.12);
  box-shadow: 0 1px 0 rgba(255, 255, 255, 0.6) inset;
}

.order-resource-page .order-status-badge-lg {
  font-size: 1.0625rem;
  padding: 0.5rem 0.875rem;
  min-height: 2.5rem;
}

.order-resource-page .order-expected-delivery-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 1.0625rem;
  font-weight: 600;
  line-height: 1.3;
  padding: 0.5rem 0.875rem;
  min-height: 2.5rem;
  border-radius: 0.5rem;
  color: #202223;
  background-color: rgba(255, 255, 255, 0.35);
  border: 1px solid rgba(32, 34, 35, 0.14);
  box-shadow: none;
}

.order-resource-page .order-badge-pill {
  font-weight: 500;
  font-size: 0.75rem;
  padding: 0.25rem 0.5rem;
  border-radius: 1rem;
}

.order-resource-page .order-header-main-row {
  column-gap: 6rem;
  row-gap: 1rem;
}

/* ── Inventory single-item page extras ─────────────────────────────── */




/* When the inventory single page renders inside the right-side drawer, hide
   the panel-level absolute close button injected by base.html — the utility
   bar's own close action takes its place, so users see exactly one close
   affordance. Uses :has() (Chrome 105+, Firefox 121+, Safari 15.4+). */
.hub-drawer-panel:has(.inv-single) > .hub-drawer-close {
  display: none;
}

/* Header layout: identity (2fr) + operational state (3fr), collapsing to one
   stacked column on narrow viewports. */
.order-resource-page .inventory-header-grid {
  display: grid;
  grid-template-columns: 2fr 3fr;
  gap: 1rem;
  align-items: start;
}

@media (max-width: 991.98px) {
  .order-resource-page .inventory-header-grid {
    grid-template-columns: 1fr;
  }
}

/* Location / stock-type selects sized to their content rather than the row. */
.order-resource-page .inventory-inline-select {
  width: auto;
  min-width: 9rem;
}

/* Backdrop tint for the server-rendered (non-JS) modal open state. */
.inventory-modal-backdrop-tint {
  background-color: rgba(0, 0, 0, 0.5);
}

/* Secondary identifier chip row under the page title — kvik_id, EAN, supplier.
   Visually de-emphasised vs. status pills so the eye lands on operational state first. */
.order-resource-page .inventory-secondary-ids {
  color: var(--order-muted);
}

.order-resource-page .inventory-secondary-id {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0.125rem 0.5rem;
  background: #fafbfb;
  border: 1px solid var(--order-border);
  border-radius: 0.375rem;
  font-size: 0.8125rem;
}

.order-resource-page .inventory-secondary-id-label {
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--order-muted);
}

.order-resource-page .inventory-secondary-id code {
  background: transparent;
  padding: 0;
}

/* Picking & Packing: highlight orders waiting 2+ supplier processing days */
.table-hover > tbody > tr.parts-ordered-attention-row > * {
  background-color: #fde8e8;
}

.table-hover > tbody > tr.parts-ordered-attention-row:hover > * {
  background-color: #f8d4d4;
}


/* ══════════════════════════════════════════════════════════════════════
   hub-* list-page contract
   The component vocabulary every list page implements. Full rationale in
   vorto/vortohub-design/DESIGN-SYSTEM.md.

   Rule 1: no <style> block in a list-page template.
   Rule 2: no layout in a style= attribute.
   If a rule is needed it goes here with a hub- prefix, or it does not exist.
   ══════════════════════════════════════════════════════════════════════ */

/* Bootstrap's .text-muted (#6c757d) measures 4.19:1 on the #f1f2f4 canvas —
   under AA — and it carries most of the secondary text on every list page. */
.hub-app .text-muted { color: var(--hub-muted) !important; }

/* Active tab. Bootstrap's default active tab is white on a white card, so the
   only cue is a hairline border. Three cues, none hue-dependent: a grey ground,
   semibold weight, and a 2px inset top rule. */
.hub-app .nav-tabs .nav-link { color: var(--hub-muted); border-color: transparent; }
.hub-app .nav-tabs .nav-link:hover { background: #f1f2f4; border-color: transparent; }
.hub-app .nav-tabs .nav-link.active {
  background: #e9ecef;
  color: #212529;
  font-weight: 600;
  border-color: var(--hub-border) var(--hub-border) #e9ecef;
  box-shadow: inset 0 2px 0 #495057;
}

/* ── Data table ────────────────────────────────────────────────────────── */
.hub-table { font-size: .8125rem; }
.hub-table > :not(caption) > * > * { padding: .5rem .375rem; }
.hub-table.hub-dense > :not(caption) > * > * { padding: .25rem .375rem; }
.hub-table thead th {
  position: sticky;
  top: 0;
  z-index: 2;
  background: #f7f8f9;
  border-bottom: 1px solid var(--hub-border) !important;
  font-weight: 600;
  font-size: .75rem;
  color: var(--hub-muted);
  white-space: nowrap;
}

/* Numbers stay in the body typeface. Column alignment comes from tabular
   figures, not from swapping typeface — v1 mixed the two inside one card, so
   BRAND read sans and SUPPLIER SKU read mono a single field apart. Monospace
   survives only where it is a badge or a literal code reference. */
.hub-num { font-variant-numeric: tabular-nums; font-feature-settings: "tnum" 1, "lnum" 1; }

/* Secondary line inside a cell — supplier SKU, stock type, scan-job name. */
.hub-sub { font-size: .6875rem; color: var(--hub-muted); }

/* Shelf address is the thing people read across a row, so it gets its own size
   rather than inheriting the 13px body scale — and it is plain text, not a
   badge. The badge in this cell is the stock type, which is a category; the
   address is an identifier you read, so it carries weight instead of a chip. */
.hub-loc {
  display: block;
  font-size: 1rem;
  font-weight: 700;
  font-family: var(--bs-font-monospace);
  letter-spacing: .01em;
  line-height: 1.25;
}

/* Sortable header. Direction is stated by an arrow, never by colour alone. */
.hub-sort { background: none; border: 0; padding: 0; font: inherit; color: inherit; white-space: nowrap; }
.hub-sort:hover { color: var(--bs-primary); }
.hub-sort .bi { opacity: .3; font-size: .7rem; }
.hub-sort[aria-sort="ascending"] .bi,
.hub-sort[aria-sort="descending"] .bi { opacity: 1; color: var(--bs-primary); }

/* No unbounded value in a table cell: truncate, full value on title=. */
.hub-clip { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Active-filter chips — without these you cannot see what is filtering without
   opening each of six dropdowns one at a time. */
.hub-chip {
  display: inline-flex;
  align-items: center;
  gap: .35rem;
  font-size: .75rem;
  background: #e9ecef;
  border: 1px solid #dee2e6;
  border-radius: 1rem;
  padding: .1rem .25rem .1rem .6rem;
}
.hub-chip button,
.hub-chip a { border: 0; background: none; line-height: 1; padding: 0 .3rem; color: var(--hub-muted); border-radius: 50%; }
.hub-chip button:hover,
.hub-chip a:hover { background: #ced4da; color: #000; }

/* Selection bar, replacing "this silently applies to all 42 641 filtered rows". */
.hub-selbar { background: #cfe2ff; border: 1px solid #9ec5fe; border-radius: .375rem; }

/* ── Drawer-aware layout ───────────────────────────────────────────────── */
/* .hub-drawer-panel declares container-type/container-name above. 780px is
   chosen so a 1280px laptop (768px panel) stacks cleanly rather than squeezing,
   while 1440px (864px) splits. */
.inv-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; }
@container drawer (min-width: 780px) { .inv-grid { grid-template-columns: 1.75fr 1fr; } }

/* Full page has no container ancestor, so it keys off the viewport — same rule,
   same proportions, same block order. Layout parity is the requirement. */
.inv-page .inv-grid { grid-template-columns: 1fr; }
@media (min-width: 992px) { .inv-page .inv-grid { grid-template-columns: 1.75fr 1fr; } }

/* Catalog card: image beside identity, both container-aware. */
.cat-split { display: grid; grid-template-columns: 1fr; gap: 1rem; }
@container drawer (min-width: 560px) { .cat-split { grid-template-columns: 132px 1fr; } }
.inv-page .cat-split, .cat-page .cat-split { grid-template-columns: 132px 1fr; }
.cat-thumb {
  width: 132px;
  height: 132px;
  border: 1px solid var(--hub-border);
  border-radius: .5rem;
  background: var(--hub-surface);
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.cat-thumb img { max-width: 100%; max-height: 100%; }

/* Identifier chips — v1 stacked these as five labelled rows down a column. */
.idchip {
  display: inline-flex;
  align-items: baseline;
  gap: .35rem;
  font-size: .75rem;
  background: #f7f8f9;
  border: 1px solid var(--hub-border);
  border-radius: .3rem;
  padding: .1rem .45rem;
}
.idchip .k { color: var(--hub-muted); font-size: .6875rem; }
.idchip code { color: #a626a4; }

.order-card { background: var(--hub-surface); border: 1px solid var(--hub-border); border-radius: .5rem; }
.order-card-header { padding: .6rem .9rem; border-bottom: 1px solid var(--hub-border); font-weight: 600; font-size: .875rem; }
.order-field-label { font-size: .6875rem; text-transform: uppercase; letter-spacing: .04em; color: var(--hub-muted); }

/* Empty state — an icon, a sentence, and an action. Never a rendered <thead>
   over zero rows, and never one grey line of text. */
.hub-empty { padding: 3rem 1rem; text-align: center; color: var(--hub-muted); }
.hub-empty .bi { font-size: 2rem; opacity: .45; display: block; margin-bottom: .75rem; }

/* Row → drawer. The rule that clicks on links inside a row do NOT open the
   drawer is invisible without a pointer cursor and a hover state. */
.hub-table tbody tr.hub-drawer-row { cursor: pointer; }


/* ── Inventory list ────────────────────────────────────────────────────────
   Column widths are the measured budget from
   app/services/inventory_list.py::INVENTORY_COLUMNS, which is the single
   declaration the table header, the Columns picker and URL state all read.
   test_hub_list_page_contract.py asserts these stay in step with it.

   The budget: 784px fixed + 220px minimum for Product = 1004px against the
   ~1090px available at 1440x900, so Product flexes into 86px of headroom and
   the table measures 0px of horizontal overflow with default columns. ── */
.hub-table .hub-w-select { width: 28px; }
.hub-table .hub-w-id { width: 56px; }
.hub-table .hub-w-product { min-width: 200px; }
.hub-table .hub-w-status { width: 96px; }
.hub-table .hub-w-location { width: 122px; }
.hub-table .hub-w-condition { width: 104px; }
.hub-table .hub-w-price { width: 84px; }
.hub-table .hub-w-supplier { width: 86px; }
.hub-table .hub-w-received { width: 90px; }
.hub-table .hub-w-source { width: 100px; }
.hub-table .hub-w-reserved { width: 86px; }
.hub-table .hub-w-actions { width: 32px; }

/* Wide tier. The team works at 1920x1200, where the table has ~1490px against
   ~1090px at 1440. Without this the flexing Product column absorbs the whole
   400px difference and every fixed column stays as cramped as it was on the
   smaller screen — measured at 1920, Product took 626px while Supplier sat at
   86px and cut off mid-name. */
@media (min-width: 1600px) {
  .hub-table .hub-w-select { width: 32px; }
  .hub-table .hub-w-id { width: 64px; }
  .hub-table .hub-w-product { min-width: 320px; }
  .hub-table .hub-w-status { width: 108px; }
  .hub-table .hub-w-location { width: 136px; }
  .hub-table .hub-w-condition { width: 116px; }
  .hub-table .hub-w-price { width: 100px; }
  .hub-table .hub-w-supplier { width: 132px; }
  .hub-table .hub-w-received { width: 104px; }
  .hub-table .hub-w-source { width: 112px; }
  .hub-table .hub-w-reserved { width: 108px; }
  .hub-table .hub-w-actions { width: 36px; }
}

/* Product is the only column allowed to flex; fixing the rest is what keeps
   the budget honest when a long value arrives. */
.hub-table { table-layout: fixed; }
.hub-table td { overflow: hidden; }

/* "+2 EAN" — the overflow marker for the multi-GTIN string that was one of the
   two unbounded values overflowing the v1 table. Full list on the title. */
.hub-ean-more { font-size: .6rem; font-weight: 500; }

/* Narrow enough to leave the filter row its space — the bar is a budget
   like the table is. */
.hub-search { width: 360px; max-width: 360px; }
.hub-colmenu { min-width: 220px; }
.hub-bulk-select { min-width: 11rem; }
.hub-perpage { width: auto; }

/* Filter picklist inside a dropdown — search box, scrolling checkbox list,
   "Clear selected", selected count. The picklist itself is deliberately
   unchanged from v1; only its sizing moved out of style= attributes. */
.hub-picklist { min-width: 320px; max-width: 420px; }
.hub-picklist-wide { min-width: 360px; max-width: 520px; }
.hub-picklist-narrow { min-width: 220px; max-width: none; }
.hub-picklist-scroll { max-height: 260px; overflow: auto; }

/* Filter overflow menu — filters that ran out of room in the bar. Each moved
   filter becomes a full-width row so its own dropdown opens downwards. */
.hub-more-menu { min-width: 260px; padding: .5rem; }
.hub-more-menu .dropdown-toggle { width: 100%; text-align: left; }

/* Stock type sits under the shelf address and is read at a glance — supplier
   stock vs owned vs deposit changes what you may do with the unit.
   Deliberately restrained: supplier stock is the common case and matches the
   location badge, owned stock is dark so it is the one that stands out, and
   only Deposit carries a hue. Colour is a second signal only; each badge
   carries its own word. Kept at the secondary text size — the shelf address
   above it is what gets the larger type. */
.hub-stock { font-size: .6875rem; font-weight: 500; padding: .1rem .35rem; border: 1px solid transparent; }
.hub-stock-supplier_stock { background: #f8f9fa; color: #212529; border-color: #dee2e6; }
.hub-stock-owned_stock { background: #495057; color: #fff; border-color: #495057; }
.hub-stock-deposit { background: #fff3cd; color: #664d03; border-color: #ffe69c; }

/* A condition that is not Good is what someone scanning this column is looking
   for, so it is the only thing in the cell that is not muted. */
.hub-bad { color: #b02a37; font-weight: 600; }

/* ── Inventory single view ─────────────────────────────────────────────── */
/* Save used to sit at the bottom of a card, below the fold in the drawer. */
.inv-savebar {
  position: sticky;
  bottom: 0;
  z-index: 3;
  background: var(--hub-surface);
  border: 1px solid var(--hub-border);
  border-radius: .5rem;
  box-shadow: 0 -2px 8px rgba(0, 0, 0, .06);
}
.inv-savebar-dirty { border-color: #ffc107; }
.hub-hint { font-size: .75rem; }

/* The single view is a reading width, not a full-bleed table. Left full-width
   it sprawls across a 1920 screen and the eye has to travel for every field. */
.inv-page.inv-single { max-width: var(--hub-fixed-width, 1240px); }
.cat-thumb-empty { font-size: .625rem; font-weight: 600; letter-spacing: .08em; color: #9aa1a8; }
/* Keeps the vorto_sku / vorto_id / EAN labels in one column so the values align. */
.cat-key { min-width: 4.5rem; flex-shrink: 0; }
.cat-search { max-width: 520px; }

/* Drawer footer slot — a flex sibling of the scrolling body, so an action bar
   is pinned whether the content scrolls or not. position:sticky inside the body
   does nothing when the content happens to fit, which left the save bar
   floating mid-panel. */
.hub-drawer-foot {
  flex: 0 0 auto;
  background: var(--hub-surface);
  border-top: 1px solid var(--hub-border);
  box-shadow: 0 -2px 8px rgba(0, 0, 0, .06);
}
/* In the footer slot the bar IS the footer — no margin, no border of its own,
   no radius. It carried Bootstrap's .mt-3, whose !important beat a plain
   margin:0 here and left 17px of dead space above it. The utility class is gone
   and the full-page gap is applied below instead. */
.hub-drawer-foot .inv-savebar {
  position: static;
  border: 0;
  border-radius: 0;
  box-shadow: none;
}

/* Only the full page needs to be pushed away from the content above it; in the
   drawer the footer slot provides that separation itself. */
.inv-single .inv-savebar { margin-top: 1rem; }
.cat-search-input { max-width: 20rem; }

/* ── Movements table ───────────────────────────────────────────────────────
   Four of the five columns are fixed-shape — a timestamp, a short badge, a
   signed single digit, an order id — so they get exactly what their widest
   real content needs and Note takes every remaining pixel. Sized against
   "2026-09-01 13:47" and a 9-digit order id at .8125rem plus .375rem padding.
   Type is 120px so the longest choice, "Return from supplier", keeps its badge
   on one line — the shortest width at which no movement type wraps. ── */
.inv-moves { table-layout: fixed; }
.inv-moves th.mv-when { width: 128px; }
.inv-moves th.mv-type { width: 120px; }
.inv-moves th.mv-qty { width: 56px; }
.inv-moves th.mv-order { width: 88px; }

/* ══ Supplier returns ══════════════════════════════════════════════════════
   Everything below serves the Supplier returns section (vorto/supplier-returns/
   ui-v3-mockups.html). Scan consoles are deliberately outside the list
   contract — "different job, different shape" — but they share these tokens.
   ══════════════════════════════════════════════════════════════════════════ */

/* One reading width for every step view and returns page, as a token so the
   pages cannot drift apart. Matches the Inventory single view. */
body.hub-app { --hub-fixed-width: 1240px; }
.hub-fixed { max-width: var(--hub-fixed-width); }

/* Warehouse badges: bright, bigger, icon-led. A deliberate divergence from the
   office-side subtle badges — this section is operated on a warehouse floor,
   often at arm's length, so state has to read as colour before it reads as
   text. Opt-in by class and used only inside the Supplier returns UI; the
   divergence is the section, not the component. */
.hub-app .hub-badge { font-size: .8125rem; font-weight: 600; padding: .4em .7em; }
.hub-app .hub-badge .bi { font-size: .95em; }
.hub-app .hub-badge-lg { font-size: .9375rem; padding: .45em .8em; }
/* The match-rung chip under a shelf address. Smaller than .hub-badge because it
   is a footnote to the location, not a state anyone reads across the room —
   the address above it is the thing carrying the weight in that cell. */
.hub-app .hub-badge-sm { font-size: .6875rem; font-weight: 600; padding: .25em .5em; }

/* KPI stat cards on the returns index. */
.hub-stat { display: block; text-decoration: none; color: inherit; }
.hub-stat .hub-stat-value { font-size: 1.75rem; font-weight: 700; font-variant-numeric: tabular-nums;
  line-height: 1.15; letter-spacing: -.01em; }
.hub-stat .hub-stat-label { font-size: .8125rem; color: var(--hub-muted); }
.hub-stat:hover .card { border-color: #adb5bd; }

/* ── Scan console ──────────────────────────────────────────────────────────
   Shared by the warehouse-return, customer-return and receive-back consoles so
   the three stop diverging. Input is large for arm's-length reading; counter is
   the one number the operator tracks across the room. */
.hub-scan-input { font-size: 1.15rem; font-family: var(--bs-font-monospace); letter-spacing: .05em; max-width: 360px; }
.hub-scan-counter { font-size: clamp(1.75rem, 4.5vw, 3.25rem); font-weight: 700;
  font-variant-numeric: tabular-nums; line-height: 1.1; white-space: nowrap; }
.hub-scan-feedback { min-height: 28px; }

/* A freshly scanned row announces itself, then settles. */
@keyframes hubRowIn { from { background: #e7f1ff; } to { background: transparent; } }
tr.hub-row-new > td:first-child { box-shadow: inset 3px 0 0 var(--bs-primary); }
tr.hub-row-new > td { animation: hubRowIn 1.6s ease-out 1; }
@media (prefers-reduced-motion: reduce) { tr.hub-row-new > td { animation: none; } }

/* Full-screen scan flash. The label sits on a dark plate (12.6:1) because v1
   rendered white 800-weight text directly on #ffc107 — 1.6:1, unreadable on
   the one screen read from across a warehouse. The word is always carried;
   colour is never the only signal. */
.hub-flash { position: fixed; inset: 0; z-index: 9999; pointer-events: none; opacity: 0;
  display: flex; align-items: center; justify-content: center; transition: opacity 0s linear; }
.hub-flash .hub-flash-label { color: #fff; background: rgba(0, 0, 0, .72); border-radius: 1rem;
  font-weight: 800; font-size: clamp(2.25rem, 10vw, 6.5rem); text-align: center; padding: .25em .5em; }
@media (prefers-reduced-motion: reduce) { .hub-flash { transition: none !important; } }

/* Radio cards for the disposition step — three equal-weight choices that state
   their consequence, instead of ranking peer outcomes by button colour. */
.hub-choice input { position: absolute; opacity: 0; }
.hub-choice label { display: block; height: 100%; cursor: pointer; border: 1px solid var(--hub-border);
  border-radius: .5rem; padding: .75rem .875rem; background: var(--hub-surface); }
.hub-choice input:checked + label { border-color: var(--bs-primary); box-shadow: 0 0 0 2px rgba(13, 110, 253, .2); }
.hub-choice input:focus-visible + label { outline: 2px solid var(--bs-primary); outline-offset: 2px; }

/* Scan views need taller rows — a CTA and stacked detail per line, read at
   arm's length. That is the ONLY difference: same .hub-table, same type scale,
   same header, more vertical room. */
.hub-table.hub-table-tall > :not(caption) > * > * { padding-top: .7rem; padding-bottom: .7rem; }

/* The scanned-product card reuses the Inventory single view's .cat-split, but
   this page has no `drawer` container to query, so — like .inv-page — it keys
   the image-beside-identity split off the viewport instead. */
@media (min-width: 576px) {
  .ret-catalog .cat-split { grid-template-columns: 132px 1fr; }
}

/* ── Customer-return pick rows ─────────────────────────────────────────────
   A choice card is: radio · flag · [heading line, then one labelled row per
   record] · verdict. The flag gets its own column so every heading line starts
   at the same x — read down the list and the order numbers line up. ── */
.cr-flag { width: 24px; }

/* One record inside a pick row: a fixed-width label, then its fields. The
   label column is what keeps ORDER and INVENTORY readable as two records
   rather than as one run-on sentence. */
.cr-rec { display: flex; align-items: baseline; gap: .5rem; }
.cr-rec > span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cr-rec-key {
  flex: 0 0 4.5rem;
  font-size: .6875rem;
  text-transform: uppercase;
  letter-spacing: .04em;
  color: var(--hub-muted);
}

/* Catalog search results: a smaller thumbnail than the scanned-product card,
   because here it is one row of a list rather than the subject of the block. */
.cr-cat-thumb { width: 56px; height: 56px; }

/* ── Potential returns · single ────────────────────────────────────────────
   The window bar's fill is per-row data, so the template passes the number as
   a custom property and the layout stays here. A style="width:..." would be a
   layout rule inlined into a template, which is what the contract forbids and
   what test_hub_list_page_contract guards. */
.hub-window-bar { height: 8px; }
.hub-window-bar .progress-bar { width: var(--hub-window-used, 0%); }
.hub-table .hub-w-rung { width: 132px; }
.hub-table .hub-w-unit { width: 82px; }
.hub-table .hub-w-shelf { width: 124px; }
.hub-deflist { font-size: .8125rem; }

/* ── Potential returns column budget ───────────────────────────────────────
   Generated from POTENTIAL_RETURN_COLUMNS; test_potential_returns.py asserts
   the two stay in step. Same two-tier rule as everywhere else: a base width
   that fits the ~1090px the table gets at 1440, and a wide tier at 1600px+.

   `product` and `actions` are NOT redeclared here — the .hub-w-* classes are
   global to .hub-table, so a second rule with different numbers would resize
   Inventory depending on rule order. Those two columns carry Inventory's
   widths by declaring the same values in Python.

   The two location columns are wider than a shelf code strictly needs, because
   .hub-loc renders the address at 1rem/700 so the warehouse can read it off
   the row while walking rather than opening each line to find out where to go.
*/
.hub-table .hub-w-verdict { width: 130px; }
.hub-table .hub-w-po { width: 100px; }
.hub-table .hub-w-ordered { width: 80px; }
.hub-table .hub-w-sku { width: 96px; }
.hub-table .hub-w-qty { width: 40px; }
.hub-table .hub-w-cost { width: 80px; }
.hub-table .hub-w-cancelled { width: 116px; }
.hub-table .hub-w-onhand { width: 96px; }

@media (min-width: 1600px) {
  .hub-table .hub-w-verdict { width: 148px; }
  .hub-table .hub-w-po { width: 164px; }
  .hub-table .hub-w-ordered { width: 110px; }
  .hub-table .hub-w-sku { width: 140px; }
  .hub-table .hub-w-qty { width: 56px; }
  .hub-table .hub-w-cost { width: 104px; }
  .hub-table .hub-w-cancelled { width: 168px; }
  .hub-table .hub-w-onhand { width: 120px; }
}

/* ── Returns list column budget ────────────────────────────────────────────
   Declared in app/services/returns_list.py::RETURNS_COLUMNS; same two-tier
   rule as Inventory. Scoped under .ret-list because the shared hub-w-* names
   (select, supplier, actions) carry Inventory's widths — each list page owns
   its own budget. ── */
.ret-list .hub-table .hub-w-ref { width: 96px; }
.ret-list .hub-table .hub-w-supplier { width: 120px; }
.ret-list .hub-table .hub-w-lines { width: 64px; }
.ret-list .hub-table .hub-w-state { width: 156px; }
.ret-list .hub-table .hub-w-value { width: 96px; }
.ret-list .hub-table .hub-w-submitted { width: 96px; }
.ret-list .hub-table .hub-w-age { width: 80px; }
.ret-list .hub-table .hub-w-owner { width: 104px; }
.ret-list .hub-table .hub-w-note { min-width: 164px; }
.ret-list .hub-table .hub-w-actions { width: 32px; }
@media (min-width: 1600px) {
  .ret-list .hub-table .hub-w-ref { width: 112px; }
  .ret-list .hub-table .hub-w-supplier { width: 144px; }
  .ret-list .hub-table .hub-w-lines { width: 72px; }
  .ret-list .hub-table .hub-w-state { width: 172px; }
  .ret-list .hub-table .hub-w-value { width: 112px; }
  .ret-list .hub-table .hub-w-submitted { width: 112px; }
  .ret-list .hub-table .hub-w-age { width: 92px; }
  .ret-list .hub-table .hub-w-owner { width: 128px; }
  .ret-list .hub-table .hub-w-note { min-width: 244px; }
  .ret-list .hub-table .hub-w-actions { width: 36px; }
}

/* ══ Catalog ═══════════════════════════════════════════════════════════════
   Products and Brands, over analytics.dim_product_v2 / dim_brand.

   The Inventory list declares a width per column: 12 columns, 24 rules. That
   does not scale to dim_product_v2's 43, where the same approach is 86 rules
   kept in step with catalog_list.py by hand. Seven tokens cover any number of
   columns, and CssContractTests asserts this set equals SIZES in that module.
   ────────────────────────────────────────────────────────────────────────── */
.hub-table .hub-w-xxs  { width: 56px; }
.hub-table .hub-w-xs   { width: 76px; }
.hub-table .hub-w-sm   { width: 100px; }
.hub-table .hub-w-md   { width: 132px; }
.hub-table .hub-w-lg   { width: 180px; }
.hub-table .hub-w-xl   { width: 260px; }
.hub-table .hub-w-flex { width: 220px; min-width: 220px; }

/* Wide tier. A column may never be narrower here than in the base tier. */
@media (min-width: 1600px) {
  .hub-table .hub-w-xxs  { width: 64px; }
  .hub-table .hub-w-xs   { width: 88px; }
  .hub-table .hub-w-sm   { width: 116px; }
  .hub-table .hub-w-md   { width: 152px; }
  .hub-table .hub-w-lg   { width: 208px; }
  .hub-table .hub-w-xl   { width: 300px; }
  .hub-table .hub-w-flex { width: 320px; min-width: 320px; }
}

/* Horizontal overflow scoped to the table — the page body never scrolls
   sideways. The default preset fits the 1090px floor with no scrollbar. */
.hub-tablescroll { overflow-x: auto; }

/* Under table-layout:fixed a 100%-wide table divides the container between its
   columns, so once the declared widths exceed it every column is squeezed and
   the flexing one collapses to zero. hub-wide switches the table to its natural
   width so the declared widths hold and the scroller does its job. The view
   adds the class when the preset is over FIT_BUDGET — one number, one place. */
.hub-tablescroll .hub-table.hub-wide { width: max-content; }
/* Under the budget the table fills the card and the flexing column takes the
   slack, exactly as the Inventory table behaves today. */
.hub-table:not(.hub-wide) .hub-w-flex { width: auto; }

/* Identity stays on screen while the rest scrolls; without it, scrolling out to
   intercars_sku loses which product the row is.

   Applied ONLY in hub-wide mode. Sticky offsets assume the declared widths are
   the rendered widths, which is true at natural width and false when a
   100%-wide fixed-layout table redistributes its slack — there the pinned cell
   lands ~16px off and covers its neighbour. The pinned pair is always ID
   (hub-w-xs) then the identity column, so the offset is a constant. */
.hub-table .hub-stick { position: sticky; background: var(--hub-surface); }
.hub-table tbody tr:hover .hub-stick { background: #f4f6f7; }
.hub-table thead .hub-stick { background: #f7f8f9; z-index: 3; }
.hub-table .hub-stick-1 { left: 0; z-index: 1; }
.hub-table .hub-stick-2 { left: 76px; z-index: 1; }
@media (min-width: 1600px) { .hub-table .hub-stick-2 { left: 88px; } }
.hub-table .hub-stick-2::after {
  content: ""; position: absolute; top: 0; right: 0; bottom: 0;
  width: 1px; background: var(--hub-border);
}
.hub-tablescroll.is-scrolled .hub-stick-2::after { box-shadow: 3px 0 8px rgba(0, 0, 0, .14); }

/* Fill meter in the Columns picker. This section exists to find what needs
   cleaning, so the picker states how populated a column is before you spend a
   slot on it. Figures come from pg_stats — counting nulls across 1.29M rows is
   ~3 s per column, pg_stats is free. */
.hub-fill { display: inline-block; width: 36px; height: 4px; border-radius: 2px;
  background: #dee2e6; vertical-align: middle; }
/* The width RULE lives here; only the value comes from the template, as a
   custom property. A style="width:" would put layout back in the markup. */
.hub-fill i { display: block; height: 100%; border-radius: 2px; background: #5c8a9e;
  width: var(--hub-fill, 0%); }
.hub-fill.is-low i { background: #c98a2e; }
.hub-fill.is-empty { background: #f1c7c4; }
.hub-fill-pct { display: inline-block; min-width: 38px; text-align: right; }
.hub-colgroup { font-size: .6875rem; text-transform: uppercase; letter-spacing: .04em;
  color: var(--hub-muted); font-weight: 600; padding: .5rem .25rem .15rem; }
/* A value the table does not hold. Distinct from an empty cell, which reads as
   "nothing to say" — this says "we should know this and do not". */
.hub-missing { color: #9aa1a8; }

/* Catalog single view. Same grid rules as .inv-grid: a container query inside
   the drawer, the matching media query on the page. */
.cat-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; }
@container drawer (min-width: 780px) { .cat-grid { grid-template-columns: 1.75fr 1fr; } }
.cat-page .cat-grid { grid-template-columns: 1fr; }
@media (min-width: 992px) { .cat-page .cat-grid { grid-template-columns: 1.75fr 1fr; } }

/* A reading width, not a full-bleed table — the same rule and the same token as
   .inv-page.inv-single, so the two single views cannot drift apart. It needs
   BOTH classes: the drawer body carries .cat-single without .cat-page, so
   inside the panel the container query still governs and nothing caps twice. */
.cat-page.cat-single { max-width: var(--hub-fixed-width, 1240px); }

.cat-raw-scroll { max-height: 340px; overflow: auto; }
.cat-alias-label { font-size: .6875rem; text-transform: uppercase; letter-spacing: .04em;
  color: var(--hub-muted); font-weight: 600; }
