/* Lasting Perfume — Catalog Operations
 * Single stylesheet for the whole administration interface.
 *
 * Constraints this file must respect:
 *  - The approval surface sends `Content-Security-Policy: ... style-src 'self'`, so there
 *    are no inline styles and no <style> blocks anywhere in the templates. Everything is
 *    here.
 *  - No JavaScript is used by the interface at all. Disclosure behaviour is native
 *    <details>/<summary>. Keep it that way; the CSP has no script-src of its own and the
 *    pages must stay light.
 *  - Colour is never the only carrier of meaning. Every badge and every decision control
 *    also differs by text, weight, or border treatment.
 */

/* ---------------------------------------------------------------- tokens */

:root {
  color-scheme: light;

  /* Brand — restrained. Ink plum for structure, warm gold used sparingly as an accent. */
  --brand-ink: #2b1f33;
  --brand-ink-soft: #43354d;
  --brand-accent: #a07d3e;
  --brand-accent-soft: #f6efe2;

  /* Neutrals */
  --bg: #f4f4f6;
  --surface: #ffffff;
  --surface-sunken: #fafafb;
  --border: #dedde3;
  --border-strong: #c6c4ce;
  --text: #23222a;
  --text-muted: #5d5b68;
  --text-faint: #75737f;

  /* Semantics — all foreground pairs meet at least 4.5:1 on their own background. */
  --ok-bg: #e3f3e8;
  --ok-fg: #1c5c33;
  --ok-line: #9dcdaf;
  --warn-bg: #fbf0d6;
  --warn-fg: #6d4c07;
  --warn-line: #ddbf78;
  --danger-bg: #fbe4e4;
  --danger-fg: #8a1f1f;
  --danger-line: #e0a2a2;
  --info-bg: #e5edf8;
  --info-fg: #1f4676;
  --info-line: #a5bedd;
  --neutral-bg: #ececef;
  --neutral-fg: #43414c;
  --neutral-line: #cbc9d2;

  --radius: 10px;
  --radius-sm: 6px;
  --shadow-sm: 0 1px 2px rgba(24, 20, 30, .06);
  --shadow-md: 0 1px 3px rgba(24, 20, 30, .08), 0 6px 16px -8px rgba(24, 20, 30, .18);

  --sidebar-w: 15.5rem;
  --page-max: 78rem;

  --font: "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, "Cascadia Mono", Consolas, "Liberation Mono", monospace;
}

/* ------------------------------------------------------------- base reset */

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.55;
  color: var(--text);
  background: var(--bg);
  overflow-wrap: break-word;
}

h1, h2, h3, h4 { margin: 0; line-height: 1.25; font-weight: 650; color: var(--brand-ink); }
h1 { font-size: 1.5rem; letter-spacing: -.01em; }
h2 { font-size: 1.125rem; }
h3 { font-size: .975rem; }
h4 { font-size: .875rem; }

p { margin: 0 0 .85rem; }
p:last-child { margin-bottom: 0; }

a { color: #1f4676; text-decoration-thickness: 1px; text-underline-offset: 2px; }
a:hover { color: #143255; }

code, pre, samp { font-family: var(--mono); font-size: .8125rem; }
code { background: var(--surface-sunken); border: 1px solid var(--border); border-radius: 4px; padding: .05em .3em; overflow-wrap: anywhere; }
/* A stored payload, a hash or a gid has no break opportunity in it, so it sets the
 * min-content width of whatever holds it. `overflow-x: auto` alone does not stop that —
 * the element still *reports* the wide intrinsic size to its parent — which is how an
 * expanded Technical details section could push the whole document sideways on a narrow
 * screen. `max-width` plus a zero minimum keeps the scrolling inside this element, where
 * it belongs, and the full value is preserved for audit either way. */
pre {
  margin: 0; padding: .85rem 1rem; background: var(--surface-sunken);
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  overflow-x: auto; max-height: 30rem; line-height: 1.5;
  max-width: 100%; min-width: 0;
}
pre code { background: none; border: 0; padding: 0; }

hr { border: 0; border-top: 1px solid var(--border); margin: 1.5rem 0; }

:focus-visible {
  outline: 3px solid #2f6fb5;
  outline-offset: 2px;
  border-radius: 3px;
}

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

.skip-link {
  position: absolute; left: .5rem; top: -4rem; z-index: 100;
  background: var(--surface); color: var(--text); font-weight: 600;
  padding: .6rem 1rem; border: 2px solid var(--brand-ink); border-radius: var(--radius-sm);
  transition: top .12s ease-out;
}
.skip-link:focus { top: .5rem; }

/* ------------------------------------------------------------------ shell */

.app { min-height: 100vh; display: flex; flex-direction: column; }

.app-header {
  position: sticky; top: 0; z-index: 20;
  background: var(--brand-ink);
  color: #f3f0f5;
  border-bottom: 1px solid #1c1422;
}
.app-header__bar {
  display: flex; align-items: center; gap: 1rem; flex-wrap: wrap;
  padding: .7rem 1.25rem;
}
.brand { display: flex; align-items: baseline; gap: .55rem; text-decoration: none; color: inherit; }
.brand:hover { color: inherit; }
.brand__mark {
  font-weight: 700; font-size: 1.0625rem; letter-spacing: -.01em; color: #fff;
}
.brand__sub {
  font-size: .75rem; letter-spacing: .09em; text-transform: uppercase;
  color: #c9bdd4; font-weight: 600;
}
.brand:hover .brand__mark { text-decoration: underline; text-underline-offset: 3px; }

.header-meta { margin-left: auto; display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }

.env-tag {
  font-size: .6875rem; font-weight: 700; letter-spacing: .07em; text-transform: uppercase;
  padding: .25rem .5rem; border-radius: 999px; white-space: nowrap;
  border: 1px solid transparent;
}
.env-tag--production { background: var(--brand-accent); color: #241a06; border-color: #c99f57; }
.env-tag--nonproduction { background: #4c3d58; color: #f0e8f5; border-color: #6a5878; }

.header-stat {
  display: inline-flex; align-items: baseline; gap: .35rem;
  font-size: .75rem; color: #d6cddd; white-space: nowrap;
  padding: .25rem .55rem; border: 1px solid #4b3d57; border-radius: 999px;
}
.header-stat strong { color: #fff; font-size: .8125rem; }
.header-stat--alert { border-color: var(--brand-accent); }
/* The demonstration-data marker, in the header's own dark palette. Dashed and quiet, so it
 * is not read as the alert treatment on the count beside it, but still unmissable — it is
 * the one thing that stops a seeded figure being taken for the real catalog. */
.header-stat--preview {
  border-style: dashed; border-color: #8d7a9b; color: #e6dcec; font-weight: 600;
  gap: .3rem;
}
.header-stat__icon { font-size: .75rem; line-height: 1; }

.header-user { font-size: .8125rem; color: #cfc4d7; }
.signout { display: inline; margin: 0; }
.signout button {
  background: none; border: 1px solid #5b4b69; color: #e6dfeb;
  font: inherit; font-size: .8125rem; padding: .25rem .7rem;
  border-radius: var(--radius-sm); cursor: pointer;
}
.signout button:hover { background: #3b2d47; border-color: #7a678a; color: #fff; }

.app-body { display: flex; flex: 1; align-items: flex-start; }

/* Sidebar */

.sidebar {
  flex: 0 0 var(--sidebar-w); width: var(--sidebar-w);
  align-self: stretch;
  background: var(--surface);
  border-right: 1px solid var(--border);
  padding: 1.15rem .75rem 2.5rem;
}
.nav-group { margin-bottom: 1.15rem; }
.nav-group:last-child { margin-bottom: 0; }
.nav-group__title {
  font-size: .6875rem; font-weight: 700; letter-spacing: .09em; text-transform: uppercase;
  color: var(--text-faint); padding: 0 .65rem; margin: 0 0 .35rem;
}
.nav-list { list-style: none; margin: 0; padding: 0; }
.nav-link {
  display: flex; align-items: center; gap: .5rem;
  padding: .42rem .65rem; margin-bottom: 1px;
  border-radius: var(--radius-sm);
  color: var(--text); text-decoration: none;
  font-size: .875rem; border-left: 3px solid transparent;
}
.nav-link:hover { background: var(--surface-sunken); color: var(--brand-ink); }
.nav-link[aria-current="page"] {
  background: var(--brand-accent-soft);
  border-left-color: var(--brand-accent);
  color: var(--brand-ink);
  font-weight: 650;
}
.nav-link--sub { padding-left: 1.5rem; font-size: .8125rem; color: var(--text-muted); }
.nav-link--sub[aria-current="page"] { color: var(--brand-ink); }
.nav-count {
  margin-left: auto; font-size: .6875rem; font-weight: 700;
  min-width: 1.4rem; text-align: center;
  padding: .08rem .35rem; border-radius: 999px;
  background: var(--neutral-bg); color: var(--neutral-fg); border: 1px solid var(--neutral-line);
}
.nav-count--attention { background: var(--warn-bg); color: var(--warn-fg); border-color: var(--warn-line); }

/* Mobile navigation.
 *
 * The nav is rendered twice from one Jinja macro: once in the desktop sidebar and once
 * inside a native <details> disclosure in the header. Exactly one of the two is displayed
 * at any width, so the hidden copy is also out of the accessibility tree and there is
 * never a duplicate landmark. This avoids depending on overriding the UA's
 * details-content hiding, which is not reliably overridable across engines. */
.nav-toggle { display: none; flex: 1 1 100%; }
.nav-toggle > summary {
  list-style: none; cursor: pointer;
  display: flex; align-items: center; gap: .5rem;
  font-size: .8125rem; font-weight: 650; color: #e9e2ee;
  padding: .35rem .75rem; border: 1px solid #5b4b69; border-radius: var(--radius-sm);
}
.nav-toggle > summary::-webkit-details-marker { display: none; }
.nav-toggle > summary::after { content: "▾"; font-size: .75rem; }
.nav-toggle[open] > summary::after { content: "▴"; }
.nav-toggle > summary:hover { background: #3b2d47; }
.nav-toggle .sidebar {
  width: auto; flex: none; margin-top: .5rem;
  background: #372943; border: 1px solid #55445f; border-radius: var(--radius-sm);
  padding: .6rem .5rem;
}
.nav-toggle .nav-group__title { color: #b7a8c2; }
.nav-toggle .nav-link { color: #eee8f2; }
.nav-toggle .nav-link:hover { background: #4a3958; color: #fff; }
.nav-toggle .nav-link[aria-current="page"] { background: #55415f; color: #fff; border-left-color: var(--brand-accent); }
.nav-toggle .nav-link--sub { color: #cfc2d8; }
.nav-toggle .nav-count { background: #4a3958; color: #f0e9f4; border-color: #6b5779; }
.nav-toggle .nav-count--attention { background: var(--warn-bg); color: var(--warn-fg); border-color: var(--warn-line); }

/* Main */

.main { flex: 1 1 auto; min-width: 0; padding: 1.5rem 1.5rem 4rem; }
.page { max-width: var(--page-max); margin: 0 auto; }

.breadcrumb { font-size: .8125rem; color: var(--text-muted); margin: 0 0 .5rem; }
.breadcrumb ol { list-style: none; display: flex; flex-wrap: wrap; gap: .3rem; margin: 0; padding: 0; }
.breadcrumb li::after { content: "/"; margin-left: .3rem; color: var(--border-strong); }
.breadcrumb li:last-child::after { content: ""; }
.breadcrumb [aria-current="page"] { color: var(--text); font-weight: 600; }
/* Breadcrumb links are small text by design, but they are still links people tap. Pad the
 * hit area vertically without changing the line's visual height. */
.breadcrumb a { display: inline-block; padding: .2rem 0; }

.page-head {
  display: flex; align-items: flex-start; justify-content: space-between;
  gap: 1rem; flex-wrap: wrap; margin-bottom: 1.15rem;
}
.page-head__text { min-width: 0; }
.page-head__lede { margin: .35rem 0 0; color: var(--text-muted); font-size: .875rem; max-width: 62ch; }
.page-head__actions { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }

.section { margin-top: 1.5rem; }
.section__head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: .75rem; flex-wrap: wrap; margin-bottom: .6rem;
}
.section__note { margin: 0; font-size: .8125rem; color: var(--text-muted); }

/* ------------------------------------------------------------------ cards */

.card {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); box-shadow: var(--shadow-sm);
  margin-bottom: 1rem;
}
.card:last-child { margin-bottom: 0; }
.card__head {
  padding: .75rem 1.1rem; border-bottom: 1px solid var(--border);
  display: flex; align-items: baseline; justify-content: space-between; gap: .75rem; flex-wrap: wrap;
}
.card__head p { margin: 0; font-size: .8125rem; color: var(--text-muted); }
.card__body { padding: 1.1rem; }
.card__body > :first-child { margin-top: 0; }
.card__foot {
  padding: .7rem 1.1rem; border-top: 1px solid var(--border);
  background: var(--surface-sunken); border-radius: 0 0 var(--radius) var(--radius);
  font-size: .8125rem; color: var(--text-muted);
}
.card--flush .card__body { padding: 0; }
.card--accent { border-left: 4px solid var(--brand-accent); }
.card--danger { border-left: 4px solid var(--danger-line); }
.card--safety { border-left: 4px solid var(--info-line); }

/* The dashboard's card row.
 *
 * This was `repeat(auto-fit, minmax(20rem, 1fr))`, which packed four cards into three ~364px
 * columns on a 1177px canvas. Every table inside them then overflowed into its own
 * horizontal scrollbar — the run history needed 538px — while the page had width to spare,
 * and product titles in the conflicts card wrapped to roughly one word per line, burying
 * the identity the operator is meant to scan.
 *
 * Cards now stack full-width until there is genuinely room for two, then pair on a
 * 12-column grid: the card carrying product titles or the widest table takes 7, its
 * companion takes 5. Auto-fit is not used because it cannot know which card needs the
 * width; the templates do, and they say so with --wide / --narrow.
 */
.card-grid { display: grid; gap: 1rem; grid-template-columns: minmax(0, 1fr); align-items: start; }

@media (min-width: 75rem) {
  .card-grid { grid-template-columns: repeat(12, minmax(0, 1fr)); }
  .card-grid > * { grid-column: span 12; }
  .card-grid > .card--wide { grid-column: span 7; }
  .card-grid > .card--narrow { grid-column: span 5; }
}

/* ------------------------------------------------------------- stat tiles */

.stat-grid {
  display: grid; gap: .75rem;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  margin: 0;
}
.stat {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: var(--radius); padding: .8rem .9rem; box-shadow: var(--shadow-sm);
  min-width: 0;
}
.stat__label {
  margin: 0; font-size: .75rem; font-weight: 600; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: .05em;
}
.stat__value {
  margin: .3rem 0 0; font-size: 1.5rem; font-weight: 680; line-height: 1.15;
  color: var(--brand-ink); font-variant-numeric: tabular-nums;
}
.stat__value--text { font-size: .9375rem; font-weight: 600; line-height: 1.35; }
.stat__meta { margin: .25rem 0 0; font-size: .75rem; color: var(--text-faint); }
.stat--link { display: block; text-decoration: none; color: inherit; }
.stat--link:hover { border-color: var(--border-strong); box-shadow: var(--shadow-md); color: inherit; }
.stat--attention { border-left: 4px solid var(--brand-accent); }
.stat--danger { border-left: 4px solid var(--danger-line); }
.stat--ok { border-left: 4px solid var(--ok-line); }
/* The one figure on an execution screen that decides what happens next. "Approved" and
   "Ready to send" sit side by side there and are routinely different numbers, so the one
   that governs is weighted rather than left for the reader to work out. */
.stat--key { border-left: 4px solid var(--brand-accent); background: var(--surface-sunken); }
.stat--key .stat__value { font-size: 1.75rem; }

/* A GraphQL variables block. Pre-formatted and scrollable in its own right: a mutation
   preview must be readable verbatim, and it must never widen the page to achieve that. */
.payload {
  margin: .5rem 0 0; padding: .7rem .8rem; overflow-x: auto;
  background: var(--surface-sunken); border: 1px solid var(--border);
  border-radius: var(--radius-sm); font-size: .8125rem; line-height: 1.5;
  white-space: pre; -webkit-overflow-scrolling: touch;
}

/* ------------------------------------------------------- manual data refresh
 * Sits directly under the freshness tiles, because it is what an operator does about what
 * those tiles say. Styled as a working panel rather than a card: it carries state (last
 * success, what is happening now, whether writes are armed) and the controls that act on
 * it, and the two have to be read together — a button labelled "refresh" with the state
 * somewhere else is how someone presses it twice.
 */
.refresh {
  margin-top: .75rem; padding: .95rem 1.1rem;
  background: var(--surface); border: 1px solid var(--border);
  border-left: 4px solid var(--brand-accent);
  border-radius: var(--radius); box-shadow: var(--shadow-sm);
}
.refresh__head { margin-bottom: .8rem; }
.refresh__title { font-size: 1rem; }
.refresh__note { margin: .2rem 0 0; font-size: .8125rem; color: var(--text-muted); max-width: 68ch; }

.refresh__facts {
  margin: 0 0 .9rem; display: grid; gap: .75rem 1.5rem;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  padding-bottom: .85rem; border-bottom: 1px solid var(--border);
}
.refresh__facts > div { min-width: 0; }
.refresh__facts dt {
  font-size: .6875rem; font-weight: 700; letter-spacing: .05em; text-transform: uppercase;
  color: var(--text-muted); margin-bottom: .2rem;
}
.refresh__facts dd { margin: 0; font-size: .875rem; font-weight: 600; min-width: 0; }
.refresh__facts .cell-sub { font-weight: 400; }

/* Capped rather than fluid: with one scope available — a deployment whose Shopify reads are
 * off — a `1fr` track made a single full-width filled button, which reads as the page's
 * main action rather than as one of a set. */
.refresh__actions {
  display: grid; gap: .75rem 1rem;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 22rem));
  align-items: start;
}
.refresh__option { display: flex; flex-direction: column; gap: .3rem; min-width: 0; }
.refresh__option .btn, .refresh__option button { width: 100%; }
.refresh__help { margin: 0; font-size: .75rem; color: var(--text-muted); }
/* Spans the grid: it explains the disabled state above it and must not sit in one column
 * as though it belonged to a single button. */
.refresh__blocked {
  grid-column: 1 / -1; margin: 0; font-size: .8125rem; font-weight: 600;
  color: var(--info-fg); background: var(--info-bg);
  border: 1px solid var(--info-line); border-radius: var(--radius-sm); padding: .55rem .7rem;
}

/* A count block that is not itself clickable still has to say where its detail lives.
 * On these four screens the filters that produce each figure sit directly beneath it, so
 * the route in is adjacent rather than hidden — this names it instead of leaving the
 * operator to infer the connection. */
.stats__note { margin: 0 0 .6rem; font-size: .8125rem; color: var(--text-muted); }

/* Legacy `.stats` blocks (Phase 2–5 pages) reuse the tile look. */
.stats { display: grid; gap: .75rem; grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr)); margin: 0; }
/* Two markup shapes are in use and both must render identically: `<dt>/<dd>` pairs, and a
 * `<strong>` label followed by a bare text value. The container therefore carries the
 * value styling and `<strong>`/`<dt>` override it back down for the label. */
.stats > div, .stats > .stat-tile {
  background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius);
  padding: .7rem .8rem; box-shadow: var(--shadow-sm); min-width: 0;
  font-size: 1.25rem; font-weight: 650; color: var(--brand-ink);
  font-variant-numeric: tabular-nums; line-height: 1.3;
}

/* A glance figure that can be inspected says so, and behaves like the link it is.
 * These were static cards: the operator read "Out of stock at supplier 35" and had no way
 * to reach those 35 rows. Each linked tile now opens the same list, filtered by the same
 * column and value the figure was counted from. The affordance is deliberate rather than
 * decorative — the tile lifts on hover, keeps a visible keyboard focus ring, and carries a
 * "View products" line so the click target is obvious without hovering. */
a.stat, a.stat-tile {
  display: block; text-decoration: none; position: relative;
  transition: border-color .12s ease, box-shadow .12s ease, transform .12s ease;
}
a.stat:hover { border-color: var(--brand-ink); box-shadow: var(--shadow-md); }
a.stat:focus-visible { outline: 3px solid #2f6fb5; outline-offset: 2px; }
a.stat-tile:hover {
  border-color: var(--brand-ink); box-shadow: var(--shadow-md, 0 4px 12px rgb(0 0 0 / .1));
  transform: translateY(-1px);
}
a.stat-tile:focus-visible { outline: 3px solid #2f6fb5; outline-offset: 2px; }
.stat-tile__go {
  display: block; margin-top: .45rem; font-size: .75rem; font-weight: 600;
  color: var(--brand-ink); text-decoration: underline;
}
a.stat-tile:hover .stat-tile__go { text-decoration: none; }

.stats dt, .stats > div strong, .stats > .stat-tile strong {
  font-size: .6875rem; font-weight: 600; color: var(--text-muted);
  text-transform: uppercase; letter-spacing: .05em; display: block;
  line-height: 1.4; margin-bottom: .25rem;
}
.stats > div .stat__meta, .stats > .stat-tile .stat__meta {
  font-size: .75rem; font-weight: 400; color: var(--text-faint);
}
.stats dd {
  margin: .25rem 0 0; font-size: 1.25rem; font-weight: 650; color: var(--brand-ink);
  font-variant-numeric: tabular-nums;
}

/* ----------------------------------------------------------------- badges */

/* The one status chip, used by every screen through the `pill`/`badge`/`flag` macros in
 * `_ui.html`. Nothing else in the application emits `class="badge"`.
 *
 * Geometry, and why each part of it is here:
 *
 *  - `align-items: flex-start` with the glyph in its own `flex: 0 0 auto` slot pins the
 *    marker to the top-left corner. Previously the glyph was a `::before` and the label was
 *    an anonymous flex item; an anonymous item cannot be given `min-width: 0`, so the only
 *    way to stop a narrow status column overflowing was `flex-wrap: wrap` — and in the
 *    dashboard's 49px status column the two items duly wrapped, producing a 49x69 tower
 *    with the tick centred above the word. That tower is the "large circular" indicator in
 *    the owner's screenshots, and it is what this markup change removes.
 *  - The label is `nowrap` by default, so a chip is one line and sizes to its content. A
 *    table that cannot give it that width scrolls inside its own container instead of
 *    crushing the chip; the scrolling is visible and recoverable, a squashed chip is not.
 *  - `min-height` fixes the chip's height independently of its label, so a column of them
 *    aligns and no chip is taller than the row needs.
 */
.badge {
  display: inline-flex; align-items: flex-start; justify-content: flex-start;
  gap: .3rem; flex-wrap: nowrap; text-align: left; max-width: 100%;
  font-size: .75rem; font-weight: 650; line-height: 1.35; min-height: 1.375rem;
  padding: .1rem .5rem; border-radius: 999px;
  border: 1px solid var(--neutral-line); background: var(--neutral-bg); color: var(--neutral-fg);
  vertical-align: top;
}
/* The marker never wraps, never shrinks, and never leaves the left edge. */
.badge__icon { flex: 0 0 auto; font-size: .8125rem; line-height: 1.35; font-weight: 700; }
/* The label is the only part allowed to give way, which is what keeps the chip inside its
 * cell without the whole chip collapsing into a column. */
.badge__text { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* Opt-in wrapping for a genuinely long free-form phrase, where one line is not realistic.
 * The glyph still stays at the top-left; only the sentence flows. */
.badge--wrap .badge__text { white-space: normal; overflow: visible; overflow-wrap: anywhere; }

.badge--ok      { background: var(--ok-bg);      color: var(--ok-fg);      border-color: var(--ok-line); }
.badge--warn    { background: var(--warn-bg);    color: var(--warn-fg);    border-color: var(--warn-line); }
.badge--danger  { background: var(--danger-bg);  color: var(--danger-fg);  border-color: var(--danger-line); }
.badge--info    { background: var(--info-bg);    color: var(--info-fg);    border-color: var(--info-line); }
.badge--neutral { background: var(--neutral-bg); color: var(--neutral-fg); border-color: var(--neutral-line); }

/* Severity is a different kind of fact from workflow status, and a conflict row shows both
 * at once. A square-cornered chip with a level marker (▲ △ ■ ▪) reads as a scale; the
 * rounded chip beside it reads as a state. The distinction therefore survives without
 * colour, which matters because Critical and Rejected share the danger palette. */
.badge--level {
  border-radius: var(--radius-sm); font-variant: small-caps; letter-spacing: .02em;
}
.badge--level .badge__icon { font-size: .6875rem; }

/* Demonstration data. Not a warning — a dashed, quiet chip that still cannot be mistaken
 * for a real value's status. Never rendered in production. */
.badge--preview {
  background: var(--brand-accent-soft); color: #5c4517;
  border: 1px dashed #d3bd94; font-weight: 600;
}

.tag {
  display: inline-block; margin: .1rem .2rem .1rem 0;
  font-size: .75rem; padding: .1rem .5rem; border-radius: var(--radius-sm);
  background: var(--brand-accent-soft); color: #5c4517; border: 1px solid #e3d4b6;
}

/* The legacy `.status` span is gone. It was a second, differently-shaped chip left over
 * from the Phase 2–5 templates, and no template emits it any more — every status in the
 * application now comes from the `badge` component above. The rules are removed rather than
 * kept "just in case", because a spare chip style is exactly how two treatments end up on
 * screen at once. `tests/test_ui_status_component.py` fails if one reappears. */

/* ----------------------------------------------------------------- alerts */

.alerts { margin: 0 0 1rem; }
.alert {
  display: flex; gap: .6rem; align-items: flex-start;
  padding: .7rem .9rem; margin: 0 0 .5rem;
  border: 1px solid var(--info-line); border-left-width: 4px;
  background: var(--info-bg); color: var(--info-fg);
  border-radius: var(--radius-sm); font-size: .875rem;
}
.alert:last-child { margin-bottom: 0; }
.alert__label { font-weight: 700; white-space: nowrap; }
/* The old single-purpose class kept working so nothing regresses if it survives. */
.message {
  padding: .7rem .9rem; margin: 0 0 .5rem;
  border: 1px solid var(--info-line); border-left-width: 4px;
  background: var(--info-bg); color: var(--info-fg);
  border-radius: var(--radius-sm); font-size: .875rem;
}

.notice {
  padding: .7rem .9rem; border-radius: var(--radius-sm); font-size: .8125rem;
  border: 1px solid var(--border); background: var(--surface-sunken); color: var(--text-muted);
}
.notice--safety { border-color: var(--info-line); background: var(--info-bg); color: var(--info-fg); }
.notice--warn { border-color: var(--warn-line); background: var(--warn-bg); color: var(--warn-fg); }
.notice strong { color: inherit; }

/* The evidence contradiction. Deliberately heavier than `notice--warn`: it is the one
   thing on the page that changes what the reviewer should do, so it is sized like a
   section rather than an aside, and it carries a shape (the left rule plus the heading)
   as well as a colour. */
.notice--contradiction {
  border: 1px solid var(--danger-line); border-left-width: 5px;
  background: var(--danger-bg); color: var(--danger-fg);
  padding: .9rem 1.05rem; margin: 0 0 1rem; font-size: .875rem;
}
.notice--contradiction .notice__title {
  margin: 0 0 .45rem; font-size: 1rem; font-weight: 650; color: inherit;
  display: flex; gap: .45rem; align-items: baseline;
}
.notice--contradiction .notice__title::before { content: "\26A0"; font-size: 1.05em; }
.notice--contradiction p { margin: 0 0 .5rem; }
.notice__list { margin: .5rem 0; padding-left: 1.15rem; display: grid; gap: .3rem; }
.notice__list li { color: inherit; }
.notice__foot { margin: .55rem 0 0 !important; font-weight: 600; }

/* A single field whose value disagrees with the proposal above it. Colour alone would not
   carry this, so the value also gets a rule and the hint states the disagreement. */
.field-pair--warn > dd {
  border-left: 3px solid var(--warn-line); padding-left: .5rem;
}
.field-pair--warn > dd > .cell-sub { color: var(--warn-fg); font-weight: 600; }

/* ----------------------------------------------------------------- tables */

.table-wrap {
  overflow-x: auto; -webkit-overflow-scrolling: touch;
  border-radius: var(--radius);
  /* Positioned so that absolutely-positioned descendants — `.visually-hidden` text in a
   * header cell or a caption — resolve against this scroll container rather than the
   * initial containing block. Without it such an element is placed at the *table's* full
   * width, escapes the scroller, and drags the whole page into horizontal scroll on a
   * narrow screen even though nothing is visibly overflowing. */
  position: relative;
}
.card--flush .table-wrap { border-radius: 0 0 var(--radius) var(--radius); }

/* The record viewport.
 *
 * A 25-row table put its horizontal scrollbar below row 25. Reading a column on the right
 * meant scrolling most of a page down, dragging the bar, and scrolling back up — for every
 * record. The fix is to bound the *rows*, not the page: the container keeps roughly five
 * to six desktop rows visible, scrolls vertically inside itself for the rest, and so the
 * horizontal scrollbar sits directly beneath the rows the operator is looking at.
 *
 * One container scrolls in both directions, which is what keeps the horizontal position
 * while moving down the rows: scrolling to rows 7-12 does not reset the columns, because
 * nothing was re-laid-out — it is the same scroll box. Splitting the rows into per-page
 * scrollers would give six horizontal bars and lose the column position at every boundary,
 * which is explicitly not what is wanted here.
 *
 * `max-height` rather than `height` is deliberate: a table with three rows is shorter than
 * the bound and gets no scrollbar at all, so the same class is safe to put on every record
 * list without giving small ones a scrollbar they do not need.
 *
 * The bound is `min()` of a fixed height and a share of the window, for two different
 * failure modes. The rem term keeps a wide desktop from showing twenty rows and putting the
 * horizontal bar back out of reach; the vh term keeps a short window — a 768px-high laptop,
 * a browser with three toolbars — from giving the table the whole screen and pushing the
 * pager below the fold. Roughly six single-line rows fit the rem term, and a dense two-line
 * row (product identity plus identifiers) gives four; both leave the bar directly under the
 * rows, which is the point.
 */
:root { --table-viewport: min(30rem, 60vh); }

.table-wrap--viewport {
  max-height: var(--table-viewport);
  overflow: auto;
  overscroll-behavior: contain;
}

/* The header has to stay while the rows move under it, or the columns lose their names two
 * rows into the scroll. `box-shadow` draws the rule instead of `border-bottom`, because a
 * border on a sticky cell is painted with the cell and separates from it while scrolling. */
.table-wrap--viewport thead th {
  position: sticky; top: 0; z-index: 3;
  border-bottom: 0; box-shadow: inset 0 -1px 0 var(--border-strong);
}

/* A floor under the identity column.
 *
 * `table { width: 100% }` with automatic layout divides the available width by content, so
 * on a narrow canvas a seven-column table gave the product title about 60px and wrapped
 * "Hugo Boss Bottled Eau de Toilette 100ml" to one word per line — the identity the
 * operator is scanning for, rendered least legibly of anything on the row. A minimum on the
 * first column pushes the table past its container instead, and the container scrolls: a
 * horizontal scrollbar the operator can see and use, directly under the rows, is a far
 * better trade than a column of single words. */
.table-wrap--viewport thead th:first-child,
.table-wrap--viewport tbody td:first-child { min-width: 14rem; }
.table-wrap--viewport tbody td.empty { min-width: 0; }

@media (max-width: 47.99em) {
  .table-wrap--viewport thead th:first-child,
  .table-wrap--viewport tbody td:first-child { min-width: 11rem; }
}

table { width: 100%; border-collapse: collapse; font-size: .875rem; }
caption { text-align: left; font-size: .8125rem; color: var(--text-muted); padding: 0 0 .5rem; }

thead th {
  text-align: left; font-size: .6875rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; color: var(--text-muted);
  background: var(--surface-sunken);
  padding: .55rem .75rem; border-bottom: 1px solid var(--border-strong);
  white-space: nowrap; vertical-align: bottom;
}
tbody th, tbody td {
  padding: .6rem .75rem; border-bottom: 1px solid var(--border);
  vertical-align: top; text-align: left;
}
tbody th { font-weight: 650; color: var(--brand-ink); }
tbody tr:last-child th, tbody tr:last-child td { border-bottom: 0; }
tbody tr:hover td, tbody tr:hover th { background: var(--surface-sunken); }

.num { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
thead th.num { text-align: right; }
.nowrap { white-space: nowrap; }
.stamp { font-variant-numeric: tabular-nums; font-size: .8125rem; color: var(--text-muted); white-space: nowrap; }
.truncate { display: block; max-width: 26rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cell-sub { display: block; font-size: .75rem; color: var(--text-faint); margin-top: .15rem; overflow-wrap: anywhere; }
/* A sub-line that is a warning rather than context. `.cell-sub` is deliberately faint —
 * it carries identifiers nobody needs to read — so a conflict mark that inherited it
 * would be quieter than the SKU above it. Weight and colour, not colour alone. */
.cell-sub--warn { color: var(--warn-fg); font-weight: 600; }
.cell-main { font-weight: 600; }
td code, th code { overflow-wrap: anywhere; }
.hash { font-size: .75rem; color: var(--text-muted); }
/* A zero in a count grid is meaningful — "none of these" — but must not read as loudly as
 * a real figure, or a wall of zeroes hides the one number that needs attention. */
.zero { color: var(--text-faint); }
.required-mark { font-weight: 500; color: var(--text-muted); }

/* Two or three observed values inside a table cell: compact, still labelled. */
.mini-pairs { margin: 0; display: grid; gap: .1rem; }
.mini-pairs > div { display: flex; gap: .4rem; flex-wrap: wrap; align-items: baseline; }
.mini-pairs dt { font-size: .75rem; color: var(--text-muted); }
.mini-pairs dd { margin: 0; font-size: .8125rem; font-weight: 600; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }

/* The identifiers under an observation's product name. These exist to be read off the
   screen and typed into Shopify, so each is on its own line, labelled, and selectable as a
   whole without dragging across a label. */
.ident-list { margin: .3rem 0 0; display: grid; gap: .1rem; }
.ident-list > div { display: flex; gap: .4rem; flex-wrap: wrap; align-items: baseline; }
.ident-list dt { font-size: .6875rem; color: var(--text-faint); min-width: 3.5rem; }
.ident-list dd { margin: 0; }
code.ident {
  font-size: .75rem; background: var(--surface-sunken); border: 1px solid var(--border);
  border-radius: 4px; padding: 0 .3rem; overflow-wrap: anywhere; user-select: all;
}
/* An identifier in a column of its own is read as one run of characters and compared
 * against a supplier listing digit by digit. Broken across two lines — "36150999999" then
 * a lone "9" — it is no longer that, so it holds one line and the table scrolls instead.
 * The wrapping form above is kept for the identifier *lists*, where a Shopify gid is long
 * enough that one line is not realistic. */
code.ident--cell { white-space: nowrap; overflow-wrap: normal; }
.cell-link { display: inline-block; margin-top: .3rem; font-size: .75rem; }

/* Row-header tables used for evidence ("key on the left, value on the right"). */
table.kv-table tbody th {
  width: 32%; min-width: 12rem; background: var(--surface-sunken);
  font-weight: 600; color: var(--text-muted); font-size: .8125rem;
}
table.kv-table tbody tr:hover th { background: var(--surface-sunken); }

.empty {
  padding: 1.75rem 1.1rem; text-align: center; color: var(--text-muted); font-size: .875rem;
}
.empty strong { display: block; color: var(--text); font-size: .9375rem; margin-bottom: .2rem; }
td.empty { border-bottom: 0; }
/* Somewhere to go from a dead end. */
.empty__link { display: inline-block; margin-top: .5rem; font-weight: 600; }

/* Definition lists */

.kv { margin: 0; display: grid; grid-template-columns: minmax(9rem, 15rem) minmax(0, 1fr); gap: .1rem .9rem; }
.kv dt { font-size: .8125rem; font-weight: 600; color: var(--text-muted); padding: .3rem 0; }
.kv dd { margin: 0; padding: .3rem 0; overflow-wrap: anywhere; }
.kv dt + dd { border-bottom: 1px solid var(--border); }
.kv dt { border-bottom: 1px solid var(--border); }
.kv dt:last-of-type, .kv dd:last-of-type { border-bottom: 0; }

/* Old `.details` lists on the Phase 2/3 detail pages. */
dl.details { margin: 0; display: grid; grid-template-columns: minmax(9rem, 15rem) minmax(0, 1fr); gap: .1rem .9rem; }
dl.details dt { font-weight: 600; font-size: .8125rem; color: var(--text-muted); padding: .3rem 0; border-bottom: 1px solid var(--border); }
dl.details dd { margin: 0; padding: .3rem 0; border-bottom: 1px solid var(--border); overflow-wrap: anywhere; }

/* ---------------------------------------------------------------- buttons */

.btn, button, input[type="submit"] {
  display: inline-flex; align-items: center; justify-content: center; gap: .4rem;
  font: inherit; font-size: .875rem; font-weight: 600; line-height: 1.2;
  padding: .5rem .95rem; border-radius: var(--radius-sm);
  border: 1px solid var(--border-strong); background: var(--surface); color: var(--text);
  cursor: pointer; text-decoration: none; white-space: nowrap;
}
.btn:hover, button:hover, input[type="submit"]:hover {
  background: var(--surface-sunken); border-color: var(--text-faint); color: var(--text);
}
button:disabled, .btn[aria-disabled="true"] { opacity: .55; cursor: not-allowed; }

/* Every modifier below names ``input[type="submit"]`` explicitly.
   Without it the modifiers lose to the base rule above: `.btn--primary` scores (0,1,0)
   against `input[type="submit"]`'s (0,1,1), so a submit input carrying `btn btn--primary`
   kept the neutral surface background and read as a third text field rather than as the
   page's primary action. That is what made Sign in look like an input. WTForms renders
   every SubmitField as `input[type="submit"]`, so this affected the primary action on
   every form in the application, not only login. */
.btn--primary, button.btn--primary, input[type="submit"].btn--primary {
  background: var(--brand-ink); border-color: var(--brand-ink); color: #fff;
}
.btn--primary:hover, button.btn--primary:hover, input[type="submit"].btn--primary:hover {
  background: #3d2c49; border-color: #3d2c49; color: #fff;
}
/* Pressed state: the control moves under the pointer, so a click is felt as well as seen. */
.btn--primary:active, button.btn--primary:active, input[type="submit"].btn--primary:active {
  background: #2b1e34; border-color: #2b1e34; transform: translateY(1px);
}

.btn--approve, button.btn--approve, input[type="submit"].btn--approve {
  background: var(--ok-fg); border-color: var(--ok-fg); color: #fff; font-weight: 700;
}
.btn--approve:hover, button.btn--approve:hover, input[type="submit"].btn--approve:hover {
  background: #164a29; border-color: #164a29; color: #fff;
}

.btn--reject, button.btn--reject, input[type="submit"].btn--reject {
  background: var(--surface); color: var(--danger-fg);
  border: 2px solid var(--danger-fg); font-weight: 700;
}
.btn--reject:hover, button.btn--reject:hover, input[type="submit"].btn--reject:hover {
  background: var(--danger-bg); color: var(--danger-fg); border-color: var(--danger-fg);
}

.btn--ignore { background: var(--surface); color: var(--text-muted); border: 1px solid var(--border-strong); }
.btn--ignore:hover { background: var(--surface-sunken); color: var(--text); }
/* Permanent suppression is the most consequential control in the decision row, so it is
   the only one that carries a heavier dashed edge — a deliberate visual "are you sure"
   that separates it from the reversible Ignore once beside it. */
.btn--ignore-permanent { border-style: dashed; border-width: 2px; }
/* The permanent option is set apart from the three reversible ones, so it is not read as a
   fourth item in a row of similar choices. */
.decision__option--permanent { border-top: 1px solid var(--border); padding-top: .7rem; }
.decision__distinction {
  margin: .7rem 0 0; font-size: .8125rem; color: var(--text-muted);
  border-left: 3px solid var(--border-strong); padding-left: .6rem;
}

/* Keyboard focus has to clear the filled primary background, so it is drawn outside the
   control rather than on its border. */
.btn:focus-visible, button:focus-visible, input[type="submit"]:focus-visible {
  outline: 3px solid #2f6fb5; outline-offset: 2px;
}

.btn--sm { padding: .3rem .6rem; font-size: .8125rem; }
.btn--ghost { background: none; border-color: var(--border); }
.btn--ghost:hover { background: var(--surface-sunken); }

.btn-row { display: flex; gap: .5rem; flex-wrap: wrap; align-items: center; }

.linkset { display: flex; flex-wrap: wrap; gap: .35rem .85rem; font-size: .8125rem; margin: 0; }

/* ------------------------------------------------------------------ forms */

label { display: block; font-size: .8125rem; font-weight: 600; color: var(--text-muted); }

input[type="text"], input[type="password"], input[type="search"], input[type="file"],
input:not([type]), select, textarea {
  font: inherit; font-size: .875rem; width: 100%;
  padding: .45rem .6rem; color: var(--text);
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
}
input[type="file"] { padding: .35rem .5rem; }
select { padding-right: .4rem; }
textarea { min-height: 5rem; resize: vertical; }
input:hover, select:hover, textarea:hover { border-color: var(--text-faint); }
input:focus, select:focus, textarea:focus { border-color: #2f6fb5; }
input[type="checkbox"] { width: 1rem; height: 1rem; accent-color: var(--brand-ink); }

.field { margin-bottom: .85rem; }
.field > label { margin-bottom: .25rem; }
.field__hint { margin: .25rem 0 0; font-size: .75rem; color: var(--text-faint); }
.field__error { margin: .3rem 0 0; font-size: .8125rem; font-weight: 600; color: var(--danger-fg); }
.field__error::before { content: "\2715\00a0"; }

.filters {
  display: flex; flex-wrap: wrap; gap: .65rem .9rem; align-items: flex-end;
  padding: .9rem 1.1rem; background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-sm); margin-bottom: 1rem;
}
.filters label { display: flex; flex-direction: column; gap: .25rem; min-width: 11rem; flex: 1 1 11rem; }
.filters input, .filters select { width: 100%; }
.filters button { flex: 0 0 auto; }
.filters .filters__actions { display: flex; gap: .5rem; align-items: flex-end; }

.upload-form { display: grid; gap: .75rem; max-width: 34rem; }
.upload-form button { justify-self: start; }

/* ------------------------------------------------------------------- tabs */

.tabs {
  display: flex; flex-wrap: wrap; gap: .3rem;
  border-bottom: 1px solid var(--border); margin-bottom: 1rem; padding: 0;
}
.tab {
  display: inline-flex; align-items: center; gap: .4rem;
  padding: .45rem .8rem; font-size: .8125rem; font-weight: 600;
  color: var(--text-muted); text-decoration: none;
  border: 1px solid transparent; border-bottom: 0;
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
  margin-bottom: -1px;
}
.tab:hover { background: var(--surface-sunken); color: var(--text); }
.tab[aria-current="page"] {
  background: var(--surface); border-color: var(--border);
  border-bottom: 1px solid var(--surface);
  color: var(--brand-ink); font-weight: 700;
}
.tab__count {
  font-size: .6875rem; font-weight: 700; padding: .05rem .35rem; border-radius: 999px;
  background: var(--neutral-bg); color: var(--neutral-fg); border: 1px solid var(--neutral-line);
}
/* A zero is worth showing — it is the answer to "is this queue empty, or is the page
 * broken?" — but it must not compete with the tabs that do hold work. */
.tab--empty { color: var(--text-faint); }
.tab--empty .tab__count { background: none; color: var(--text-faint); }

/* --------------------------------------------------------------- paginator */

.pager {
  display: flex; align-items: center; justify-content: space-between;
  gap: .75rem; flex-wrap: wrap;
  padding: .7rem 1.1rem; border-top: 1px solid var(--border);
  background: var(--surface-sunken); font-size: .8125rem; color: var(--text-muted);
  border-radius: 0 0 var(--radius) var(--radius);
}
.pager__links { display: flex; gap: .5rem; }

/* ------------------------------------------------------- approval detail  */

.decision {
  display: grid; gap: .75rem;
  grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
  margin: 0;
}
.decision__option {
  display: flex; flex-direction: column; gap: .35rem;
  padding: .8rem; border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface-sunken);
}
.decision__option button { width: 100%; }
.decision__help { margin: 0; font-size: .75rem; color: var(--text-muted); }

.change-line {
  display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: .1rem .9rem;
  font-variant-numeric: tabular-nums;
}
.change-line dt { padding: .4rem 0; font-size: .8125rem; font-weight: 600; color: var(--text-muted); border-bottom: 1px solid var(--border); }
.change-line dd { margin: 0; padding: .4rem 0; border-bottom: 1px solid var(--border); font-weight: 600; }

.money { font-variant-numeric: tabular-nums; font-weight: 650; }
.money--from { color: var(--danger-fg); }
.money--to { color: var(--ok-fg); }
.arrow { color: var(--text-faint); padding: 0 .25rem; }

/* The product column takes the width first, in any table that has one.
 *
 * Automatic table layout divides width by content, which gave a 650px table three roughly
 * equal columns and left product titles wrapping to four lines — one or two words each —
 * while a status word and a timestamp sat on comfortable single lines beside them. Product
 * identity is what the operator scans to find the row they care about, so it is the column
 * that gets the space, and the columns beside it are held to what they actually need. */
th.col-product { width: 46%; min-width: 13rem; }
th.col-when { width: 1%; white-space: nowrap; }
td .product-identity__title { overflow-wrap: break-word; }

/* The approval queue carries eight columns, which automatic layout cannot divide sensibly:
 * it hands each column its preferred content width, so a timestamp that happens to be wide
 * wins space from the product title, and the table overflows its card on a full desktop.
 * Fixed layout makes the division a decision instead of an accident — the product column is
 * given a quarter of the table outright and the remaining columns share what is left
 * equally, which is enough for a status word, a short money value and a date on two lines. */
/* A floor under the fixed layout.
 * Percentage columns shrink with the table, so on a narrower canvas the 13% status column
 * fell below the width of the chip inside it and the overlap came back. Below this width
 * the table keeps its proportions and `.table-wrap` scrolls it instead — a contained
 * scrollbar the operator can see and use, rather than two columns painted on top of each
 * other. */
.table--queue { table-layout: fixed; width: 100%; min-width: 64rem; }
.table--queue th.col-product { width: 24%; min-width: 0; }
.table--queue td { overflow-wrap: break-word; vertical-align: middle; }
/* Headings wrap in this table, all of them.
 *
 * `thead th` is `nowrap` everywhere else, which is right for an automatic layout: the
 * heading's width becomes the column's minimum and the table grows to fit. Under
 * `table-layout: fixed` the column width is decided by the percentage instead, so a heading
 * wider than its share is simply clipped — "Previously recorded" rendered as "Previously
 * reco" in a 10% column, and the Raised timestamp lost its timezone off the right edge.
 * Only `col-when` was exempted; every column in this table needs the same treatment. */
.table--queue thead th { white-space: normal; }
.table--queue th.col-when { width: 13%; }
/* And the timestamps themselves, for the same reason: `.stamp` is `nowrap` everywhere else
 * so a date never breaks mid-value, but in a fixed 13% column that clipped "2:13 AM UTC" to
 * "2:13 AM UT" — a time whose timezone has been cut off is worse than one on two lines,
 * because these are compared against Shopify and against a UTC schedule. */
.table--queue td.stamp { white-space: normal; }

/* Widths are allocated per column, not split equally.
 *
 * Equal division gave every non-product column 121px, which is narrower than the
 * "Pending review" chip's own 118px plus cell padding — so the chip rendered 9px past its
 * cell and sat on top of the Raised column beside it. Each column now gets what its
 * content actually needs: the status chip and the timestamp are the two widest, the
 * numeric columns are the narrowest. */
.table--queue th.col-change  { width: 12%; }
.table--queue th.col-value   { width: 10%; }
.table--queue th.col-delta   { width: 10%; }
.table--queue th.col-status  { width: 13%; }
.table--queue th.col-action  { width: 8%; }

/* The identity column stays put while the columns to its right scroll past.
 *
 * Applied only where the first column genuinely *is* the product's identity, and only on
 * desktop: on a tablet or a phone a frozen column takes width the remaining columns cannot
 * spare, which is a worse trade than scrolling the whole row. The width is capped so the
 * frozen part can never take more than about a third of a desktop canvas — a long fragrance
 * title would otherwise pin half the table in place.
 *
 * `box-shadow` draws both edges. A sticky cell in a `border-collapse: collapse` table does
 * not reliably keep its own borders, and the shadow also gives the frozen column a visible
 * edge so it reads as held rather than as overlapping the row behind it. */
@media (min-width: 64em) {
  .table--sticky-identity thead th:first-child,
  .table--sticky-identity tbody td:first-child,
  .table--sticky-identity tbody th:first-child {
    position: sticky; left: 0; z-index: 2;
    background: var(--surface);
    box-shadow: inset -1px 0 0 var(--border), inset 0 -1px 0 var(--border);
  }
  /* The header's own corner has to sit above both the sticky row cells and the sticky
   * header, or a row scrolls over it. */
  .table--sticky-identity thead th:first-child {
    z-index: 4; background: var(--surface-sunken);
    box-shadow: inset -1px 0 0 var(--border-strong), inset 0 -1px 0 var(--border-strong);
  }
  .table--sticky-identity tbody tr:hover td:first-child,
  .table--sticky-identity tbody tr:hover th:first-child { background: var(--surface-sunken); }
  .table--sticky-identity th:first-child { max-width: 24rem; }
  .table--sticky-identity td:first-child { max-width: 24rem; }
  /* An empty-state cell spans the table; freezing it would leave the message anchored to
   * the left edge while the (empty) columns scrolled beside it. */
  .table--sticky-identity tbody td.empty { position: static; box-shadow: none; max-width: none; }
}

/* Belt and braces: a chip can never paint outside the cell holding it, whatever the column
 * widths become later.
 *
 * This rule used to force `flex-wrap: wrap` and `text-align: center` on every chip in a
 * table, which is what turned a status cell into a tower with the glyph centred above the
 * word. The chip now shrinks correctly on its own — `.badge__text` is a real element with
 * `min-width: 0`, so the label ellipses and the glyph holds the left edge — and the full
 * wording stays reachable in the chip's `title`. All this needs to do is cap the width. */
td .badge, th .badge {
  max-width: 100%; min-width: 0;
}

/* The gap between two compared values in a table cell. The sign lives in the text, so the
   colour is reinforcement and never the only carrier of meaning. */
.delta { font-variant-numeric: tabular-nums; font-weight: 700; white-space: nowrap; }
.delta--increase { color: var(--ok-fg); }
.delta--decrease { color: var(--danger-fg); }
.delta--no-change { color: var(--text-muted); font-weight: 600; }

/* Provenance line under a card heading: which system each compared column came from. */
.card__note {
  margin: .5rem 0 0; font-size: .8125rem; color: var(--text-muted); line-height: 1.5;
}
.card__note strong { color: var(--text); font-weight: 650; }
.card__note strong::after { content: " —"; font-weight: 400; color: var(--text-muted); }

.will-change, .wont-change { margin: 0; padding-left: 1.15rem; font-size: .875rem; }
.will-change li { margin-bottom: .25rem; }
.wont-change li { margin-bottom: .25rem; color: var(--text-muted); }

details.raw { border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface-sunken); }
details.raw > summary {
  cursor: pointer; padding: .55rem .8rem; font-size: .8125rem; font-weight: 650; color: var(--text-muted);
}
details.raw > summary:hover { color: var(--text); }
details.raw[open] > summary { border-bottom: 1px solid var(--border); }
details.raw .raw__body { padding: .8rem; }
details.raw pre { background: var(--surface); }

/* ------------------------------------------- operator-facing field blocks
 * The vocabulary of the humanised screens: a labelled business fact, an identifier the
 * operator has to retype into Shopify, a before/after comparison, and the collapsed
 * technical disclosure that keeps the audit material available without letting it lead.
 */

.kv-pairs { margin: 0; display: grid; gap: 0; grid-template-columns: minmax(0, 1fr); }
.kv-pairs--secondary { margin-top: 1rem; }

.field-pair {
  display: grid; grid-template-columns: minmax(9rem, 17rem) minmax(0, 1fr); gap: .25rem .9rem;
  padding: .4rem 0; border-bottom: 1px solid var(--border);
}
.field-pair:last-child { border-bottom: 0; }
.field-pair > dt { font-size: .8125rem; font-weight: 600; color: var(--text-muted); }
.field-pair > dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }

/* Missing data is stated, not hidden — and it must not read as a value. */
.missing { color: var(--text-faint); font-style: italic; }

/* An identifier on its own line, so a double-click selects it and nothing else. No script
 * is involved: the approval surface serves no script-src and a copy button could not run. */
.copyable {
  display: inline-block; font-family: var(--mono); font-size: .8125rem;
  padding: .15rem .45rem; background: var(--surface-sunken);
  border: 1px solid var(--border); border-radius: 4px;
  user-select: all; overflow-wrap: anywhere;
}

.product-identity { display: grid; gap: .8rem; }
.product-identity__head { min-width: 0; }
.product-identity__title { font-size: 1.0625rem; line-height: 1.35; overflow-wrap: anywhere; }
.product-identity__title .missing { font-size: .9375rem; font-weight: 500; }
.product-identity__brand { margin: .15rem 0 0; font-size: .8125rem; color: var(--text-muted); }
.product-identity__links { margin-top: .1rem; }

.compare { margin: 0; display: grid; gap: 0; }
.compare__row {
  display: grid; grid-template-columns: minmax(10rem, 18rem) minmax(0, 1fr); gap: .2rem .9rem;
  padding: .5rem 0; border-bottom: 1px solid var(--border); align-items: baseline;
}
.compare__row:last-child { border-bottom: 0; }
.compare__row > dt { font-size: .8125rem; font-weight: 600; color: var(--text-muted); }
.compare__row > dd { margin: 0; min-width: 0; }
.compare__value { font-variant-numeric: tabular-nums; font-weight: 650; font-size: 1rem; overflow-wrap: anywhere; }
.compare__value--from { color: var(--danger-fg); }
.compare__value--to { color: var(--ok-fg); }
.compare__row--total { border-top: 2px solid var(--border-strong); border-bottom: 0; margin-top: .25rem; }
.compare__row--total > dt { color: var(--text); }
.compare__value--delta { font-size: 1.125rem; }

.result-line { margin: .9rem 0 0; font-size: .9375rem; }
.result-line strong { font-variant-numeric: tabular-nums; }

/* The pricing calculation, step by step. Numbered because the order is the argument: each
   line follows from the one above it, and a reviewer checking a price needs to see where a
   figure entered rather than just that it is present. The two key lines — the selling price
   and the was-price — are weighted, because those are the only two figures that would
   actually reach Shopify. Grid rather than a table so it collapses to one column on a
   narrow screen without the label and value drifting apart. */
.calc-steps {
  margin: 0 0 1rem; padding: 0 0 0 1.4rem; display: grid; gap: .55rem;
  font-size: .875rem;
}
.calc-step { padding-left: .2rem; }
.calc-step__label { display: block; color: var(--text-muted); font-weight: 600; }
.calc-step__value {
  display: block; font-variant-numeric: tabular-nums; font-size: 1rem;
  overflow-wrap: anywhere;
}
.calc-step__note { display: block; color: var(--text-muted); font-size: .8125rem; }
.calc-step--key > .calc-step__value { font-weight: 700; color: var(--text); }
.calc-step--key > .calc-step__label { color: var(--text); }
@media (min-width: 40rem) {
  .calc-step {
    display: grid; grid-template-columns: minmax(0, 12rem) minmax(0, 1fr);
    column-gap: .9rem; align-items: baseline;
  }
  .calc-step__note { grid-column: 2; }
}

.empty-state {
  margin: 0; padding: 1rem; border: 1px dashed var(--border-strong);
  border-radius: var(--radius-sm); background: var(--surface-sunken); font-size: .875rem;
}
.empty-state strong { display: block; color: var(--text); margin-bottom: .15rem; }
.empty-state span { color: var(--text-muted); }

details.technical {
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  background: var(--surface-sunken); margin-bottom: .6rem;
}
details.technical:last-child { margin-bottom: 0; }
details.technical > summary {
  cursor: pointer; padding: .55rem .8rem; font-size: .8125rem; font-weight: 650;
  color: var(--text-muted);
}
details.technical > summary:hover { color: var(--text); }
details.technical[open] > summary { border-bottom: 1px solid var(--border); }
details.technical .technical__body { padding: .8rem; min-width: 0; overflow-x: hidden; }
details.technical pre { background: var(--surface); }
/* The disclosure itself is a containing block for its contents at every width: nothing
 * opened inside it may widen the page, however long a single unbroken value is. */
details.technical { min-width: 0; max-width: 100%; }
details.technical .technical__value, details.technical code { overflow-wrap: anywhere; word-break: break-word; }
.technical__note { margin: 0 0 .6rem; font-size: .75rem; color: var(--text-faint); }
.technical__row {
  display: grid; grid-template-columns: minmax(8rem, 14rem) minmax(0, 1fr); gap: .2rem .8rem;
  padding: .25rem 0; border-bottom: 1px solid var(--border); align-items: baseline;
}
.technical__row:last-child { border-bottom: 0; }
.technical__label { font-size: .75rem; color: var(--text-muted); }
.technical__value {
  font-size: .75rem; background: var(--surface); border: 1px solid var(--border);
  border-radius: 4px; padding: .1rem .35rem; overflow-wrap: anywhere; user-select: all;
}

.page-head__ref { color: var(--text-faint); font-size: .8125rem; }

.env-tag__sub { display: block; font-size: .625rem; font-weight: 500; opacity: .85; letter-spacing: 0; text-transform: none; }

.timeline { list-style: none; margin: 0; padding: 0; }
.timeline li {
  position: relative; padding: 0 0 .9rem 1.35rem;
  border-left: 2px solid var(--border); margin-left: .35rem;
}
.timeline li:last-child { padding-bottom: 0; border-left-color: transparent; }
.timeline li::before {
  content: ""; position: absolute; left: -.4rem; top: .35rem;
  width: .65rem; height: .65rem; border-radius: 50%;
  background: var(--surface); border: 2px solid var(--border-strong);
}
.timeline__title { font-weight: 650; font-size: .875rem; }
.timeline__meta { font-size: .75rem; color: var(--text-faint); }

/* ------------------------------------------------------------- login page */

.auth-wrap {
  min-height: 100vh; display: flex; align-items: center; justify-content: center;
  padding: 2rem 1.25rem; background: var(--bg);
}
.auth-card {
  width: 100%; max-width: 24rem; background: var(--surface);
  border: 1px solid var(--border); border-radius: var(--radius);
  box-shadow: var(--shadow-md); padding: 1.75rem;
}
.auth-card .brand { justify-content: center; margin-bottom: .35rem; }
.auth-card .brand__mark { color: var(--brand-ink); }
.auth-card .brand__sub { color: var(--text-faint); }
.auth-card h1 { font-size: 1.125rem; margin-bottom: 1.1rem; text-align: center; }
.auth-card input[type="submit"], .auth-card .btn { width: 100%; }
.auth-card .alerts { margin-bottom: 1rem; }
.auth-foot { margin: 1.1rem 0 0; text-align: center; font-size: .75rem; color: var(--text-faint); }

/* ----------------------------------------------------------------- footer */

.app-foot {
  border-top: 1px solid var(--border); padding: .9rem 1.5rem;
  font-size: .75rem; color: var(--text-faint); background: var(--surface);
}
.app-foot__inner { max-width: var(--page-max); margin: 0 auto; display: flex; gap: .75rem; flex-wrap: wrap; justify-content: space-between; }

/* ------------------------------------------------------------ responsive  */

@media (max-width: 63.99em) {
  /* Below the desktop breakpoint the sidebar copy is removed entirely and the header
   * disclosure copy takes over. Exactly one nav exists in the accessibility tree. */
  .app-body > .sidebar { display: none; }
  .nav-toggle { display: block; }
  .nav-group { margin-bottom: .75rem; }
  .main { padding: 1.15rem 1rem 3rem; }

  /* Rows get taller as columns get narrower — a row that is one line at 1440px is four at
   * 375px — so a fixed row count is the wrong unit here and a *smaller* height is the wrong
   * instinct. Bounding at 24rem gave a two-row queue a scrollbar, which is the "unnecessary
   * scrollbar on a tiny table" this is supposed to avoid. The allowance grows as the rows do,
   * and the `vh` term still stops the table taking the whole screen. */
  :root { --table-viewport: min(27rem, 60vh); }
}

@media (max-width: 47.99em) {
  :root { --table-viewport: min(26rem, 62vh); }
}

@media (max-width: 47.99em) {
  body { font-size: 14.5px; }
  h1 { font-size: 1.3125rem; }
  .app-header__bar { padding: .6rem .9rem; gap: .6rem; }
  .brand__sub { display: none; }
  .header-meta { width: 100%; margin-left: 0; }
  .main { padding: 1rem .8rem 3rem; }
  .page-head { flex-direction: column; align-items: stretch; }
  .page-head__actions { width: 100%; }

  /* Key/value grids stack rather than squeeze. */
  .kv, dl.details, .change-line { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .kv dt, dl.details dt, .change-line dt { border-bottom: 0; padding-bottom: 0; }
  .kv dd, dl.details dd, .change-line dd { padding-top: .1rem; }

  /* The humanised field, comparison and technical rows do the same: label above value,
   * so a long SKU or a long product title never forces the page sideways. */
  .field-pair, .compare__row, .technical__row { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .field-pair > dt, .compare__row > dt { padding-bottom: 0; }
  .field-pair > dd, .compare__row > dd { padding-top: .1rem; }
  .technical__label { padding-bottom: .1rem; }

  table.kv-table, table.kv-table tbody, table.kv-table tr, table.kv-table th, table.kv-table td { display: block; width: auto; }
  table.kv-table tbody th { width: auto; min-width: 0; border-bottom: 0; padding-bottom: .1rem; background: none; }
  table.kv-table tbody td { padding-top: .1rem; }
  table.kv-table tbody tr { border-bottom: 1px solid var(--border); padding: .35rem 0; }
  table.kv-table tbody tr:last-child { border-bottom: 0; }

  .truncate { max-width: 100%; white-space: normal; }
  .filters { padding: .8rem; }
  .filters label { min-width: 100%; flex-basis: 100%; }
  .decision { grid-template-columns: minmax(0, 1fr); }
  .pager { flex-direction: column; align-items: flex-start; }
  .stat-grid { grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); }

  /* Touch targets.
   *
   * Desktop density is deliberate and is left exactly as it is above this breakpoint: a
   * queue is read by eye at 1440px and a taller row would fit fewer requests on screen.
   * On a phone the same controls were 29-35px high, which is below what a thumb reliably
   * hits, so the high-value ones — the navigation disclosure, sign out, the queue's own
   * tabs, Review, and the four decision buttons — get a comfortable minimum here and only
   * here. `min-height` with centred content grows the small ones without stretching the
   * ones that are already large enough. */
  .nav-toggle > summary,
  .signout button,
  .tabs a,
  .btn,
  .btn--sm,
  .decision button,
  .filters select,
  .filters input,
  .pager a {
    min-height: 44px;
    display: inline-flex; align-items: center; justify-content: center;
  }
  .nav-toggle > summary { justify-content: space-between; }
  .tabs a { padding-left: .85rem; padding-right: .85rem; }
  .btn, .btn--sm { padding-top: .35rem; padding-bottom: .35rem; }
  /* The nav links are a list of destinations rather than a row of buttons, so they get
   * height without becoming full-width blocks that lose their list shape. */
  .nav-toggle .nav-link { min-height: 44px; }
  /* An inline link inside a sentence must not become a 44px block — that would break the
   * paragraph it sits in. Only standalone controls are grown. */
  .cell-link, .empty__link { min-height: 0; display: inline; }
}

@media print {
  .app-header, .sidebar, .app-foot, .page-head__actions, .decision, .tabs, .pager { display: none !important; }
  .main { padding: 0; }
  .card { box-shadow: none; break-inside: avoid; }
  body { background: #fff; }
  /* A bounded viewport is a screen affordance. On paper it would print six rows and hide
   * the rest, so the table is released to its full height and the sticky cells are let go. */
  .table-wrap--viewport { max-height: none; overflow: visible; }
  .table-wrap--viewport thead th,
  .table--sticky-identity thead th:first-child,
  .table--sticky-identity tbody td:first-child,
  .table--sticky-identity tbody th:first-child { position: static; }
}
