/* ============================================================
   TRILLIONS DESIGN SYSTEM 1.0 — tokens and primitives
   ------------------------------------------------------------
   The single source of truth for every Trillions surface: public
   site, buyer portal, operations dashboard and the server-rendered
   admin pages. Load this first, then the surface stylesheet.

   Two rules govern everything below.

   1. The brand is monochrome. Colour enters the interface only to
      carry a verdict. If a hue appears on a Trillions screen, it
      means something has been judged.
   2. Every verdict is encoded twice — once in colour, once in fill.
      Nothing in this file depends on colour vision alone.

   Written in logical properties so `dir="rtl"` mirrors correctly.
   ============================================================ */

:root {
  /* brand — fixed by the identity, do not extend.
     The field is white end to end: --canvas and --surface hold the same
     value, and separation is carried by --line rather than by a fill.
     The navy is a mark now — it fills buttons, tiles, pills and toasts,
     and no longer fills a band. */
  --ink: #0f172a;
  --surface: #ffffff;
  --canvas: #ffffff;

  /* neutral ramp — the brand navy is slate-900, so the ramp is its own family */
  --ink-2: #1e293b;
  --ink-3: #334155;
  --mute: #475569;
  --mute-2: #64748b;
  --mute-3: #94a3b8;
  --line-strong: #cbd5e1;
  --line: #e2e8f0;
  --wash: #f1f5f9;

  /* Reversed out of the ink field. The ramp's 500 step only reaches 3.75:1
     against ink, so text on the brand navy uses these two steps instead —
     12.0:1 and 6.9:1. Nothing else in the ramp may carry text on ink. */
  --on-ink: #cbd5e1;
  --on-ink-2: #94a3b8;

  /* verdict — the only colour in the system. never decorative. */
  --ok: #0f6e4c;
  --ok-bg: #e6f2ec;
  --ok-line: #bfddd0;
  --gap: #8a5300;
  --gap-bg: #fbf0df;
  --gap-line: #ebd4a8;
  --miss: #a32017;
  --miss-bg: #fbeae8;
  --miss-line: #efc7c2;
  --rev: #1d4ed8;
  --rev-bg: #e9effc;
  --rev-line: #c3d3f7;
  --nil: #64748b;
  --nil-bg: #f1f5f9;
  --nil-line: #dee5ec;

  /* type */
  --sans: Poppins, "Segoe UI", Helvetica, Arial, sans-serif;
  --mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
  --arabic: "IBM Plex Sans Arabic", Poppins, sans-serif;
  --han: "Noto Sans SC", Poppins, sans-serif;

  /* space — 4px base */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 24px;
  --s6: 32px;
  --s7: 48px;
  --s8: 64px;
  --s9: 96px;

  /* radius — derived from the 88px monogram tile at 24px */
  --r1: 4px;
  --r2: 8px;
  --r3: 12px;
  --r4: 16px;
  --r5: 24px;
  --pill: 999px;

  /* elevation — one step only */
  --e1: 0 1px 2px rgba(15, 23, 42, 0.05);
  --e2: 0 1px 2px rgba(15, 23, 42, 0.04), 0 12px 32px -16px rgba(15, 23, 42, 0.28);

  /* motion */
  --fast: 120ms;
  --base: 180ms;
  --slow: 260ms;
  --ease: cubic-bezier(0.2, 0.7, 0.3, 1);

  /* layout — 12 columns at 1280, 24px gutters, 32px page margin */
  --measure: 1280px;
  --measure-narrow: 760px;
  --gutter: var(--s5);
  --page-margin: var(--s6);
}

/* ============================================================
   RESET
   ============================================================ */

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

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

body {
  margin: 0;
  background: var(--canvas);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 15px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3,
h4,
h5,
h6,
p,
figure,
fieldset,
blockquote {
  margin: 0;
}

ul,
ol {
  margin: 0;
  padding: 0;
  list-style: none;
}

fieldset {
  padding: 0;
  border: 0;
  min-inline-size: 0;
}

legend {
  padding: 0;
}

a {
  color: inherit;
}

img,
svg {
  display: block;
  max-width: 100%;
}

img {
  height: auto;
}

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}

table {
  border-collapse: collapse;
}

/* Focus is never removed. 2px ink outline at 2px offset, everywhere. */
:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
  border-radius: var(--r1);
}

::selection {
  background: var(--ink);
  color: #fff;
}

[hidden] {
  display: none !important;
}

/* Nothing bounces, nothing pulses, nothing loops — and under
   prefers-reduced-motion nothing moves at all. No state in this
   system is conveyed by animation. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}

/* ============================================================
   SCRIPTS — three writing systems share every screen
   ============================================================ */

:lang(ar) {
  font-family: var(--arabic);
  line-height: 1.7;
  letter-spacing: 0;
}

/* Arabic needs slightly more height than Latin at the same nominal size.
   The bump is applied once, on the body, and inherited from there. Putting
   it on :lang(ar) itself would match every descendant — lang is inherited —
   and 1.06em would compound at each level, so deeply nested small print
   (.foot-grid, .finding) would render larger than the body text. */
html:lang(ar) body {
  font-size: 1.06em;
}

:lang(zh),
:lang(zh-CN),
:lang(zh-Hans) {
  font-family: var(--han);
  letter-spacing: 0;
}

/* The six supplier statuses are frozen in each language, but the report, the
   outbound email and the API are English. So on a translated page the English
   term is published beside the translation, and a buyer can map what they read
   here to what arrives in the PDF. On the English page it would only repeat the
   label next to itself, so it is not shown there. */
.term-en {
  display: none;
}

:lang(ar) .term-en,
:lang(zh) .term-en {
  display: inline-block;
  margin-inline-end: 8px;
  font-size: 0.85em;
  color: var(--mute);
}

/* The gloss carries dir="ltr" so the Latin term shapes correctly, which also
   means its own logical properties resolve left-to-right — margin-inline-end
   would put the gap on the right, where the Arabic sentence is not. The gap
   belongs on the side the sentence continues towards. */
[dir="rtl"] .term-en {
  margin-inline-end: 0;
  margin-left: 8px;
}

/* Document numbers, IBANs, HS codes, amounts and Latin company names
   stay left-to-right inside a right-to-left line. */
.mono,
.num,
[dir="ltr"] {
  unicode-bidi: isolate;
}

/* ============================================================
   TYPE SCALE
   ============================================================ */

.t-d1 {
  font-size: clamp(40px, 5vw, 62px);
  font-weight: 800;
  letter-spacing: -0.04em;
  line-height: 1.02;
}

.t-d2 {
  font-size: clamp(32px, 3.6vw, 44px);
  font-weight: 800;
  letter-spacing: -0.035em;
  line-height: 1.06;
}

.t-h1 {
  font-size: clamp(26px, 2.6vw, 30px);
  font-weight: 700;
  letter-spacing: -0.025em;
  line-height: 1.15;
}

.t-h2 {
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.25;
}

.t-h3 {
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.01em;
  line-height: 1.35;
}

.t-body {
  font-size: 15px;
  line-height: 1.6;
  color: var(--ink-3);
}

.t-sm {
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--mute);
}

.t-cap {
  font-size: 12px;
  line-height: 1.45;
  color: var(--mute);
}

/* The only uppercase in the system. */
.t-lab {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--mute);
}

/* Mono means: this value was transcribed from evidence. */
.mono {
  font-family: var(--mono);
  font-size: 12.5px;
  letter-spacing: -0.01em;
}

.t-num {
  font-family: var(--mono);
  font-size: 15px;
  font-weight: 500;
  letter-spacing: -0.02em;
}

.spec {
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--mute);
  letter-spacing: -0.01em;
}

/* Body copy is capped at 68 characters, report findings at 76.
   Nothing runs the full width of a desktop screen. */
.measure {
  max-width: 68ch;
}

.measure-wide {
  max-width: 76ch;
}

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

/* Every form on the site is submitted by script, and the controls around it are
   hidden until script unhides them. With scripting off the page is not merely
   degraded, it is inert and says nothing about why. This is what it says
   instead, so the reader gets a route to the desk rather than a dead button. */
.noscript-note {
  display: block;
  margin-block: var(--s4);
  padding: var(--s4);
  border: 1px solid var(--gap-line);
  border-radius: var(--r2);
  background: var(--gap-bg);
  color: var(--gap);
  font-size: 14px;
  line-height: 1.6;
}

.noscript-note a {
  color: inherit;
  font-weight: 600;
}

/* ============================================================
   LAYOUT PRIMITIVES
   ============================================================ */

.wrap {
  width: 100%;
  max-width: var(--measure);
  margin-inline: auto;
  padding-inline: var(--page-margin);
}

.wrap-narrow {
  width: 100%;
  max-width: var(--measure-narrow);
  margin-inline: auto;
  padding-inline: var(--page-margin);
}

.grid {
  display: grid;
  gap: var(--s4);
}

.g2 {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.g3 {
  grid-template-columns: repeat(3, minmax(0, 1fr));
}

.g4 {
  grid-template-columns: repeat(4, minmax(0, 1fr));
}

.stack {
  display: grid;
  gap: var(--s5);
}

.row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  align-items: center;
}

.row--between {
  justify-content: space-between;
  font-size: 13.5px;
}

.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r4);
  padding: var(--s5);
}

.panel-flush {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r4);
  overflow: hidden;
}

.panel-canvas {
  background: var(--canvas);
}

.panel-inner {
  background: var(--wash);
  border-radius: var(--r3);
  padding: var(--s4);
}

/* Depth is structural, not decorative. */
.raised {
  box-shadow: var(--e1);
}

.floating {
  box-shadow: var(--e2);
  border-color: transparent;
}

.rule {
  height: 1px;
  border: 0;
  background: var(--line);
  margin-block: var(--s5);
}

@media (max-width: 900px) {
  .g3,
  .g4 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 620px) {
  .g2,
  .g3,
  .g4 {
    grid-template-columns: 1fr;
  }

  :root {
    --page-margin: var(--s4);
  }
}

/* ============================================================
   IDENTITY — the monogram tile
   ------------------------------------------------------------
   The only shape in the system carrying the 24px radius, which is
   what keeps it recognisable at 20px and at 200px. Never recoloured
   to a verdict, never given a gradient, shadow or outline.
   ============================================================ */

.tile {
  display: grid;
  place-items: center;
  flex: none;
  width: 34px;
  height: 34px;
  border-radius: 9px;
  background: var(--ink);
  color: #fff;
  font-weight: 800;
  font-size: 19px;
  line-height: 1;
}

.tile-sm {
  width: 26px;
  height: 26px;
  border-radius: 7px;
  font-size: 15px;
}

.tile-lg {
  width: 44px;
  height: 44px;
  border-radius: 12px;
  font-size: 25px;
}

.lockup {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  text-decoration: none;
}

.lockup b {
  font-size: 19px;
  font-weight: 800;
  letter-spacing: -0.03em;
}

/* The field is white everywhere now, so there is nothing left to invert
   against. The class is kept because it is written into the header and
   footer of every page; it resolves to the ordinary lockup. */
.lockup-inv .tile {
  background: var(--ink);
  color: #fff;
}

.lockup-inv b {
  color: var(--ink);
}

/* ============================================================
   ICONS — 24×24, 1.6px stroke, round caps
   ------------------------------------------------------------
   Icons name objects a buyer handles: a licence, a quote, a
   container. No icon ever carries a verdict on its own — a verdict
   is always a word plus a colour.
   ============================================================ */

.ic {
  width: 24px;
  height: 24px;
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.ic-sm {
  width: 17px;
  height: 17px;
  stroke-width: 1.8;
}

.ic-lg {
  width: 26px;
  height: 26px;
}

/* Arrows and chevrons point the way the language reads. */
[dir="rtl"] .ic-flip {
  transform: scaleX(-1);
}

/* ============================================================
   THE EVIDENCE LEDGER — the signature element
   ------------------------------------------------------------
   Twelve cells, one per standing check, always in the same order.
   Filled is matched. Dashed outline is a gap — the document exists
   but does not hold up. Solid outline is missing. Grey is not
   applicable. Blue is still running and carries no verdict.

   The same object at every size, so a buyer who learns to read it
   in a table row can read it on a report, an email or a phone. It
   never wraps: a wrapped ledger is a different shape.
   ============================================================ */

.ledger {
  display: flex;
  gap: 3px;
  align-items: stretch;
  flex-wrap: nowrap;
}

.ledger i {
  flex: 1;
  height: 12px;
  border-radius: 2px;
  background: var(--nil-bg);
  display: block;
}

.ledger i[data-s="ok"] {
  background: var(--ok);
}

.ledger i[data-s="rev"] {
  background: var(--rev);
}

.ledger i[data-s="gap"] {
  background: transparent;
  border: 1.5px dashed var(--gap);
}

.ledger i[data-s="miss"] {
  background: transparent;
  border: 1.5px solid var(--miss);
}

.ledger i[data-s="nil"] {
  background: var(--nil-bg);
}

.ledger--sm i {
  height: 6px;
  border-radius: 1.5px;
}

.ledger--lg i {
  height: 22px;
  border-radius: var(--r1);
}

.ledger--hair i {
  height: 4px;
  border-radius: 1px;
}

[dir="rtl"] .ledger {
  flex-direction: row-reverse;
}

.ledger-cols {
  display: flex;
  gap: 3px;
  margin-block-start: var(--s2);
}

.ledger-cols span {
  flex: 1;
  font-family: var(--mono);
  font-size: 9.5px;
  line-height: 1.2;
  letter-spacing: -0.02em;
  color: var(--mute);
  text-align: center;
}

[dir="rtl"] .ledger-cols {
  flex-direction: row-reverse;
}

@media (max-width: 720px) {
  .ledger-cols {
    display: none;
  }
}

.ledger-key {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s4);
  margin-block-start: var(--s3);
}

.ledger-key span {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 12px;
  color: var(--mute);
}

.ledger-key b {
  display: block;
  width: 14px;
  height: 10px;
  border-radius: 2px;
  flex: none;
}

.ledger-key b[data-s="ok"] {
  background: var(--ok);
}

.ledger-key b[data-s="gap"] {
  border: 1.5px dashed var(--gap);
}

.ledger-key b[data-s="miss"] {
  border: 1.5px solid var(--miss);
}

.ledger-key b[data-s="nil"] {
  background: var(--nil-bg);
}

.ledger-key b[data-s="rev"] {
  background: var(--rev);
}

/* ============================================================
   STATUS CHIPS — the six fixed strings
   ------------------------------------------------------------
   Not reviewed · In review · Too little to assess · Gaps found ·
   Fully evidenced · Flagged. Same words in the product, the email,
   the PDF and the API. Pill shape distinguishes a verdict from a
   control; a chip never appears without a route to the evidence.
   ============================================================ */

.chip {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding-block: 5px;
  padding-inline: 10px 12px;
  border-radius: var(--pill);
  border: 1px solid transparent;
  font-size: 12.5px;
  font-weight: 600;
  white-space: nowrap;
  text-decoration: none;
}

.chip i {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex: none;
}

[dir="rtl"] .chip {
  flex-direction: row-reverse;
}

.chip--nil {
  background: var(--nil-bg);
  color: var(--ink-3);
  border-color: var(--nil-line);
}

.chip--nil i {
  background: var(--mute-3);
}

.chip--rev {
  background: var(--rev-bg);
  color: var(--rev);
  border-color: var(--rev-line);
}

.chip--rev i {
  background: var(--rev);
}

.chip--thin {
  background: var(--nil-bg);
  color: var(--mute);
  border-color: var(--nil-line);
}

.chip--thin i {
  background: transparent;
  border: 1.5px solid var(--mute-2);
}

.chip--gap {
  background: var(--gap-bg);
  color: var(--gap);
  border-color: var(--gap-line);
}

.chip--gap i {
  background: var(--gap);
}

.chip--ok {
  background: var(--ok-bg);
  color: var(--ok);
  border-color: var(--ok-line);
}

.chip--ok i {
  background: var(--ok);
}

.chip--flag {
  background: var(--miss-bg);
  color: var(--miss);
  border-color: var(--miss-line);
}

.chip--flag i {
  background: var(--miss);
}

.chip--xs {
  padding-block: 3px;
  padding-inline: 9px 10px;
  font-size: 11px;
}

/* ============================================================
   EVIDENCE SCORE
   ------------------------------------------------------------
   One number, in mono, without colour, never alone. No ramp from
   red to green behind it, no letter grades, no badge.
   ============================================================ */

.score {
  display: flex;
  align-items: flex-end;
  gap: var(--s4);
}

.score-n {
  font-family: var(--mono);
  font-size: 56px;
  font-weight: 600;
  letter-spacing: -0.05em;
  line-height: 0.9;
}

.score-d {
  font-family: var(--mono);
  font-size: 18px;
  color: var(--mute-2);
  letter-spacing: -0.03em;
  padding-block-end: 5px;
}

.score-bar {
  height: 8px;
  border-radius: var(--r1);
  background: var(--wash);
  overflow: hidden;
  margin-block-start: var(--s4);
}

.score-bar i {
  display: block;
  height: 100%;
  background: var(--ink);
  border-radius: var(--r1);
}

/* Inline style attributes are forbidden by the content security policy, so a
   score fill declares its value as data and the width is resolved here. Add a
   step when a new score appears in the markup. */
.score-bar i[data-fill="31"]{ width: 31%; }
.score-bar i[data-fill="62"]{ width: 62%; }
.score-bar i[data-fill="74"]{ width: 74%; }
.score-bar i[data-fill="88"]{ width: 88%; }
.score-bar i[data-fill="96"]{ width: 96%; }

.score-ticks {
  display: flex;
  justify-content: space-between;
  margin-block-start: 6px;
}

.score-ticks span {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--mute-2);
}

/* ============================================================
   BUTTONS
   ------------------------------------------------------------
   One primary action per screen, in ink. Labels name the outcome —
   "Send for review", not "Submit" — and the same verb carries
   through to the confirmation that follows.
   ============================================================ */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  height: 40px;
  padding-inline: 18px;
  border-radius: var(--r2);
  border: 1px solid transparent;
  background: transparent;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease),
    color var(--fast) var(--ease);
}

.btn .ic,
.btn svg {
  width: 17px;
  height: 17px;
  stroke-width: 1.8;
}

.btn--primary {
  background: var(--ink);
  color: #fff;
}

.btn--primary:hover {
  background: var(--ink-3);
}

.btn--primary:active {
  background: #0b1120;
}

.btn--secondary {
  background: var(--surface);
  color: var(--ink);
  border-color: var(--line-strong);
}

.btn--secondary:hover {
  background: var(--wash);
  border-color: var(--mute-2);
}

.btn--ghost {
  color: var(--ink-3);
}

.btn--ghost:hover {
  background: var(--wash);
  color: var(--ink);
}

/* Destructive is never primary-filled. */
.btn--danger {
  background: var(--surface);
  color: var(--miss);
  border-color: var(--miss-line);
}

.btn--danger:hover {
  background: var(--miss-bg);
}

.btn--link {
  height: auto;
  padding: 0;
  color: var(--ink);
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-color: var(--line-strong);
}

.btn--link:hover {
  text-decoration-color: var(--ink);
}

/* The white-filled action for a band that used to be ink. On a white field
   there is nothing to invert, so it carries the primary fill; the class
   stays because the pages still carry it. */
.btn--inverse {
  background: var(--ink);
  color: #fff;
}

.btn--inverse:hover {
  background: var(--ink-3);
}

/* Named for the ink band it used to sit on. Those bands are white now, so
   it reads as an ordinary secondary action; the class stays because the
   pages still carry it.

   One band did not turn white: the support-session banner is deliberately
   still ink, because it is the one thing on the page that has to be seen. A
   button carrying this class sat on it reading var(--ink) on var(--ink) —
   present, focusable, announced, and invisible until a pointer crossed it and
   the hover background arrived underneath. That banner styles its own button
   below rather than reaching for this one. */
.btn--onink {
  color: var(--ink);
  border-color: var(--line-strong);
}

.btn--onink:hover {
  background: var(--wash);
  color: var(--ink);
}

/* ------------------------------------------------------------
   The support-session banner

   FR-IMP-13: fixed, non-dismissible, naming both identities for the whole
   session. Three surfaces raise it — the operations dashboard, the buyer
   workspace and the supplier workspace — and it was three copies of the same
   twelve inline style assignments in three controllers. It is one class now,
   here, because the three pages share no stylesheet but this one.

   The band stays ink. What sits on ink has to be legible on ink, which is
   what the button below is for and what .btn--onink is not.
   ------------------------------------------------------------ */

.impersonation-band {
  position: fixed;
  inset-block-start: 0;
  inset-inline: 0;
  z-index: 1000;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--s3);
  padding: 10px var(--s4);
  background: var(--ink);
  color: #fff;
  font-weight: 600;
}

.impersonation-band .btn {
  background: rgba(255, 255, 255, 0.12);
  border-color: rgba(255, 255, 255, 0.45);
  color: #fff;
}

.impersonation-band .btn:hover {
  background: rgba(255, 255, 255, 0.22);
  border-color: #fff;
  color: #fff;
}

/* The band is the page's loudest element and its control has to be reachable
   from the keyboard on it too — the shared focus ring is drawn for light
   ground and disappears here. */
.impersonation-band .btn:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}

.btn--lg {
  height: 48px;
  padding-inline: 24px;
  font-size: 15px;
  border-radius: 10px;
}

.btn--sm {
  height: 32px;
  padding-inline: 12px;
  font-size: 13px;
}

.btn--icon {
  width: 40px;
  padding: 0;
}

.btn--block {
  width: 100%;
}

.btn[disabled],
.btn[aria-disabled="true"] {
  opacity: 0.42;
  pointer-events: none;
}

/* Loading keeps the button's width so the layout does not jump. */
.btn--loading {
  pointer-events: none;
  color: transparent;
  position: relative;
}

.btn--loading::after {
  content: "";
  position: absolute;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.35);
  border-top-color: #fff;
  animation: trillions-spin 0.7s linear infinite;
}

@keyframes trillions-spin {
  to {
    transform: rotate(360deg);
  }
}

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  align-items: center;
}

@media (max-width: 480px) {
  .btn-row .btn {
    width: 100%;
  }
}

/* ============================================================
   FORM CONTROLS
   ------------------------------------------------------------
   Buyers fill these in from documents in another window, often in a
   second language, often on a phone at a warehouse. Labels sit above
   fields, hints explain where to find the value, and errors say what
   to do rather than what went wrong.
   ============================================================ */

.field {
  display: grid;
  gap: 6px;
}

.field > label,
.field-label {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ink-2);
}

.field .opt {
  font-weight: 400;
  color: var(--mute);
}

.field .hint,
.hint {
  font-size: 12px;
  line-height: 1.45;
  color: var(--mute);
}

.input,
.select,
.textarea {
  width: 100%;
  height: 42px;
  padding-inline: 13px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r2);
  background: var(--surface);
  font-family: var(--sans);
  font-size: 14px;
  color: var(--ink);
  transition: border-color var(--fast) var(--ease), box-shadow var(--fast) var(--ease);
  /* A value somebody types is Arabic or Latin and the field does not know
     which. Under an rtl paragraph a Latin value that overruns its box scrolls
     to its END: a legal name reads back as "- SOLE PROPRIETORSHIP L.L.C" with
     the company's own name out of sight, so the field an operator most needs
     to check is the one they cannot. The display surfaces already carry this
     -- .rq-summary dd calls it "every value here was typed by the person
     reading it back" -- and the fields those values were typed into did not.
     plaintext resolves each from its own first strong character, which is
     right for both scripts and forces neither. */
  unicode-bidi: plaintext;
}

.textarea {
  height: auto;
  min-height: 96px;
  padding-block: 11px;
  line-height: 1.55;
  resize: vertical;
}


.input::placeholder,
.textarea::placeholder {
  color: var(--mute-2);
}

.input:hover,
.select:hover,
.textarea:hover {
  border-color: var(--mute-2);
}

.input:focus,
.select:focus,
.textarea:focus {
  outline: none;
  border-color: var(--ink);
  box-shadow: 0 0 0 3px rgba(15, 23, 42, 0.09);
}

.input[data-mono],
.mono-input {
  font-family: var(--mono);
  letter-spacing: -0.01em;
}

.select {
  appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%2364748B' stroke-width='1.8' stroke-linecap='round'><path d='M4 6.5l4 4 4-4'/></svg>");
  background-repeat: no-repeat;
  background-position: right 12px center;
  padding-inline-end: 36px;
}

[dir="rtl"] .select {
  background-position: left 12px center;
  padding-inline-end: 36px;
}

.input[disabled],
.select[disabled],
.textarea[disabled] {
  background: var(--wash);
  color: var(--mute);
  cursor: not-allowed;
}

.field--error .input,
.field--error .select,
.field--error .textarea {
  border-color: var(--miss);
  background: #fffdfd;
}

.field--error .hint {
  color: var(--miss);
  font-weight: 500;
  display: flex;
  gap: 6px;
  align-items: flex-start;
}

.input-group {
  display: flex;
}

.input-group .addon {
  display: grid;
  place-items: center;
  padding-inline: 13px;
  border: 1px solid var(--line-strong);
  border-inline-end: 0;
  border-start-start-radius: var(--r2);
  border-end-start-radius: var(--r2);
  background: var(--wash);
  font-family: var(--mono);
  font-size: 12.5px;
  color: var(--mute);
}

.input-group .input {
  border-start-start-radius: 0;
  border-end-start-radius: 0;
}

.check {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  cursor: pointer;
}

.check input {
  appearance: none;
  width: 19px;
  height: 19px;
  flex: none;
  margin: 1px 0 0;
  border: 1.5px solid var(--line-strong);
  border-radius: 5px;
  background: var(--surface);
  cursor: pointer;
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease);
}

.check input[type="radio"] {
  border-radius: 50%;
}

.check input:checked {
  background: var(--ink);
  border-color: var(--ink);
}

.check input[type="checkbox"]:checked {
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='19' height='19' fill='none' stroke='white' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'><path d='M5 9.8l3 3 6-6.5'/></svg>");
}

.check input[type="radio"]:checked {
  box-shadow: inset 0 0 0 4px var(--surface);
}

.check > span {
  font-size: 13.5px;
  line-height: 1.45;
  color: var(--ink-3);
}

/* A required box that was left unticked when the form was sent. The controller
   sets this and clears it when the box is ticked, so the mark outlasts the
   browser's own bubble instead of vanishing at the next scroll. */
.check.is-missing input {
  border-color: var(--miss);
  background: var(--miss-bg);
}

.check.is-missing > span {
  color: var(--miss);
}

.check em {
  display: block;
  font-style: normal;
  font-size: 12px;
  color: var(--mute);
  margin-block-start: 2px;
}

.switch {
  position: relative;
  width: 42px;
  height: 24px;
  flex: none;
}

.switch input {
  appearance: none;
  width: 100%;
  height: 100%;
  margin: 0;
  border-radius: var(--pill);
  background: var(--line-strong);
  cursor: pointer;
  transition: background var(--fast) var(--ease);
}

.switch input:checked {
  background: var(--ink);
}

.switch::after {
  content: "";
  position: absolute;
  inset-block-start: 3px;
  inset-inline-start: 3px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: #fff;
  pointer-events: none;
  transition: transform var(--base) var(--ease);
}

.switch:has(input:checked)::after {
  transform: translateX(18px);
}

[dir="rtl"] .switch:has(input:checked)::after {
  transform: translateX(-18px);
}

/* A card-shaped radio, used where the choice is the main decision
   on the screen rather than one field among many. */
.choice {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  padding: var(--s3) var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r3);
  background: var(--surface);
  cursor: pointer;
  transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease);
}

.choice:hover {
  border-color: var(--mute-2);
}

.choice:has(input:checked) {
  border-color: var(--ink);
  background: var(--wash);
}

.choice input {
  appearance: none;
  width: 19px;
  height: 19px;
  flex: none;
  margin: 2px 0 0;
  border: 1.5px solid var(--line-strong);
  border-radius: 50%;
  background: var(--surface);
  cursor: pointer;
}

.choice input:checked {
  background: var(--ink);
  border-color: var(--ink);
  box-shadow: inset 0 0 0 4px var(--surface);
}

.choice strong {
  display: block;
  font-size: 13.5px;
  font-weight: 600;
}

.choice small {
  display: block;
  font-size: 12px;
  color: var(--mute);
  margin-block-start: 2px;
}

/* Honeypot. Removed from the accessibility tree, never shown. */
.hidden-field {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Form status lines. Errors stay on the screen; they never become
   a toast that disappears before it has been read. */
.form-status {
  display: none;
  font-size: 13px;
  line-height: 1.5;
  padding: var(--s3) var(--s4);
  border-radius: var(--r3);
  border: 1px solid var(--line);
  background: var(--wash);
  color: var(--ink-3);
}

.form-status.visible {
  display: block;
}

.form-status.info {
  background: var(--rev-bg);
  border-color: var(--rev-line);
  color: #12358f;
}

.form-status.success {
  background: var(--ok-bg);
  border-color: var(--ok-line);
  color: var(--ok);
}

.form-status.error {
  background: var(--miss-bg);
  border-color: var(--miss-line);
  color: var(--miss);
}

/* ============================================================
   FILE INTAKE
   ------------------------------------------------------------
   Buyers arrive with a folder of photographed documents in mixed
   languages. The intake never rejects a file for being messy — it
   accepts, then says plainly what it could and could not read.
   ============================================================ */

.drop {
  border: 1.5px dashed var(--line-strong);
  border-radius: var(--r3);
  background: var(--wash);
  padding: var(--s6) var(--s5);
  text-align: center;
  transition: border-color var(--base) var(--ease), background var(--base) var(--ease);
}

.drop:hover,
.drop:focus-within {
  border-color: var(--ink);
  background: #eff3f7;
}

.drop .ic {
  margin-inline: auto;
  stroke: var(--mute);
}

.drop b {
  display: block;
  font-size: 14.5px;
  font-weight: 600;
  margin: 10px 0 4px;
}

.drop p {
  font-size: 12.5px;
  color: var(--mute);
}

.drop input[type="file"] {
  display: block;
  width: 100%;
  margin-block-start: var(--s4);
  font-size: 12.5px;
  color: var(--mute);
}

.file {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: 11px var(--s3);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  background: var(--surface);
}

.file + .file {
  margin-block-start: 8px;
}

.file-ic {
  display: grid;
  place-items: center;
  flex: none;
  width: 34px;
  height: 34px;
  border-radius: var(--r2);
  background: var(--wash);
  font-family: var(--mono);
  font-size: 9.5px;
  font-weight: 600;
  color: var(--mute);
}

.file b {
  display: block;
  font-size: 13px;
  font-weight: 600;
}

.file span {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--mute);
}

.file-summary {
  font-size: 12px;
  color: var(--mute);
}

.file-summary.has-files {
  color: var(--ok);
  font-weight: 600;
}

/* ============================================================
   TABLES AND DATA
   ------------------------------------------------------------
   Values transcribed from documents are set in mono, so a buyer can
   tell at a glance what came off paper and what is ours.
   ============================================================ */

.tbl-wrap {
  border: 1px solid var(--line);
  border-radius: var(--r4);
  overflow-x: auto;
  background: var(--surface);
}

.tbl {
  width: 100%;
  min-width: 640px;
  border-collapse: collapse;
}

.tbl th {
  padding: 12px var(--s4);
  background: var(--wash);
  border-bottom: 1px solid var(--line);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--mute);
  text-align: start;
  white-space: nowrap;
}

.tbl td {
  padding: 14px var(--s4);
  border-bottom: 1px solid var(--line);
  font-size: 13.5px;
  vertical-align: middle;
}

.tbl tr:last-child td {
  border-bottom: 0;
}

.tbl tbody tr {
  transition: background var(--fast) var(--ease);
}

.tbl tbody tr:hover {
  background: #fafbfc;
}

.tbl .num {
  font-family: var(--mono);
  font-size: 12.5px;
  letter-spacing: -0.02em;
  text-align: end;
}

.tbl .co b {
  display: block;
  font-size: 13.5px;
  font-weight: 600;
}

.tbl .co span {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--mute);
}

.tbl-toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  align-items: center;
  padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--line);
}

.tbl-foot {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  justify-content: space-between;
  align-items: center;
  padding: var(--s3) var(--s4);
  border-top: 1px solid var(--line);
  font-size: 12.5px;
  color: var(--mute);
}

.pager {
  display: flex;
  gap: 4px;
}

.pager button {
  width: 30px;
  height: 30px;
  border-radius: var(--r1);
  border: 1px solid var(--line);
  background: var(--surface);
  font-family: var(--mono);
  font-size: 12.5px;
  cursor: pointer;
}

.pager button[aria-current] {
  background: var(--ink);
  color: #fff;
  border-color: var(--ink);
}

/* ============================================================
   NAVIGATION PRIMITIVES
   ============================================================ */

/* The interface language control. Every surface that shows navigation
   shows this, so it is a primitive rather than a public-site component.

   Each entry links to the same page in that language: switching language
   never drops the reader back onto the home page. The active language
   carries aria-current and is not a link to itself. The glyphs are the
   language's own — ع and 中 — because a reader looking for their own
   language recognises it faster than a translated name or a flag, and a
   flag would claim a country rather than a language. */
.lang {
  display: inline-flex;
  align-items: center;
  gap: 2px;
  font-family: var(--mono);
  font-size: 11.5px;
  letter-spacing: -0.01em;
}

.lang-on,
.lang-off {
  padding: 3px 6px;
  border-radius: var(--r1);
  text-decoration: none;
  transition: color var(--fast) var(--ease), background-color var(--fast) var(--ease);
}

.lang-on {
  color: #fff;
  background: var(--ink-3);
}

.lang-off {
  color: var(--mute-2);
}

.lang-off:hover,
.lang-off:focus-visible {
  color: #fff;
  background: var(--ink-2);
}

/* The ع and 中 glyphs pick up their own script's face automatically:
   each anchor carries lang, and the :lang() rules in SCRIPTS above beat
   the inherited mono face. Nothing more is needed here. */

.tabs {
  display: flex;
  gap: var(--s5);
  border-bottom: 1px solid var(--line);
  overflow-x: auto;
}

.tabs a,
.tabs button {
  padding: 0 0 11px;
  border: 0;
  background: transparent;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--mute);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  white-space: nowrap;
  cursor: pointer;
  transition: color var(--fast) var(--ease);
}

.tabs a:hover,
.tabs button:hover {
  color: var(--ink);
}

.tabs a[aria-current],
.tabs button[aria-current] {
  color: var(--ink);
  border-bottom-color: var(--ink);
}

.crumb {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 12.5px;
  color: var(--mute);
  flex-wrap: wrap;
}

.crumb a {
  text-decoration: none;
}

.crumb a:hover {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 3px;
}

.crumb b {
  color: var(--ink);
  font-weight: 600;
}

.seg {
  display: inline-flex;
  background: var(--wash);
  border-radius: var(--r2);
  padding: 3px;
  gap: 2px;
}

.seg button,
.seg a {
  border: 0;
  background: transparent;
  padding: 6px 13px;
  border-radius: 6px;
  font-size: 12.5px;
  font-weight: 600;
  color: var(--mute);
  text-decoration: none;
  cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}

.seg button[aria-pressed="true"],
.seg a[aria-current] {
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--e1);
}

/* Three steps, and the buyer can stop after the first. */
.step {
  display: flex;
  align-items: center;
  gap: var(--s4);
  flex-wrap: wrap;
}

.step > div {
  display: flex;
  align-items: center;
  gap: 9px;
  font-size: 13px;
  font-weight: 500;
  color: var(--mute);
}

.step b {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--wash);
  font-family: var(--mono);
  font-size: 11px;
  color: var(--mute);
}

.step > div.is-active,
.step > div[data-on] {
  color: var(--ink);
  font-weight: 600;
}

.step > div.is-active b,
.step > div[data-on] b {
  background: var(--ink);
  color: #fff;
  border-color: var(--ink);
}

.step .bar {
  flex: 1;
  min-width: 16px;
  height: 1px;
  background: var(--line);
}

/* ============================================================
   FEEDBACK
   ------------------------------------------------------------
   Alerts state the finding, then the action. They never open with
   an apology and never hedge. The colour matches the verdict system
   exactly — a buyer never learns a second colour language.
   ============================================================ */

.alert {
  display: flex;
  gap: var(--s3);
  align-items: flex-start;
  padding: var(--s4);
  border-radius: var(--r3);
  border: 1px solid;
}

.alert .ic {
  width: 19px;
  height: 19px;
  stroke-width: 1.7;
  margin-block-start: 1px;
}

.alert b {
  display: block;
  font-size: 13.5px;
  font-weight: 700;
  margin-block-end: 3px;
}

.alert p {
  font-size: 13px;
  line-height: 1.55;
}

.alert--info {
  background: var(--rev-bg);
  border-color: var(--rev-line);
  color: #12358f;
}

.alert--info .ic {
  stroke: var(--rev);
}

.alert--gap {
  background: var(--gap-bg);
  border-color: var(--gap-line);
  color: var(--gap);
}

.alert--gap .ic {
  stroke: var(--gap);
}

.alert--flag {
  background: var(--miss-bg);
  border-color: var(--miss-line);
  color: var(--miss);
}

.alert--flag .ic {
  stroke: var(--miss);
}

.alert--ok {
  background: var(--ok-bg);
  border-color: var(--ok-line);
  color: var(--ok);
}

.alert--ok .ic {
  stroke: var(--ok);
}

.alert--neutral {
  background: var(--wash);
  border-color: var(--line);
  color: var(--ink-3);
}

.alert--neutral .ic {
  stroke: var(--mute);
}

.toast {
  display: flex;
  gap: var(--s3);
  align-items: center;
  max-width: 430px;
  padding: 13px var(--s4);
  border-radius: var(--r3);
  background: var(--ink);
  color: #fff;
  box-shadow: var(--e2);
}

.toast p {
  font-size: 13.5px;
}

.modal {
  background: var(--surface);
  border-radius: var(--r5);
  box-shadow: var(--e2);
  max-width: 480px;
  overflow: hidden;
}

.modal-b {
  padding: var(--s5);
}

.modal-b h3 {
  font-size: 19px;
  font-weight: 700;
  letter-spacing: -0.02em;
  margin-block-end: 8px;
}

.modal-b p {
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--mute);
}

.modal-f {
  display: flex;
  gap: var(--s2);
  justify-content: flex-end;
  padding: var(--s4) var(--s5);
  background: var(--wash);
  border-top: 1px solid var(--line);
}

/* ============================================================
   EMPTY AND LOADING
   ------------------------------------------------------------
   An empty screen explains the one thing to do next, in the same
   words used on the button that does it. Skeletons match the shape
   of what is coming; no progress bar is faked.
   ============================================================ */

.empty {
  text-align: center;
  padding: var(--s8) var(--s5);
  border: 1px solid var(--line);
  border-radius: var(--r4);
  background: var(--surface);
}

.empty .ledger {
  max-width: 190px;
  margin: 0 auto var(--s5);
  opacity: 0.5;
}

.empty h4 {
  font-size: 17px;
  font-weight: 700;
  margin-block-end: 8px;
}

.empty p {
  font-size: 13.5px;
  color: var(--mute);
  max-width: 44ch;
  margin: 0 auto var(--s5);
}

.skel {
  height: 12px;
  border-radius: var(--r1);
  background: linear-gradient(90deg, var(--wash) 25%, #e9edf2 50%, var(--wash) 75%);
  background-size: 200% 100%;
  animation: trillions-shimmer 1.4s linear infinite;
}

@keyframes trillions-shimmer {
  to {
    background-position: -200% 0;
  }
}

/* The shimmer is the one animation with a direction. It has to travel the
   way the text is read, so right-to-left it runs the other way. */
[dir="rtl"] .skel {
  animation-name: trillions-shimmer-rtl;
}

@keyframes trillions-shimmer-rtl {
  to {
    background-position: 200% 0;
  }
}

/* ============================================================
   FINDINGS
   ------------------------------------------------------------
   What did not hold up first, what matched second, what we could
   not check last — and never hidden. Every finding names its source
   document and the date it was read.
   ============================================================ */

.finding {
  display: grid;
  grid-template-columns: 26px minmax(0, 1fr) 150px;
  gap: var(--s4);
  padding: var(--s4) 0;
  border-bottom: 1px solid var(--line);
  align-items: start;
}

.finding:last-child {
  border-bottom: 0;
}

.finding .dot {
  width: 11px;
  height: 11px;
  border-radius: 3px;
  margin-block-start: 5px;
}

.finding .dot[data-s="ok"] {
  background: var(--ok);
}

.finding .dot[data-s="gap"] {
  border: 1.5px dashed var(--gap);
}

.finding .dot[data-s="miss"] {
  border: 1.5px solid var(--miss);
}

.finding .dot[data-s="nil"] {
  background: var(--nil-bg);
}

.finding h5 {
  font-size: 14px;
  font-weight: 600;
  margin-block-end: 3px;
}

.finding p {
  font-size: 13px;
  line-height: 1.55;
  color: var(--mute);
}

.finding .src {
  font-family: var(--mono);
  font-size: 11px;
  line-height: 1.5;
  color: var(--mute);
}

@media (max-width: 720px) {
  .finding {
    grid-template-columns: 26px minmax(0, 1fr);
  }

  .finding .src {
    grid-column: 2;
  }
}

/* The upload card and the definition list the journey is built from.
   Shared for the same reason the journey is. */
.supplier-upload-card {
  display: grid;
  gap: 6px;
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r3);
  background: var(--surface);
}

.supplier-upload-card > b {
  font-size: 13px;
  font-weight: 600;
}

.supplier-upload-card input[type="file"] {
  font-size: 12.5px;
  color: var(--mute);
}

.defs {
  border: 1px solid var(--line);
  border-radius: var(--r4);
  background: var(--surface);
  overflow: hidden;
}

.def {
  display: grid;
  grid-template-columns: 210px minmax(0, 1fr);
  gap: var(--s5);
  padding: var(--s4) var(--s5);
  border-bottom: 1px solid var(--line);
  align-items: start;
}

.def:last-child {
  border-bottom: 0;
}

.def dt,
.def > b {
  font-size: 13.5px;
  font-weight: 600;
  color: var(--ink);
}

.def dd,
.def > p {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.6;
  color: var(--mute);
}

/* The card a form is written on, and the row of controls that ends a step.
   Shared because the journey is asked on the public page and inside the
   buyer workspace, and a step whose actions do not lay out in one of them
   is a step whose Next button sits on top of its own label. */
.form-card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r4);
  padding: var(--s6);
  display: grid;
  gap: var(--s5);
}
.form-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  align-items: center;
}

/* ============================================================
   REQUEST JOURNEY
   The one page where the form is not a section of the page but
   the whole of it: no page head, no guidance rail, nothing to
   read past before answering. Everything below is scoped to
   .rq-form so the shared .choice, .supplier-upload-card and
   .form-actions primitives keep their behaviour everywhere else.

   request-quote.js writes data-rq-step on the form. That is the
   only channel the progress meter needs, and it keeps the fill
   width in this file — CSP forbids an inline style attribute.

   In the token layer because the journey is asked in two places
   now: on the public page, and inside the buyer workspace where a
   signed-in buyer raises the same request. The same five questions
   have to look and behave the same in both, or the desk is reading
   answers to two different forms.
   ============================================================ */

.rq-section {
  padding-block: var(--s7) var(--s8);
}

.rq-shell {
  max-width: 820px;
  margin-inline: auto;
}

.rq-form {
  padding: var(--s7);
  border-radius: var(--r5);
  box-shadow: var(--e2);
  gap: var(--s6);
}

/* ------------------------------------------------- the pinned shell

   The journey is one screen, not a document: the page is exactly a
   viewport tall and does not scroll, and the step body scrolls inside
   the card while the progress row and the actions stay put. Step 4's
   body alone is taller than a 900px viewport, so the page carries no
   footer — design-system-test.py exempts it by name.

   .rq-form is already the grid holding every step block, so it becomes
   the scroller and its two fixed pieces pin with position: sticky.
   That needs no wrapper element, which matters: the five
   data-journey-step blocks are asserted, and CSP forbids writing a
   measured height from the controller.

   Only above the two-column breakpoint and on a tall enough window.
   A pinned shell fights a phone's collapsing browser chrome, and a
   clipped step 4 on a short laptop is worse than a scrolling one.
   ------------------------------------------------------------ */

/* The gate was (981px, 700px), which let the shell engage on windows that
   could not hold the tallest step — a 1000x700 window pinned the card and
   then scrolled step 4 by 310px inside it, which is the worst of both
   arrangements. It engages only where the content measurably fits: 1100px
   is where the three-column step 4 below applies, and the height floor is
   where the tallest step clears with the fluid spacing — measured, not
   assumed. Under either, the page is an ordinary scrolling document and
   there is no scrollbar inside the card. */
@media (min-width: 1100px) and (min-height: 760px) {
  body[data-page="request-quote"] {
    height: 100vh;
    height: 100dvh;
    overflow: hidden;
    /* The card's own padding, bound once: the sticky progress row and
       action bar cancel it with negative offsets, so the four values have
       to move together or the pinned edges tear. Fluid against viewport
       height — a 1310px window keeps the full air, a 780px one gives it
       back to the content instead of to a scrollbar. */
    --rq-pad: clamp(14px, 2.2vh, 24px);
  }

  body[data-page="request-quote"] main {
    min-height: 0;
    display: flex;
  }

  body[data-page="request-quote"] .rq-section {
    flex: 1;
    min-height: 0;
    display: flex;
    /* The block padding moves inside the scroller below; left here it
       would spend viewport height the card can never use. */
    padding-block: 0;
  }

  body[data-page="request-quote"] .wrap,
  body[data-page="request-quote"] .rq-shell {
    display: flex;
    flex-direction: column;
    min-height: 0;
    width: 100%;
  }

  body[data-page="request-quote"] .rq-shell {
    /* Grows to the wrap's full height — without this the shell hugs its
       content and the auto margins below have no free space to share. */
    flex: 1;
    /* The floor the auto margins rest on: the card never touches the
       header or the viewport edge even when it fills the whole height. */
    padding-block: clamp(10px, 1.6vh, 24px);
  }

  body[data-page="request-quote"] .rq-form {
    min-height: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* Keeps a focused field off both clipped edges. */
    scroll-padding-block: var(--s6);
    /* Auto, not a fixed gap: a short step centres in the viewport instead
       of leaving all its spare height parked under the card, and a tall
       one collapses the margins to zero and scrolls. */
    margin-block: auto;
    /* The gap between a step's blocks scales with the window like the
       padding does. This is also the deterministic headroom that keeps
       fractional text heights from tipping a step a few pixels over its
       box — two gaps per step, so even the tightest window holds ~12px
       of slack against rounding. */
    gap: clamp(16px, 2.4vh, 32px);
  }

  body[data-page="request-quote"] .rq-progress,
  body[data-page="request-quote"] .rq-actions {
    position: sticky;
    z-index: 2;
    background: var(--surface);
  }

  body[data-page="request-quote"] .rq-form {
    padding: var(--rq-pad);
  }

  body[data-page="request-quote"] .rq-progress {
    inset-block-start: calc(var(--rq-pad) * -1);
    padding-block: var(--rq-pad) clamp(10px, 1.6vh, 16px);
    margin-block-start: calc(var(--rq-pad) * -1);
  }

  body[data-page="request-quote"] .rq-actions {
    inset-block-end: calc(var(--rq-pad) * -1);
    padding-block: clamp(12px, 1.8vh, 20px) var(--rq-pad);
    margin-block-end: calc(var(--rq-pad) * -1);
  }

  /* The one flexible block in the tallest step: rows="6" is the ceiling,
     not a demand, and a shorter window trims the textarea before it grows
     a scrollbar around the whole card. */
  body[data-page="request-quote"] .rq-form .textarea {
    min-height: clamp(88px, 14vh, 132px);
    height: clamp(88px, 14vh, 132px);
  }

  body[data-page="request-quote"] .rq-form .rq-ask {
    margin-block-end: clamp(8px, 1.4vh, 16px);
  }
}

/* ------------------------------------------------- fitting the steps

   The journey is meant to be read without scrolling, and step 4 is the
   step that breaks it: measured on a 1440x900 window it wanted 1132px
   inside a 769px box. Everything below buys that back without dropping
   a field or shortening a question.

   The largest single win is where the description sits. Stacked under a
   2x2 field grid it costs its own full height; beside them it costs
   nothing, because the four short fields are already two rows tall and
   the column simply fills that. Needs the width to exist, so it is
   confined to viewports wide enough to spare a third column.
   ------------------------------------------------------------ */

@media (min-width: 1100px) {
  body[data-page="request-quote"] .form-grid[data-journey-step="4"] {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.25fr);
  }

  body[data-page="request-quote"] .form-grid[data-journey-step="4"] .field-full {
    grid-column: 3;
    grid-row: 1 / span 2;
    display: flex;
    flex-direction: column;
  }

  /* The textarea takes the height the two field rows already occupy
     instead of dictating its own, so rows="6" stops setting the step's
     height. min-height: 0 is what lets a flex child actually shrink. */
  body[data-page="request-quote"] .form-grid[data-journey-step="4"] .field-full .textarea {
    flex: 1;
    min-height: 118px;
  }
}

/* The remaining cuts are rhythm, not layout: this page is a fixed frame
   rather than a document, so it does not need a document's air. */

body[data-page="request-quote"] .rq-form .choice {
  /* 9px, not a token: the choice rows are the tallest repeated element on
     steps 1-4, and the odd step measured a few fractional pixels over its
     box from wrapped hint lines. Sub-token trim absorbs that rounding so a
     step never grows a scrollbar for 6px. */
  padding: 9px var(--s3);
}

body[data-page="request-quote"] .form-grid {
  gap: var(--s3) var(--s4);
}

body[data-page="request-quote"] .rq-hintrow {
  margin-block-start: 2px;
}

/* Step 5 is seven rows of two words each. It read as a document table;
   at this density it still reads, and stops being the reason a summary
   needs scrolling. */
.rq-summary .def {
  padding: var(--s3) var(--s5);
}

/* A buyer who chose to attach adds a drop zone to the step, which is the
   one path that still would not fit. Centred and stacked it was five rows
   tall; laid out around the icon it says the same things in two, and
   stays a full-width click and drop target either way. */
.rq-form .rq-drop {
  grid-template-columns: auto minmax(0, 1fr);
  justify-items: start;
  align-items: center;
  column-gap: var(--s4);
  text-align: start;
}

.rq-form .rq-upload {
  padding: var(--s4);
}

.rq-form .rq-drop .rq-drop-ic {
  grid-row: 1 / span 2;
}

.rq-form .rq-drop input[type="file"],
.rq-form .rq-drop .file-summary {
  grid-column: 2;
}

/* ------------------------------------------------ the status message

   It floats over the top of the page rather than sitting in the form's
   flow. Inside the pinned shell the card scrolls under fixed furniture,
   so a message parked between two step blocks can be announced somewhere
   the buyer is not looking — which is how "Internal server error" got
   read as "nothing happened". Fixed to the top centre, it is in the same
   place whatever the card is doing.

   Scoped to this page: .form-status is a shared primitive and the two
   other forms that use it are ordinary scrolling documents where an
   inline message is exactly right.
   ------------------------------------------------------------ */

body[data-page="request-quote"] .form-status.visible {
  position: fixed;
  inset-block-start: calc(var(--s4) + 65px);   /* clear of the sticky header */
  /* Centred by auto margins inside a full-width inset rather than by a 50%
     offset and a translate: inset-inline-start resolves to `right` under
     dir="rtl" while translateX stays physical, so that pair centres the
     English page and pushes the Arabic one off to the side. This holds in
     both directions without knowing which one it is in. */
  inset-inline: 0;
  margin-inline: auto;
  width: max-content;
  max-width: min(560px, calc(100vw - var(--s6) * 2));
  z-index: 200;                                /* over .site-head's 100 */
  box-shadow: var(--e2);
  text-align: center;
}

@media (max-width: 620px) {
  body[data-page="request-quote"] .form-status.visible {
    margin-inline: var(--s4);
    width: auto;
    max-width: none;
  }
}

/* ---------------------------------------------------------- progress */

.rq-progress {
  display: grid;
  gap: var(--s4);
}

.rq-steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  justify-content: space-between;
  gap: var(--s2);
}

.rq-step {
  appearance: none;
  border: 0;
  background: none;
  padding: 0;
  font: inherit;
  color: var(--mute-2);
  display: flex;
  align-items: center;
  gap: var(--s2);
  cursor: pointer;
  transition: color var(--fast) var(--ease);
}

.rq-step span {
  font-size: 12.5px;
  font-weight: 500;
  white-space: nowrap;
}

.rq-step b {
  display: grid;
  place-items: center;
  flex: none;
  width: 28px;
  height: 28px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: var(--wash);
  font-family: var(--mono);
  font-size: 11.5px;
  font-weight: 500;
  color: var(--mute);
  transition: background var(--base) var(--ease), color var(--base) var(--ease),
    border-color var(--base) var(--ease), box-shadow var(--base) var(--ease);
}

.rq-step:hover {
  color: var(--ink);
}

/* A step the buyer has not reached is not a shortcut past the step
   they are on, so it reads as a marker rather than a control. */
.rq-step[disabled] {
  cursor: default;
  color: var(--mute-3);
}

.rq-step[disabled] b {
  color: var(--mute-3);
}

.rq-step:focus-visible {
  outline: none;
}

.rq-step:focus-visible b {
  box-shadow: 0 0 0 3px rgba(15, 23, 42, 0.16);
}

.rq-steps .is-active .rq-step {
  color: var(--ink);
  font-weight: 600;
}

.rq-steps .is-active .rq-step b {
  background: var(--ink);
  border-color: var(--ink);
  color: #fff;
}

/* A finished step is ink like the current one, but wears the tick
   instead of its number, so "where I am" and "what I answered" are
   never the same mark. */
.rq-steps .is-done .rq-step {
  color: var(--ink-3);
}

.rq-steps .is-done .rq-step b {
  background-color: var(--ink);
  border-color: var(--ink);
  color: transparent;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='white' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'><path d='M2 6.4 4.7 9.2 10 3.2'/></svg>");
  background-repeat: no-repeat;
  background-position: center;
}

.rq-meter {
  block-size: 3px;
  border-radius: var(--pill);
  background: var(--line);
  overflow: hidden;
}

.rq-meter i {
  display: block;
  block-size: 100%;
  inline-size: 20%;
  border-radius: var(--pill);
  background: var(--ink);
  transition: inline-size var(--slow) var(--ease);
}

/* One rule per (step, total) pair. The fill is a fraction of two numbers the
   controller knows and this file does not, and CSP forbids writing the width
   onto the element — so the pairs are enumerated. Five for the buyer's
   journey, three for the supplier's registration. */
[data-rq-total="5"][data-rq-step="1"] .rq-meter i { inline-size: 20%; }
[data-rq-total="5"][data-rq-step="2"] .rq-meter i { inline-size: 40%; }
[data-rq-total="5"][data-rq-step="3"] .rq-meter i { inline-size: 60%; }
[data-rq-total="5"][data-rq-step="4"] .rq-meter i { inline-size: 80%; }
[data-rq-total="5"][data-rq-step="5"] .rq-meter i { inline-size: 100%; }

[data-rq-total="3"][data-rq-step="1"] .rq-meter i { inline-size: 33.333%; }
[data-rq-total="3"][data-rq-step="2"] .rq-meter i { inline-size: 66.667%; }
[data-rq-total="3"][data-rq-step="3"] .rq-meter i { inline-size: 100%; }

[data-rq-total="4"][data-rq-step="1"] .rq-meter i { inline-size: 25%; }
[data-rq-total="4"][data-rq-step="2"] .rq-meter i { inline-size: 50%; }
[data-rq-total="4"][data-rq-step="3"] .rq-meter i { inline-size: 75%; }
[data-rq-total="4"][data-rq-step="4"] .rq-meter i { inline-size: 100%; }

/* ---------------------------------------------------------- the ask */

.rq-form fieldset {
  display: grid;
  gap: var(--s4);
}

/* The label above and the question below were the wrong way round:
   the eyebrow was loud and the thing being asked was set at 12px.
   The question is now what the eye lands on.

   A legend is not a grid item — it renders in the fieldset's border,
   so the fieldset's gap never reaches it and the spacing under the
   eyebrow has to be its own margin. */
.rq-form legend.t-lab {
  margin-block-end: var(--s2);
}

.rq-form .rq-ask {
  font-size: clamp(17px, 1.5vw, 19px);
  line-height: 1.35;
  font-weight: 600;
  color: var(--ink);
  margin: 0;
}

.rq-form fieldset .grid {
  grid-template-columns: 1fr;
  gap: var(--s3);
}

.rq-form .choice {
  padding: var(--s4);
  gap: var(--s3);
  transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease),
    box-shadow var(--fast) var(--ease);
}

.rq-form .choice:hover {
  box-shadow: var(--e1);
}

.rq-form .choice:has(input:checked) {
  box-shadow: inset 0 0 0 1px var(--ink);
}

.rq-form .choice:has(input:focus-visible) {
  border-color: var(--ink);
  box-shadow: 0 0 0 3px rgba(15, 23, 42, 0.16);
}

.rq-form .choice strong {
  font-size: 14.5px;
}

/* ---------------------------------------------------------- fields */

.rq-form .form-grid {
  gap: var(--s5) var(--s4);
}

/* Step 1's only field is revealed by one answer out of three. With
   every child hidden the grid is still a row, and an empty row above
   the action bar reads as something that failed to load. */
.rq-form .form-grid:not(:has(> :not([hidden]))) {
  display: none;
}

.rq-form .input,
.rq-form .select,
.rq-form .textarea {
  height: 46px;
  border-radius: var(--r3);
}

.rq-form .textarea {
  height: auto;
  min-height: 148px;
}

.rq-form .field > label {
  font-size: 13px;
}

.rq-hintrow {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  align-items: baseline;
  justify-content: space-between;
}

/* "1 240 / 5000" and "2.3 MB" are Latin measurements. Left to the
   bidi algorithm inside an Arabic paragraph they come out reversed —
   "MB 2.3" — so each one is isolated as the LTR run it is. */
/* "1 240 / 5000", "2.3 MB" and a file name are Latin runs. Left to the
   bidi algorithm inside an Arabic row they come out reversed — "MB 2.3",
   and a name ending in a bracket or a dot loses its punctuation to the
   far end — so each is isolated as the run it is. A name can hold Arabic
   too, so it resolves its own direction rather than being forced to ltr. */
.rq-count,
.rq-file span {
  direction: ltr;
  unicode-bidi: isolate;
}

.rq-file b {
  unicode-bidi: plaintext;
}

.rq-count {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--mute-2);
  white-space: nowrap;
}

/* ---------------------------------------------------------- uploads */

/* Every row the height of the tallest row.

   Stretching only equalises the cards that share a row, so the answer to
   one ragged row was still five cards over three rows of three heights —
   and a title that wraps to a second line in one language and not another
   moved the line between them. An implicit row sized 1fr in a container
   that has no height of its own resolves to the largest row's content, so
   all of them take that height and every card in the block matches. */
.grid:has(> .rq-upload) {
  grid-auto-rows: 1fr;
}

/* One column, explicitly zero-floored.
   
   An implicit grid column is sized `auto`, which resolves against
   max-content — so a long file name made this box's contents 495px wide
   inside a 353px column, and the list painted straight across the card
   beside it. minmax(0, 1fr) is what lets the row truncate instead of
   pushing. The card it holds does the same for its own second column. */
/* The card is the wrapper, not the label inside it.

   The label carried the border, and the list of attached files sat outside
   it as a sibling. Stacked on a phone that put a row nearer the card below
   it than the card it belonged to — a file attached to the licence reading
   as if it were the catalogue's. The border moved out one level, so the
   files are inside the box that asked for them and the whole box is the
   drop target it looks like.

   One column, explicitly zero-floored: an implicit column is sized `auto`,
   which resolves against max-content, so a long file name made this box's
   contents wider than its own column and painted across the card beside it. */
.rq-upload {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s3);
  /* One height across the row, and the words at the top of it.
  
     These two go together. Stretching alone was the first attempt and it
     spread an empty card's own words apart to fill the height a filled
     card beside it had claimed; packing to the start alone left five cards
     of five different heights with ragged gaps between the rows. Stretch
     the box, pack its rows — so the card that is holding nothing is the
     same size as the one beside it and still reads from the top. */
  align-content: start;
  padding: var(--s5) var(--s4);
  border: 1.5px dashed var(--line-strong);
  border-radius: var(--r4);
  background: var(--wash);
  transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease);
}

.rq-upload:has(.rq-drop:hover),
.rq-upload:has(.rq-drop.is-dragging) {
  border-color: var(--ink);
  background: var(--surface);
}

.rq-upload:has(.rq-drop.is-dragging) .rq-drop-ic {
  color: var(--ink);
}

.rq-upload:has(input:focus-visible) {
  border-color: var(--ink);
  box-shadow: 0 0 0 3px rgba(15, 23, 42, 0.16);
}

/* A required card that was still empty when the form tried to move past it.
   main.js sets this and clears it the moment a file lands, so the mark
   outlives the attempt that raised it rather than going with the step. It
   sits above .has-files deliberately: a card cannot be both, and if it ever
   were, still-missing is the more useful of the two things to say. */
.rq-upload.is-missing {
  border-color: var(--miss);
  background: var(--miss-bg);
}

/* A file the card could not take, named where it was chosen. Not the refusal
   colour: nothing failed here, one attachment was left out and the registration
   still goes. */
.rq-file-note {
  grid-column: 1 / -1;
  margin: 0;
  padding: var(--s2) var(--s3);
  border-block-start: 1px solid var(--line);
  font-size: 12px;
  line-height: 1.5;
  color: var(--ink-2);
  unicode-bidi: plaintext;
}

/* A registration number whose check character does not agree. The same mark the
   declarations and the upload cards carry, on the control rather than on a box
   around it: a text field is its own edge. */
.field.is-missing > .input {
  border-color: var(--miss);
  background: var(--miss-bg);
}

.field.is-missing > label {
  color: var(--miss);
}

/* A card with something in it stops asking. The dashed edge is an
   invitation to drop; once a file is there the edge is solid and the card
   reads as done rather than as still waiting. */
.rq-upload.has-files {
  border-style: solid;
  border-color: var(--ok-line);
  background: var(--surface);
}

.rq-upload.has-files .rq-drop-ic {
  color: var(--ok);
}

/* The label is the click and the drop target and nothing else now: the box
   around it is what a supplier sees. .supplier-upload-card is the shared
   primitive and still draws its own box everywhere else, so the card that
   is also a drop target gives that box back rather than nesting a second
   one inside the first. */
.rq-drop {
  justify-items: center;
  gap: var(--s2);
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
  text-align: center;
  cursor: pointer;
}

.rq-drop-ic {
  width: 28px;
  height: 28px;
  color: var(--mute-2);
}

.rq-drop > b {
  font-size: 14.5px;
}

/* The native control is the drop target and the click target; the
   label around it already says everything it would have said. */
.rq-drop input[type="file"] {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
}

/* Inside the card, under a rule rather than across a gap: these are that
   card's files and nothing else's. */
.rq-files {
  list-style: none;
  margin: 0;
  padding: var(--s3) 0 0;
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--s2);
  border-top: 1px solid var(--line);
  /* Room for one row, held whether or not a file is in it.

     A .rq-file is exactly this tall — a 26px remove button between 10px of
     padding and 1px of border on each side, and a name that ellipses rather
     than wraps. Reserving it here is what keeps a card the same size before
     and after the first attachment: the space the row will occupy is already
     part of the box, so nothing below it moves when the row arrives.

     Everything here is border-box, so the reservation has to name the rule
     and the padding above the row as well as the row itself — 48px alone
     left the card 13px short and it still grew on the first upload. */
  min-height: calc(48px + var(--s3) + 1px);
}

/* Still holding its place, no longer drawing a line across nothing: the
   reserved space stays in the box and the rule that separates files from
   the words above them only appears once there are files to separate. */
.rq-files:empty {
  border-top-color: transparent;
}

.rq-file {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: 10px var(--s3);
  border: 1px solid var(--line);
  border-radius: var(--r3);
  background: var(--surface);
}

.rq-file b {
  flex: 1;
  min-width: 0;
  font-size: 13px;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.rq-file span {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--mute-2);
  white-space: nowrap;
}

.rq-file button {
  appearance: none;
  flex: none;
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  font-size: 19px;
  line-height: 1;
  color: var(--mute-2);
  cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}

.rq-file button:hover,
.rq-file button:focus-visible {
  background: var(--wash);
  color: var(--miss);
}

/* ---------------------------------------------------------- review */

.rq-summary {
  border-radius: var(--r3);
}

.rq-summary .def {
  grid-template-columns: 190px minmax(0, 1fr);
  gap: var(--s4);
  padding: var(--s3) var(--s4);
}

.rq-summary dt {
  font-size: 12.5px;
  font-weight: 500;
  color: var(--mute);
}

.rq-summary dd {
  font-size: 13.5px;
  font-weight: 500;
  color: var(--ink);
  /* Every value here was typed by the person reading it back, so the column
     holds both directions at once: an Arabic industry beside a Latin company
     name beside "500 units, 35 days". plaintext resolves each value from its
     own first strong character, which is the only rule that gets all three
     right — under the paragraph's rtl the quantity came back as
     "units, 35 days 500". */
  unicode-bidi: plaintext;
}

.rq-gate {
  display: grid;
  gap: var(--s4);
}

.rq-gate .btn-row {
  margin: 0;
}

/* ---------------------------------------------------------- actions */

.rq-actions {
  gap: var(--s3);
  padding-block-start: var(--s5);
  border-block-start: 1px solid var(--line);
}

.rq-actions .hint {
  margin-inline-end: auto;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--mute-2);
}

/* ---------------------------------------------------------- motion */

@media (prefers-reduced-motion: no-preference) {
  .rq-form [data-journey-step]:not([hidden]) {
    animation: rq-enter var(--base) var(--ease) both;
  }
}

@keyframes rq-enter {
  from {
    opacity: 0;
    transform: translateY(6px);
  }
}

/* ---------------------------------------------------------- narrow */

@media (max-width: 760px) {
  .rq-section {
    padding-block: var(--s5) var(--s7);
  }

  .rq-form {
    padding: var(--s5) var(--s4);
    gap: var(--s5);
    border-radius: var(--r4);
  }

  /* Below this width the labels cannot all sit on one line, so the
     row keeps the numbers and the meter and the action bar says
     which step they are on. */
  .rq-step span {
    display: none;
  }

  .rq-steps {
    justify-content: space-between;
  }

  .rq-summary .def {
    grid-template-columns: 1fr;
    gap: 2px;
  }

  .rq-actions {
    position: sticky;
    inset-block-end: 0;
    z-index: 2;
    background: var(--surface);
    padding-block: var(--s3);
  }

  .rq-actions .btn {
    flex: 1;
    justify-content: center;
  }

  .rq-actions .hint {
    flex-basis: 100%;
    margin-inline-end: 0;
  }
}

/* ============================================================
   METRIC CARDS
   ------------------------------------------------------------
   A count, what it counts, and the list it opens. Numbers in
   mono, because they came off the database rather than out of a
   copywriter.

   In the token layer because both signed-in surfaces show counts:
   the operations desk counts what is on the desk, the buyer
   workspace counts what is theirs. The card is the same card —
   what differs is only which rows were counted, and that is not
   a reason for two stylesheets to describe one component.
   ============================================================ */

.dashboard-metrics {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  gap: var(--s3);
}

/* What is being counted, its icon and the arrow share the top line; the
   count sits under them. The card used to spell its destination out —
   "Open supplier directory" beneath "Total suppliers" — which named the
   same directory twice and made every card taller than its number needed.
   The words are kept for a screen reader, which has no arrow to read. */
.metric {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-areas:
    "icon label arrow"
    "value value value";
  align-items: center;
  /* Labels are not all one line long — "Offers released" wraps in Arabic where
     "Invoices" does not — and the cards are stretched to a common height by
     the grid around them. Pushing the value row to the bottom keeps every
     number on one line across the row instead of stepping down at whichever
     card had the longest label. */
  align-content: space-between;
  gap: var(--s2);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r3);
  background: var(--surface);
  text-align: start;
  cursor: pointer;
  transition: border-color var(--fast) var(--ease);
}

.metric:hover {
  border-color: var(--mute-2);
}

/* The icon names the directory the card opens — the same mark the rail
   uses for it, so the two read as one destination. */
.metric-icon {
  grid-area: icon;
  color: var(--mute-2);
}

.metric span {
  grid-area: label;
  font-size: 12px;
  color: var(--mute);
}

.metric strong {
  grid-area: value;
  font-family: var(--mono);
  font-size: 26px;
  font-weight: 600;
  letter-spacing: -0.04em;
  line-height: 1;
}

/* Each card opens a directory. The arrow says so before it is clicked, and
   only leans in once the card is under the pointer or the keyboard. */
.metric small {
  grid-area: arrow;
  font-size: 13px;
  line-height: 1;
  color: var(--mute-2);
}

.metric small::after {
  content: "\2192";
  display: inline-block;
  transition: transform var(--fast) var(--ease);
}

[dir="rtl"] .metric small::after {
  content: "\2190";
}

.metric:hover small,
.metric:focus-visible small {
  color: var(--ink);
}

.metric:hover small::after,
.metric:focus-visible small::after {
  transform: translateX(3px);
}

[dir="rtl"] .metric:hover small::after,
[dir="rtl"] .metric:focus-visible small::after {
  transform: translateX(-3px);
}

@media (prefers-reduced-motion: reduce) {
  .metric small::after {
    transition: none;
  }

  .metric:hover small::after,
  .metric:focus-visible small::after {
    transform: none;
  }
}

.metric-attention {
  border-color: var(--miss-line);
  background: var(--miss-bg);
}

.metric-attention strong,
.metric-attention span,
.metric-attention small,
.metric-attention .metric-icon {
  color: var(--miss);
}

/* Once the cards are one to a row the number no longer needs the width it
   was given to stand apart from its neighbours. */
@media (max-width: 620px) {
  .metric strong {
    font-size: 24px;
  }
}


/* ============================================================
   APPLICATION TOP BAR
   ------------------------------------------------------------
   Brand on one side, the signed-in person on the other, and one
   toggle for whatever destinations the app below it has. It
   carries no destinations itself — a bar is a row, and a row is
   the wrong shape for a list.

   In the token layer because both signed-in surfaces wear it: the
   operations dashboard and the buyer workspace. An operator and a
   buyer are different people with different rights, and the
   control that says who you are signed in as should not be a
   different control depending on which of them you are.

   Each app supplies only its own placement — where the bar sits
   in its shell, and what its toggle opens.
   ============================================================ */

.app-topbar {
  position: sticky;
  top: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  gap: var(--s5);
  padding: var(--s3) var(--s5);
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}

/* The mark, then the name, then which of the two apps you are in. The
   mark is the same .tile every other page wears, at the small size — a
   signed-in surface is not a different company from the one that sent
   you here, and the bar is the only place left on these pages where the
   brand appears at all. */

.topbar-brand {
  display: flex;
  align-items: center;
  gap: 9px;
  color: var(--ink);
  white-space: nowrap;
  /* A brand lockup is never underlined, including when it is the way home. */
  text-decoration: none;
}

.topbar-brand b {
  font-size: 17px;
  font-weight: 800;
  letter-spacing: -0.03em;
}

.topbar-brand strong {
  font-family: var(--mono);
  font-size: 11.5px;
  font-weight: 500;
  color: var(--mute-2);
}

/* One control, the meaning the width calls for: it collapses the rail to
   icons on a wide screen, and opens the drawer once the rail has left the
   layout. Two buttons doing halves of the same job would be worse. */

.topbar-nav-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r2);
  background: transparent;
  color: var(--ink);
  font-size: 15px;
  line-height: 1;
  cursor: pointer;
  transition: background var(--fast) var(--ease);
}

.topbar-nav-toggle:hover {
  background: var(--wash);
}

.topbar-session {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin-inline-start: auto;
  white-space: nowrap;
}

/* ------------------------------------------------------------
   Profile control
   One target on the bar. Everything an operator does about their
   own account — read the record, review access, open a support
   session, sign out — hangs off it, so no personal action is ever
   confused with an operational destination in the rail.
   ------------------------------------------------------------ */

.user-trigger {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 4px 8px 4px 4px;
  border: 1px solid transparent;
  border-radius: 999px;
  background: transparent;
  cursor: pointer;
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease);
}

.user-trigger:hover,
.user-trigger[aria-expanded="true"] {
  background: var(--wash);
  border-color: var(--line-strong);
}

.user-avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  flex: none;
  border-radius: 999px;
  background: var(--ink);
  color: #fff;
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.02em;
}

.user-identity {
  display: grid;
  gap: 1px;
  text-align: start;
  line-height: 1.25;
}

.user-identity > span:first-child {
  font-size: 13px;
  font-weight: 500;
  color: var(--ink);
  max-width: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
}

.user-caret {
  font-size: 10px;
  color: var(--mute-2);
}

.topbar-role {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--mute-2);
}

.user-menu {
  position: absolute;
  top: calc(100% + 8px);
  inset-inline-end: 0;
  z-index: 60;
  display: grid;
  gap: 1px;
  min-width: 224px;
  padding: 6px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r3);
  box-shadow: 0 8px 24px rgb(15 23 42 / 0.28);
}

.user-menu-item {
  padding: 9px 10px;
  border: 0;
  border-radius: var(--r2);
  background: transparent;
  color: var(--ink);
  font-size: 13.5px;
  font-weight: 500;
  text-align: start;
  cursor: pointer;
  transition: background var(--fast) var(--ease);
}

.user-menu-item:hover {
  background: var(--wash);
}

.user-menu-item--end {
  margin-block-start: 5px;
  padding-block-start: 12px;
  border-top: 1px solid var(--line);
  border-radius: 0 0 var(--r2) var(--r2);
}

/* ============================================================
   APPLICATION SHELL AND RAIL
   Eight destinations, one column, in a fixed order an operator
   learns once. Three things name each one, and each survives a
   different loss: the glyph for recognition at a glance, the word
   for anyone who does not yet know the glyph, and the number
   because that is how a queue gets referred to out loud between
   two operators.

   Collapsed, only the glyph is left on screen — the word is still
   in the accessible name and returns as a flyout on hover or on
   focus. A glyph with no way back to its word would be a puzzle.

   In the token layer because both signed-in surfaces wear it. An
   operator's rail holds eight destinations and a buyer's holds
   four, but a rail is a rail: same column, same collapse, same
   drawer once the screen is too narrow to hold it.
   ============================================================ */

.ops-rail {
  position: sticky;
  top: var(--app-topbar);
  height: calc(100vh - var(--app-topbar));
  display: grid;
  align-content: start;
  gap: 2px;
  overflow-y: auto;
  /* Flush to the page edge and hard against the bar, the same negative
     margin .app-topbar uses to escape the wrap padding. It reads as one
     side of the shell rather than a card that happens to hold links. */
  margin-inline-start: calc(var(--s5) * -1);
  padding: var(--s4) var(--s3);
  background: var(--surface);
  border-inline-end: 1px solid var(--line);
}

.ops-rail-link {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 9px 10px;
  border: 1px solid transparent;
  border-radius: var(--r2);
  background: transparent;
  color: var(--ink);
  font-size: 13px;
  font-weight: 500;
  text-align: start;
  cursor: pointer;
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}

.ops-rail-link:hover {
  background: var(--wash);
}

.ops-rail-link.is-active {
  background: var(--ink);
  color: #fff;
  font-weight: 600;
}

/* currentColor throughout: the glyph darkens with its row and reverses
   with the active state, and never carries a verdict of its own. */

.ops-rail-icon {
  color: var(--mute);
}

.ops-rail-link:hover .ops-rail-icon {
  color: var(--ink);
}

.ops-rail-link.is-active .ops-rail-icon {
  color: #fff;
}

.ops-rail-label {
  flex: 1;
  min-width: 0;
}

.ops-rail-index {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--mute);
}

.ops-rail-link.is-active .ops-rail-index {
  color: var(--on-ink-2);
}

/* Collapsed: glyphs only, and the work area takes the width back.
   Scoped above the drawer breakpoint on purpose — below it the rail is
   already a drawer you opened deliberately, and a drawer that hides its
   words would be answering a question nobody asked. */

@media (min-width: 901px) {
  .app-shell.is-rail-collapsed {
    grid-template-columns: 68px minmax(0, 1fr);
  }

  /* Eight rows need no scrolling at this height, so the rail can stop
     clipping and let a flyout out. */
  .is-rail-collapsed .ops-rail {
    overflow: visible;
    padding-inline: var(--s2);
  }

  .is-rail-collapsed .ops-rail-link {
    justify-content: center;
    padding-inline: 0;
  }

  .is-rail-collapsed .ops-rail-icon {
    width: 20px;
    height: 20px;
  }

  .is-rail-collapsed .ops-rail-index {
    display: none;
  }

  .is-rail-collapsed .ops-rail-label {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
  }

  /* The word comes back beside the glyph. On focus as well as hover: a
     collapsed rail that can only be read with a mouse is a rail half the
     operators cannot read. */

  .is-rail-collapsed .ops-rail-link:hover .ops-rail-label,
  .is-rail-collapsed .ops-rail-link:focus-visible .ops-rail-label {
    position: absolute;
    inset-inline-start: calc(100% + 10px);
    top: 50%;
    z-index: 80;
    width: auto;
    height: auto;
    margin: 0;
    padding: 6px 10px;
    overflow: visible;
    clip-path: none;
    transform: translateY(-50%);
    border-radius: var(--r2);
    background: var(--ink);
    color: #fff;
    font-size: 12.5px;
    font-weight: 500;
    white-space: nowrap;
    box-shadow: 0 6px 18px rgb(15 23 42 / 0.22);
    pointer-events: none;
  }
}

.ops-rail-scrim {
  position: fixed;
  inset: 0;
  z-index: 90;
  background: rgb(15 23 42 / 0.45);
}

.app-shell {
  /* The bar the rail hangs off: --s3 twice, the 38px profile trigger with its
     1px border, and the bar's own bottom rule. An app that builds a taller bar
     sets --app-topbar for itself. */
  --app-topbar: var(--ops-topbar, 65px);
  display: grid;
  grid-template-columns: 248px minmax(0, 1fr);
  gap: var(--s5);
  align-items: start;
  /* Layout, so the slower of the three motion steps. */
  transition: grid-template-columns 260ms var(--ease);
}

.app-content {
  min-width: 0;
  padding-block: var(--s5);
}

@media (max-width: 1100px) {
  .app-shell {
    grid-template-columns: 184px minmax(0, 1fr);
    gap: var(--s4);
  }

  .ops-rail-link {
    padding-inline: 8px;
  }

  .ops-rail-index {
    display: none;
  }
}

@media (max-width: 900px) {
  .topbar-nav-toggle {
    display: inline-flex;
  }
  .user-identity {
    display: none;
  }
  .app-shell {
    grid-template-columns: minmax(0, 1fr);
  }
  .ops-rail {
    position: fixed;
    inset-block: 0;
    inset-inline-start: 0;
    z-index: 100;
    width: 264px;
    max-width: 82vw;
    height: auto;
    align-content: start;
    overflow-y: auto;
    margin-inline-start: 0;
    padding: var(--s4);
    transform: translateX(-100%);
    transition: transform var(--fast) var(--ease);
  }
  .ops-rail.is-open {
    transform: translateX(0);
  }
  .ops-rail-link {
    justify-content: flex-start;
    padding: 11px 10px;
  }
}

@media (min-width: 901px) {
  .ops-rail-scrim {
    display: none;
  }
}

/* A record's name is the way into it. Shared, because a buyer opening one of
   their own requests and an officer opening the same request are doing the
   same thing, and a row whose name is not the way in grows a button that
   duplicates it. */
.table-link {
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-weight: 600;
  color: var(--ink);
  text-align: start;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

.table-link:hover {
  color: var(--rev);
}

/* ============================================================
   REQUEST STATUS RAIL
   ------------------------------------------------------------
   The pipeline a request walks, drawn as the line it is: every
   stage in order, the ones behind the request filled, the one it
   is in ringed, the ones ahead left open. It stays a row at every
   width and scrolls inside its own box — eleven stages down the
   side of a record is a wall, and the shape of a pipeline is the
   one thing a column loses.

   In the token layer because two surfaces draw it: the operations
   record page and the buyer's own workspace. Its markup is shared
   the same way, by request-stages.js — a buyer and an officer
   reading the same request must not be reading two lines.

   Logical properties throughout, so the line runs the way the
   page reads. On the Arabic buyer dashboard it runs right to left
   without a rule of its own.
   ============================================================ */

.status-rail-scroll {
  overflow-x: auto;
  margin-block-start: var(--s5);
  padding-block-end: var(--s2);
}

.status-rail {
  display: grid;
  grid-auto-flow: column;
  /* The floor, not the width: the columns take an equal share of whatever room
     there is, and only fall back to this when a whole pipeline would not fit.
     Low enough that ten stages stand together on an ordinary screen — the stage
     name and its timestamp wrap onto a second line at that size, which costs a
     little height and saves the shape of the line, which is the whole point of
     drawing it. Past that floor the box scrolls rather than the stages
     shrinking any further. */
  grid-auto-columns: minmax(98px, 1fr);
  gap: var(--s3);
  margin: 0;
  padding: 0;
  list-style: none;
  min-inline-size: min-content;
}

.status-step {
  position: relative;
  min-width: 0;
  padding-block-start: 25px;
}

/* The segment leaving a stage, drawn as travelled only for the stages the
   request is already past — so the line itself says how far it has got, before
   any of the words under it are read. */
.status-step::before {
  content: "";
  position: absolute;
  inset-block-start: 6px;
  inset-inline: 18px calc(var(--s3) * -1);
  height: 1px;
  background: var(--line);
}

.status-step--walked::before {
  background: var(--ink-3);
}

.status-step:last-child::before {
  display: none;
}

.status-node {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  width: 13px;
  height: 13px;
  border: 1px solid var(--ink);
  border-radius: 50%;
  background: var(--ink);
}

/* Where the request is now. The ring is the same mark the buyer journey gives
   its terminus, because it means the same thing: this is where the line has
   got to. */
.status-step--now .status-node {
  outline: 1px solid var(--ink);
  outline-offset: 3px;
}

.status-step--now .status-step-name {
  font-weight: 700;
}

.status-step--ahead .status-node {
  background: var(--surface);
  border-color: var(--line-strong);
}

.status-step--ahead .status-step-name {
  color: var(--mute-3);
}

/* A stage the request passed without entering — a buyer's request never sees
   draft, and an offer can be recommended without a negotiation. Dashed, the
   treatment the evidence ledger already gives a gap, and uncoloured: passing a
   stage that was never required is not a fault. */
.status-step--skipped .status-node {
  background: var(--surface);
  border-style: dashed;
  border-color: var(--line-strong);
}

.status-step--skipped .status-step-name {
  color: var(--mute-2);
}

/* Cancelled or refunded. The request stopped, which is a verdict on it, so the
   node carries the colour this platform gives one — and nothing is drawn after
   it, because nothing after it is going to happen. */
.status-step--exit .status-node {
  background: var(--miss);
  border-color: var(--miss);
}

.status-step--exit .status-step-name {
  color: var(--miss);
  font-weight: 700;
}

.status-step-name {
  margin: 0;
  font-size: 12.5px;
  font-weight: 500;
  line-height: 1.35;
  color: var(--ink);
}

/* A UTC stamp is a left-to-right run whatever the page reads like. Left to the
   paragraph's own direction on the Arabic dashboard, "31 Jul" reorders to
   "Jul 31" and the day lands after the month; isolating it keeps the stamp in
   the order it was written, and the right-alignment keeps it under its own
   stage rather than adrift at the other edge of the column. */
.status-step-when {
  margin-block-start: var(--s1);
  font-family: var(--mono);
  font-size: 10px;
  line-height: 1.4;
  color: var(--mute-2);
  min-block-size: 1.4em;
  direction: ltr;
  unicode-bidi: isolate;
}

[dir="rtl"] .status-step-when {
  text-align: right;
}

/* ============================================================
   TOUCH
   ------------------------------------------------------------
   44px minimum on touch. The ledger is never itself a control —
   the row is, so nobody has to hit a 6px cell.
   ============================================================ */

@media (pointer: coarse) {
  .btn,
  .tabs a,
  .tabs button,
  .seg button,
  .pager button {
    min-height: 44px;
  }
}

/* ------------------------------------------------------------
   Modal layer and document viewer
   These are two shared components, not two pages' decoration. The
   admin console and the supplier workspace both float a dialog and
   both read a file in one, and a component defined once per surface
   is a component that drifts: the .modal card above asks a question,
   this is the layer it floats on.
   ------------------------------------------------------------ */

/* ------------------------------------------------------------
   Modal layer
   A dialog that asks for a decision floats over the page. In flow
   it pushed the dashboard down and had to be scrolled to, which
   left the page underneath looking operable while it was not.
   The scrim dims what is behind it and closes on a click, as
   Escape does.
   ------------------------------------------------------------ */

.modal-layer {
  position: fixed;
  inset: 0;
  z-index: 120;
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: var(--s5) var(--s4);
  overflow-y: auto;
  overscroll-behavior: contain;
}

.modal-scrim {
  position: fixed;
  inset: 0;
  background: rgb(15 23 42 / 0.45);
}

.modal-dialog {
  position: relative;
  width: min(640px, 100%);
  margin-block: auto;
  box-shadow: var(--e2);
  animation: modal-dialog-in var(--fast) var(--ease);
}

/* A form asks a question; a document has to be legible. The wide dialog is the
   same object at reading width. */
.modal-dialog--wide {
  width: min(1080px, 100%);
}

@keyframes modal-dialog-in {
  from {
    opacity: 0;
    transform: translateY(-8px);
  }

  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .modal-dialog {
    animation: none;
  }
}

/* The page behind a modal does not scroll: two scrollbars answering one
   wheel is the page moving under a dialog the operator is reading. */
body.is-modal-open {
  overflow: hidden;
}

/* ------------------------------------------------------------
   Document viewer
   A PDF or an image is evidence, so it is read over the page
   rather than in a strip of it: the viewer is a .modal-dialog on
   the shared .modal-layer, at reading width. The frame is sized
   to the viewport it floats in — never to the file — and the
   file keeps its own aspect inside it.
   ------------------------------------------------------------ */

.document-viewer {
  /* The bar and the frame reach the panel's own edge, so the dialog reads as a
     window onto the file rather than a card with a file printed inside it. */
  --document-viewer-height: clamp(320px, calc(100vh - 260px), 900px);
  padding: 0;
  overflow: hidden;
}

.document-viewer-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  border-block-end: 1px solid var(--line);
  background: var(--wash);
}

.document-viewer-bar h3 {
  margin: 0;
  overflow-wrap: anywhere;
}

.document-viewer-actions {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

.document-viewer-frame {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 320px;
  max-height: var(--document-viewer-height);
  overflow: auto;
  background: var(--wash);
}

.document-viewer-embed {
  width: 100%;
  height: var(--document-viewer-height);
  border: 0;
  background: var(--surface);
}

.document-viewer-image {
  display: block;
  max-width: 100%;
  height: auto;
  background: var(--surface);
}

/* ------------------------------------------------------------
   Product gallery
   A catalogue row is a thing you look at before you read it, so
   it is a card with a picture rather than a line of cells. The
   same component serves the supplier's own Products tab and the
   public catalogue -- two surfaces that share only this
   stylesheet, which is why it lives here and not beside either.
   ------------------------------------------------------------ */

/* .g2/.g3/.g4 are fixed counts, which is right for a landing page laid out by
   hand and wrong for a grid whose length nobody knows. This one reflows on the
   card's own width. */
.g-auto {
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}

.product-grid {
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
}

.product-card {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r4);
  box-shadow: var(--e1);
  transition: box-shadow var(--fast) var(--ease), transform var(--fast) var(--ease);
}

.product-card:hover {
  box-shadow: var(--e2);
  transform: translateY(-1px);
}

/* The box is the shape, never the file: the aspect ratio holds the row steady
   while the pictures are still arriving, so a gallery does not reflow under the
   reader as each one lands. */
.product-card-media {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  background: var(--wash);
}

/* No photo is a state, not a gap. The placeholder holds the same box so the
   grid stays even, and carries the product's own initial rather than a stock
   image of something it is not. */
.product-card-placeholder {
  display: grid;
  place-items: center;
  width: 100%;
  aspect-ratio: 4 / 3;
  background: var(--wash);
  border-block-end: 1px solid var(--line);
}

.product-card-body {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  padding: var(--s4);
}

.product-card-name {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
  overflow-wrap: anywhere;
}

.product-card-meta {
  margin: 0;
  font-size: 12.5px;
  color: var(--mute);
}

.product-card-price {
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}

/* The controls sit at the card's foot whatever the name above them ran to, so a
   row of cards has one line of buttons rather than a ragged one. */
.product-card-actions {
  margin-block-start: auto;
  padding: 0 var(--s4) var(--s4);
}

/* The candidates in the photo picker: the picture is the label, so each one is
   a button that is mostly image. */
.product-photo-grid {
  display: grid;
  gap: var(--s3);
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  padding: var(--s4);
}

.product-photo-choice {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  padding: 0;
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r3);
  cursor: pointer;
  text-align: start;
}

.product-photo-choice img {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  background: var(--wash);
}

.product-photo-choice span {
  padding: 0 var(--s3) var(--s3);
  font-size: 12px;
  color: var(--mute);
  overflow-wrap: anywhere;
}

.product-photo-choice:hover,
.product-photo-choice:focus-visible {
  border-color: var(--ink);
}

.product-photo-choice[aria-pressed="true"] {
  border-color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);
}

/* A photo that has been sent and not yet scanned. The choice is offered — the
   pointer is what is being attached, not the picture — so the box says which of
   the two it is instead of showing an image element with nothing behind it. */
.product-photo-choice .product-photo-waiting {
  display: grid;
  place-items: center;
  width: 100%;
  aspect-ratio: 1 / 1;
  padding: 0;
  background: var(--wash);
  font-size: 12px;
  font-weight: 600;
  color: var(--mute);
}

/* The commercial line: a price, and the least a buyer may order. Two facts
   rather than one sentence, so they stay legible when either is absent. */
.product-card-terms {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s3);
}

/* A listing the desk has taken off the catalogue, said on the supplier's own
   card. It wears the refusal colour the chip beside it wears, and it says the
   reason in words as well, because nothing on this platform depends on colour
   alone to carry a decision. */
.product-card-withheld {
  color: var(--miss);
}

/* Said under the placeholder tile rather than instead of it: the tile is what
   keeps the grid even, and this is why the tile is what is there. */
.product-card-note {
  margin-block-start: var(--s2);
  font-size: 11.5px;
  font-weight: 600;
  color: var(--mute);
  text-align: center;
}

/* The description on a card is a preview, not the whole of it: three lines,
   then it stops, because a card is a thing you look at before you read it. */
.product-card-description {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* The filter over the catalogue. .seg is the system's segmented control; what
   is added here is the count each choice carries, so how many there are to
   expect is read before the grid is. */
.product-filter {
  align-self: start;
  max-width: 100%;
  overflow-x: auto;
}

.product-filter button {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}

.product-filter button b {
  font-family: var(--mono);
  font-size: 11.5px;
  font-weight: 600;
  color: var(--mute);
}

.product-filter button[aria-pressed="true"] b {
  color: var(--ink);
}

/* Nothing to show is a state with two readings — a catalogue that is empty and
   a filter that matches nothing — and only the first of them has a next step,
   so the box holds one and may hold the other. */
.product-empty {
  display: grid;
  justify-items: start;
  gap: var(--s3);
  padding: var(--s5);
  border: 1px dashed var(--line-strong);
  border-radius: var(--r3);
  background: var(--wash);
}

.product-empty p {
  margin: 0;
  font-size: 13px;
  color: var(--mute);
}

/* ------------------------------------------------------------
   One product, at deciding size

   The card is a thing you look at before you read it. This is
   the other half: the row somebody clicked, with every
   photograph on it and the terms beside them. It serves the
   public product page and the supplier's own view of one of
   their products, which — like the card above — share only this
   stylesheet.
   ------------------------------------------------------------ */

.product-detail {
  display: grid;
  gap: var(--s5);
  grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  align-items: start;
}

/* One column before the gallery gets narrow enough that a
   thumbnail is smaller than a fingertip. */
@media (max-width: 860px) {
  .product-detail {
    grid-template-columns: minmax(0, 1fr);
  }
}

.product-gallery {
  display: grid;
  gap: var(--s3);
  min-width: 0;
}

/* CONTAIN, not cover. The card crops because a grid has to stay
   even and a cropped thumbnail still says which product it is.
   Here the picture is the thing being examined, and a detail
   view that cuts the edges off a component is a detail view
   that hides the part somebody opened it to see. */
.product-gallery-main {
  display: block;
  width: 100%;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--r3);
  background: var(--wash);
  cursor: zoom-in;
  overflow: hidden;
}

.product-gallery-main img {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: contain;
}

.product-gallery-thumbs {
  display: grid;
  gap: var(--s2);
  grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
}

/* aria-pressed marks the one on show, as it does on the photo
   picker: the same control, the same state, the same styling. */
.product-gallery-thumb {
  display: block;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--r2);
  background: var(--wash);
  cursor: pointer;
  overflow: hidden;
}

.product-gallery-thumb img {
  display: block;
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
}

.product-gallery-thumb[aria-pressed="true"] {
  border-color: var(--ink);
  box-shadow: 0 0 0 1px var(--ink);
}

.product-gallery-empty {
  display: grid;
  place-items: center;
  aspect-ratio: 4 / 3;
  margin: 0;
  padding: var(--s4);
  border: 1px dashed var(--line-strong);
  border-radius: var(--r3);
  background: var(--wash);
  font-size: 13px;
  color: var(--mute);
  text-align: center;
}

.product-detail-facts {
  display: grid;
  gap: var(--s3);
  min-width: 0;
}

.product-detail-name {
  margin: 0;
}

/* A definition list, because each of these is a labelled value
   and a run of them read as one sentence is a run nobody can
   scan. */
.product-detail-terms {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s4);
  margin: 0;
}

.product-detail-terms dt {
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--mute);
}

.product-detail-terms dd {
  margin: 0;
  font-size: 15px;
}

/* The ladder in the buy box.
   Two columns, quantity and unit price, sitting under the terms it qualifies.
   It is drawn only where there is more than one rung — a table with a single
   row would restate the minimum the card above already gives. */
.product-breaks {
  margin-block-start: var(--s4);
  padding-block-start: var(--s4);
  border-block-start: 1px solid var(--line);
}

.product-breaks-title {
  margin: 0 0 var(--s3);
  font-size: 11.5px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--mute);
}

.product-breaks-table {
  width: 100%;
  border-collapse: collapse;
}

.product-breaks-table th,
.product-breaks-table td {
  padding-block: 7px;
  border-block-end: 1px solid var(--nil-line);
  font-size: 14px;
  text-align: start;
  font-weight: 400;
}

.product-breaks-table thead th {
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--mute);
}

/* The price column is the reason the table exists, so it reads as a price:
   tabular figures that line up down the column, in the face the card uses. */
.product-breaks-table td.num,
.product-breaks-table th.num {
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  text-align: end;
}

.product-breaks-table tbody tr:last-child th,
.product-breaks-table tbody tr:last-child td {
  border-block-end: 0;
}

.product-breaks-note {
  margin: var(--s3) 0 0;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--mute);
}

.product-detail-price {
  font-family: var(--mono);
  font-weight: 600;
}

.product-detail-description {
  font-size: 14px;
  line-height: 1.65;
}

.product-detail-description p {
  margin: 0 0 var(--s2);
}

/* The way back, which is a link on the public page and a button in the
   workspace: one goes to another document and the other changes which panel is
   on, and that difference is not something a reader should be shown. So the
   rule carries the button reset the <a> has no need of. */
.product-detail-back {
  padding: 0;
  border: 0;
  background: none;
  font-size: 13px;
  text-decoration: underline;
  cursor: pointer;
}

/* ------------------------------------------------------------
   One product, as a page

   The workspace opens a product into a view of its own rather
   than a dialog, so the picture gets the width the scrim used
   to take. Only what changes with that width is here; the rest
   is the shared .product-detail above.
   ------------------------------------------------------------ */

/* The gallery takes the larger half. The picture is what the
   page was wanted for, and the terms are a short column that
   grows no more readable for being wider. */
.product-detail--page {
  gap: var(--s6);
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
}

@media (max-width: 860px) {
  .product-detail--page {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* A square that fills the width of a wide workspace is a
   photograph you cannot see the whole of without scrolling,
   and a fixed box letterboxes every landscape picture in grey.
   So the box takes the shape of what is in it and the picture
   is what is bounded: by the column on one side and by a
   screenful on the other, whichever runs out first. The
   min-height is for the two states that hold no image — a photo
   still being scanned, and one that was refused. */
.product-detail--page .product-gallery-main {
  aspect-ratio: auto;
  min-height: 240px;
}

.product-detail--page .product-gallery-main img {
  width: auto;
  max-width: 100%;
  max-height: min(62vh, 560px);
}

/* Everything that can be done to the product being read, in the
   .dashboard-head opposite its name — which is where this
   workspace already puts the controls for whatever a view is
   about. */
.product-page-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}

/* The card's picture, once clicking it goes somewhere. A link
   and a button look identical here on purpose: the public
   catalogue opens a page and the workspace opens a dialog, and
   which of the two it is is not something the grid should show. */
.product-card-media-link,
.product-card-media-button {
  display: block;
  width: 100%;
  padding: 0;
  border: 0;
  background: none;
  cursor: pointer;
}

.product-card-link {
  color: inherit;
  text-decoration: none;
}

.product-card-link:hover {
  text-decoration: underline;
}

/* A thumbnail the supplier can act on: the picture, and under it the
   two or three things that can be done to its place in the order.
   The controls sit under rather than over the image, because a
   control floating on a photograph is a control that lands on
   whatever the photograph happens to be showing. */
.product-photo-strip-item {
  display: grid;
  gap: 4px;
  align-content: start;
}

.product-photo-strip-controls {
  display: flex;
  flex-wrap: wrap;
  gap: 2px;
}

.product-photo-strip-controls .btn {
  padding: 2px 6px;
  font-size: 11px;
}

/* Which one a buyer sees. A badge rather than a button, because
   the first photograph is already the cover and there is nothing
   to press. */
.product-photo-strip-cover {
  align-self: center;
  padding: 2px 6px;
  border-radius: var(--r1);
  background: var(--wash);
  font-size: 11px;
  font-weight: 600;
  color: var(--mute);
}

/* In the workspace the large picture holds a scan state as well as
   an image, so it is a box rather than an image element. */
.product-gallery-main:not(button) {
  display: grid;
  place-items: center;
  aspect-ratio: 1 / 1;
  cursor: default;
}

.product-gallery-main:not(button) img {
  aspect-ratio: auto;
  max-height: 100%;
}

/* ------------------------------------------------------------
   The products page

   A catalogue nobody can search is a catalogue nobody reads past
   the second row. This is the shop furniture around the grid:
   the search field, the facet rail, the toolbar that says how
   many matched and in what order, the chips that say what is
   currently being excluded, and the pager.

   Written in logical properties throughout, because the Arabic
   copy of this page mirrors and a rail pinned to `left` would
   end up on the wrong side of an RTL reader's screen.
   ------------------------------------------------------------ */

.shop-crumb {
  margin-block-end: var(--s4);
}

.shop-crumb-tail,
.shop-crumb-cat {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}

/* The search field and its button are one object, so the button
   does not drift onto its own line at a width where the field
   still has room. */
.shop-search {
  display: flex;
  gap: var(--s2);
  align-items: stretch;
  max-width: 640px;
}

.shop-search .input {
  flex: 1 1 auto;
  min-width: 0;
}

.shop-note {
  margin-block: var(--s3) 0;
  font-size: 12.5px;
  color: var(--mute);
  max-width: 68ch;
}

/* The rail is a rail on a desktop and a panel a reader opens on a
   phone, where five filter groups above the first product is five
   screenfuls of nothing they came for. */
.shop-facets-toggle {
  display: none;
  margin-block-start: var(--s4);
}

.shop-layout {
  display: grid;
  gap: var(--s5);
  grid-template-columns: minmax(0, 240px) minmax(0, 1fr);
  align-items: start;
  margin-block-start: var(--s5);
}

.shop-facets {
  display: grid;
  gap: var(--s5);
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r3);
  background: var(--surface);
  position: sticky;
  inset-block-start: var(--s4);
}

/* Below the rail's breakpoint the facets are what the toggle
   opens, so the rules that hide them have to come after the ones
   that lay them out — same specificity, later wins. */
@media (max-width: 900px) {
  .shop-facets-toggle {
    display: inline-flex;
  }

  .shop-layout {
    grid-template-columns: minmax(0, 1fr);
  }

  .shop-facets {
    display: none;
    position: static;
  }

  .shop-facets.is-open {
    display: grid;
  }
}

.shop-facet-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
}

.shop-facet-head h2 {
  margin: 0;
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--ink);
}

.shop-facet-clear {
  border: 0;
  padding: 0;
  background: none;
  font-family: inherit;
  font-size: 12px;
  color: var(--mute);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

.shop-facet-clear:hover {
  color: var(--ink);
}

.shop-facet h3 {
  margin: 0 0 var(--s2);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--mute);
}

.shop-facet-list {
  display: grid;
  gap: 6px;
  max-height: 260px;
  overflow-y: auto;
}

.shop-facet-hint,
.shop-facet-none {
  margin: 0 0 var(--s2);
  font-size: 11.5px;
  color: var(--mute-2);
}

/* The count is the whole reason a facet is worth reading before
   it is clicked: it says what is behind the box without opening
   it. A zero stays on screen and goes quiet rather than
   disappearing, so the list does not reshuffle under a reader
   mid-click. */
.shop-facet-option {
  justify-content: flex-start;
  gap: var(--s2);
}

.shop-facet-count {
  margin-inline-start: auto;
  font-family: var(--mono);
  font-size: 11px;
  font-style: normal;
  color: var(--mute-2);
}

.shop-facet-option.is-empty {
  opacity: 0.45;
}

.shop-facet-range {
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex-wrap: wrap;
}

.shop-facet-range .input {
  width: 78px;
  padding-inline: var(--s2);
}

.shop-facet-dash {
  color: var(--mute-2);
}

.shop-results {
  display: grid;
  gap: var(--s4);
  min-width: 0;
}

.shop-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  padding-block-end: var(--s3);
  border-block-end: 1px solid var(--line);
}

.shop-count {
  margin: 0;
  font-size: 12.5px;
  color: var(--mute);
}

.shop-count b {
  font-family: var(--mono);
  color: var(--ink);
}

.shop-count-sep {
  color: var(--mute-3);
}

.shop-bar-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s3);
}

.shop-sort {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  font-size: 12.5px;
  color: var(--mute);
}

.shop-sort .select {
  width: auto;
  padding-block: 6px;
  font-size: 12.5px;
}

.shop-applied {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}

/* One chip per exclusion currently in force, and clicking it
   lifts that one. A reader who cannot see what a page is hiding
   from them reads an empty grid as an empty catalogue. */
.shop-applied-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: var(--surface);
  font-family: inherit;
  font-size: 12px;
  color: var(--ink);
  cursor: pointer;
}

.shop-applied-chip:hover {
  border-color: var(--ink);
}

.shop-applied-x {
  font-size: 13px;
  color: var(--mute);
}

.shop-label {
  display: none;
}

/* The same cards, one per row, for a reader comparing description
   against description rather than picture against picture. */
.product-grid--list {
  grid-template-columns: minmax(0, 1fr);
}

.product-grid--list .product-card {
  display: grid;
  grid-template-columns: minmax(0, 200px) minmax(0, 1fr);
  align-items: start;
}

/* The picture takes the whole height of the row rather than its
   own aspect ratio, so a card with three lines of description
   does not sit beside a band of empty card. */
/* `span 2`, not `1 / -1`: the rows here are implicit, so -1 names
   the end of an explicit grid that has one line in it and the
   picture would span nothing. */
.product-grid--list .product-card-media-link {
  grid-row: 1 / span 2;
  height: 100%;
}

.product-grid--list .product-card-media,
.product-grid--list .product-card-placeholder {
  height: 100%;
  min-height: 150px;
  aspect-ratio: auto;
}

.product-grid--list .product-card-actions {
  grid-column: 2;
  margin-block-start: 0;
  padding-block-start: 0;
}

@media (max-width: 620px) {
  .product-grid--list .product-card {
    grid-template-columns: minmax(0, 1fr);
  }

  .product-grid--list .product-card-actions {
    grid-column: 1;
  }
}

.shop-card-moq b {
  font-family: var(--mono);
  color: var(--ink-3);
}

.product-card-description {
  margin: 0;
}

.shop-pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--s3);
  padding-block-start: var(--s4);
  border-block-start: 1px solid var(--line);
}

.shop-pager .btn[disabled] {
  opacity: 0.4;
  pointer-events: none;
}

/* ------------------------------------------------------------
   One product, as a shop page

   Three columns: the photographs, what the supplier said, and
   the one action a reader can take. The third is a box rather
   than a button in a paragraph because it holds a quantity and
   an arithmetic total as well, and those belong beside the
   thing they are about.
   ------------------------------------------------------------ */

.product-detail--shop {
  grid-template-columns: minmax(0, 5fr) minmax(0, 6fr) minmax(0, 4fr);
}

@media (max-width: 1080px) {
  .product-detail--shop {
    grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
  }

  .product-detail--shop .product-buybox {
    grid-column: 1 / -1;
  }
}

@media (max-width: 860px) {
  .product-detail--shop {
    grid-template-columns: minmax(0, 1fr);
  }
}

.product-detail-heading {
  margin: var(--s5) 0 var(--s2);
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
}

.product-detail-empty,
.product-gallery-hint {
  margin: 0;
  font-size: 12px;
  color: var(--mute-2);
}

/* .tbl is a data table: it holds a 640px floor so a dozen columns
   do not crush, and puts numbers hard against the end edge. Six
   rows of one value each is the other shape entirely — the floor
   would push every figure past the edge of a column this narrow,
   into a scroll region nobody thinks to drag. */
.product-spec {
  min-width: 0;
}

.product-spec th {
  width: 44%;
  font-weight: 500;
  color: var(--mute);
}

.product-spec .num {
  text-align: start;
}

.product-buybox {
  display: grid;
  gap: var(--s3);
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r3);
  background: var(--surface);
  box-shadow: var(--e1);
  position: sticky;
  inset-block-start: var(--s4);
}

@media (max-width: 1080px) {
  .product-buybox {
    position: static;
  }
}

.product-buybox p {
  margin: 0;
}

.product-buybox-price .product-detail-price {
  font-size: 22px;
}

.product-buybox-per {
  font-size: 12.5px;
  color: var(--mute);
}

.product-buybox-note {
  font-size: 11.5px;
  color: var(--mute-2);
}

/* A stated minimum the reader has gone under is a gap, not an
   error: the request can still be sent, and the sourcing team
   will come back on whether the supplier will take it. */
.product-buybox-warn {
  padding: 6px 10px;
  border: 1.5px dashed var(--gap-line);
  border-radius: var(--r2);
  background: var(--gap-bg);
  font-size: 12px;
  color: var(--gap);
}

.product-buybox-total {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  padding-block-start: var(--s3);
  border-block-start: 1px solid var(--line);
  font-size: 12.5px;
  color: var(--mute);
}

.product-buybox-total b {
  font-family: var(--mono);
  font-size: 15px;
  color: var(--ink);
}

.product-buybox-list {
  margin: 0;
  padding-inline-start: 18px;
  display: grid;
  gap: 6px;
  font-size: 11.5px;
  color: var(--mute-2);
}

.product-qty {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  font-size: 12.5px;
  color: var(--mute);
}

.product-qty-control {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--line-strong);
  border-radius: var(--r2);
  overflow: hidden;
}

.product-qty-control .input {
  width: 76px;
  border: 0;
  border-radius: 0;
  text-align: center;
  font-family: var(--mono);
}

.product-qty-step {
  width: 32px;
  border: 0;
  background: var(--wash);
  font-size: 15px;
  line-height: 1;
  color: var(--ink);
  cursor: pointer;
}

.product-qty-step:hover {
  background: var(--line);
}

.product-more {
  margin-block-start: var(--s5);
}

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

   Nine fields about one thing, asked in three groups, because
   nine in an undifferentiated run is a wall rather than a form.
   The group is a <fieldset> so its legend is announced, and the
   pairs inside it collapse to one column before they get narrow
   enough to be unreadable.
   ------------------------------------------------------------ */

.product-fieldset {
  min-width: 0;
  margin: 0;
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r3);
  background: var(--surface);
}

.product-fieldset legend {
  padding-inline: 6px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--mute);
}

.product-fieldset-grid {
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}

.product-fieldset-grid--three {
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
}

/* A price break: the quantity it starts at, the price from there, and the way
   to take it off. A row rather than a grid cell each, because the three belong
   to one rung and a grid would let them reflow apart from each other on a
   narrow screen — a quantity above one price and beside another. */
.product-tier-rows {
  display: grid;
  gap: var(--s3);
  margin-block-start: var(--s3);
  margin-block-end: var(--s3);
}

.product-tier-row {
  display: grid;
  gap: var(--s3);
  align-items: end;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) auto;
}

/* Under about 460px the remove control has nowhere to sit beside two fields,
   so the rung stacks and the control runs the width of it — which is also the
   size a thumb needs. */
@media (max-width: 460px) {
  .product-tier-row {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* The picture beside the control that chooses it, so what is about to be sent
   is looked at rather than read as a filename. */
.product-photo-field {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--s4);
}

.product-photo-field-body {
  display: grid;
  justify-items: start;
  gap: var(--s2);
  flex: 1 1 260px;
  min-width: 0;
}

.product-photo-preview {
  display: grid;
  place-items: center;
  width: 132px;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--r2);
  background: var(--wash);
}

.product-photo-preview img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.product-photo-preview-empty {
  padding: var(--s2);
  font-size: 11.5px;
  font-weight: 600;
  color: var(--mute);
  text-align: center;
}

.product-form-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
}

/* ------------------------------------------------------------
   Written description

   A description is prose with structure — a line of emphasis, a
   list of specifications, a link to a datasheet — and the field
   that held it was a textarea, so all three arrived as one run of
   text. .richtext is the control that writes it and .richtext-body
   is the same prose rendered back; they share their type scale so
   what a supplier typed is what the card shows.

   Nothing here styles a tag the sanitiser does not keep. The
   allowlist lives in product-description-service.php, and a rule
   for <table> would be a rule for markup that never arrives.
   ------------------------------------------------------------ */

.richtext {
  display: grid;
  gap: 6px;
}

.richtext-label {
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ink-2);
}

.richtext-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  padding: 5px;
  border: 1px solid var(--line-strong);
  border-end-start-radius: 0;
  border-end-end-radius: 0;
  border-start-start-radius: var(--r2);
  border-start-end-radius: var(--r2);
  border-block-end: 0;
  background: var(--wash);
}

.richtext-btn {
  min-width: 32px;
  height: 28px;
  padding-inline: 9px;
  border: 1px solid transparent;
  border-radius: var(--r1);
  background: transparent;
  font-family: var(--sans);
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ink-2);
  cursor: pointer;
  transition: background var(--fast) var(--ease), border-color var(--fast) var(--ease);
}

.richtext-btn:hover {
  background: var(--surface);
  border-color: var(--line);
}

/* Pressed is a state of the text under the cursor, not of the button, so it is
   drawn as the surface being lit rather than as a verdict colour. */
.richtext-btn[aria-pressed="true"] {
  background: var(--surface);
  border-color: var(--line-strong);
  color: var(--ink);
}

.richtext-area {
  min-height: 140px;
  max-height: 420px;
  overflow-y: auto;
  padding: 11px 13px;
  border: 1px solid var(--line-strong);
  border-start-start-radius: 0;
  border-start-end-radius: 0;
  border-end-start-radius: var(--r2);
  border-end-end-radius: var(--r2);
  background: var(--surface);
  font-family: var(--sans);
  font-size: 14px;
  line-height: 1.55;
  color: var(--ink);
}

.richtext-area:focus {
  outline: none;
  border-color: var(--ink);
  box-shadow: 0 0 0 3px rgba(15, 23, 42, 0.09);
}

.richtext-area > :first-child,
.richtext-body > :first-child {
  margin-block-start: 0;
}

.richtext-area > :last-child,
.richtext-body > :last-child {
  margin-block-end: 0;
}

.richtext-body {
  font-size: 12.5px;
  line-height: 1.55;
  color: var(--mute);
  overflow-wrap: anywhere;
}

.richtext-area p,
.richtext-body p,
.richtext-area ul,
.richtext-body ul,
.richtext-area ol,
.richtext-body ol,
.richtext-area blockquote,
.richtext-body blockquote {
  margin-block: 0 var(--s2);
}

.richtext-area ul,
.richtext-body ul,
.richtext-area ol,
.richtext-body ol {
  padding-inline-start: 1.25em;
}

.richtext-area h3,
.richtext-body h3,
.richtext-area h4,
.richtext-body h4 {
  margin-block: var(--s2) 6px;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
}

.richtext-area blockquote,
.richtext-body blockquote {
  padding-inline-start: var(--s3);
  border-inline-start: 2px solid var(--line-strong);
  color: var(--mute);
}

/* The global reduced-motion rule zeroes transition duration, not transform, so
   a lift still lands -- instantly. The public catalogue never loads
   buyer-dashboard.css, where the workspace keeps its equivalent block, so this
   one has to be here or the card is unprotected on exactly one of its two
   surfaces. */
@media (prefers-reduced-motion: reduce) {
  .product-card:hover {
    transform: none;
  }
}

/* ============================================================
   THE ORDERING JOURNEY — cart, checkout, order
   ------------------------------------------------------------
   Three public pages: what is in the basket, the four steps that
   turn it into an order, and where that order has got to. They
   are here rather than in styles.css because they extend the
   shop vocabulary above — .product-qty-control, .product-card,
   .shop-crumb — and a rule that reached across two stylesheets
   for one component is a rule nobody finds twice.

   Nothing below introduces a colour. The four verdict colours
   carry judgements; a basket, a step and a shipment are none.
   The one exception is .cart-qty-warn, which is a refusal: that
   quantity will not be accepted, and the platform says so in the
   colour it says every other "this will not do" in.

   Logical properties throughout, so all three pages mirror on
   the Arabic tree without a rule of their own.
   ============================================================ */

/* ---------------------------------------------------------- cart */

.cart-layout {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 22rem);
  gap: var(--s5);
  align-items: start;
  margin-block-start: var(--s5);
}

@media (max-width: 900px) {
  .cart-layout {
    grid-template-columns: minmax(0, 1fr);
  }
}

/*
 * A row header, not a column header.
 *
 * The product cell is <th scope="row"> because the row is about that product,
 * and that is the right markup. What it inherited from it was `.tbl th`, which
 * is the styling of a COLUMN heading: 11px, tracked out 0.13em, uppercase,
 * muted, nowrap, on the header wash. So a supplier's own product name came out
 * as "STAINLESS STEEL GASKET, 40 MM" in a shaded band that could never wrap,
 * and a placeholder listing called Name in Category A read on screen as NAME /
 * CATEGORY A -- indistinguishable from the template text of an unfinished page.
 *
 * A product name is the supplier's words. It is set as body text, and the six
 * declarations below are all named rather than left to inheritance, because
 * each of them is one `.tbl th` is imposing and every one of them was wrong
 * here. The selector carries the table too: `.tbl th` is (0,1,1) and a lone
 * class is (0,1,0), which is how this cell kept the column-heading styling
 * through every rule that looked like it was overriding it.
 */
.cart-table th.cart-cell-product {
  display: flex;
  gap: var(--s3);
  align-items: flex-start;
  padding: 14px var(--s4);
  background: transparent;
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: normal;
  text-transform: none;
  white-space: normal;
  color: var(--ink);
}

.cart-thumb {
  display: block;
  flex: none;
  inline-size: 56px;
  block-size: 56px;
  border: 1px solid var(--line);
  border-radius: var(--r2);
  overflow: hidden;
  background: var(--wash);
}

.cart-thumb img {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  display: block;
}

/* Stands in the picture's place until one loads, so a row whose photograph
   was withdrawn keeps its shape instead of collapsing. */
.cart-thumb-empty {
  display: block;
  inline-size: 100%;
  block-size: 100%;
}

.cart-cell-names {
  display: grid;
  gap: 2px;
  min-inline-size: 0;
}

.cart-name {
  font-size: 13.5px;
  font-weight: 600;
  color: var(--ink);
}

.cart-cell-category,
.cart-cell-moq {
  font-size: 12px;
  font-weight: 400;
  color: var(--mute-2);
}

.cart-qty {
  inline-size: 8.5rem;
}

/* A refusal, in the colour every refusal on this platform wears — and, because
   nothing here depends on colour alone, said in words as well. */
.cart-qty-warn {
  display: block;
  margin-block-start: 6px;
  font-size: 11.5px;
  line-height: 1.4;
  color: var(--miss);
}

.cart-cell-remove {
  text-align: end;
}

.cart-mixed {
  margin-block-start: var(--s4);
  padding: var(--s3) var(--s4);
  border: 1px solid var(--gap-line);
  border-radius: var(--r3);
  background: var(--gap-bg);
  font-size: 13px;
  line-height: 1.6;
  color: var(--ink-3);
}

.cart-summary {
  display: grid;
  gap: var(--s3);
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r3);
  background: var(--surface);
  box-shadow: var(--e1);
  position: sticky;
  inset-block-start: var(--s4);
}

@media (max-width: 900px) {
  .cart-summary {
    position: static;
  }
}

.cart-summary h2 {
  margin: 0;
  font-size: 15px;
  font-weight: 600;
}

.cart-summary p {
  margin: 0;
}

.cart-summary-lines {
  display: grid;
  gap: var(--s2);
  margin: 0;
  padding-block: var(--s3);
  border-block: 1px solid var(--line);
}

.cart-summary-lines > div {
  display: flex;
  justify-content: space-between;
  gap: var(--s3);
  font-size: 13px;
}

.cart-summary-lines dt {
  color: var(--mute);
}

.cart-summary-lines dd {
  margin: 0;
  font-weight: 600;
  color: var(--ink);
}

.cart-summary-note {
  font-size: 12px;
  line-height: 1.55;
  color: var(--mute-2);
}

/* A cart holding two currencies cannot become one order, so the control that
   would try is a marker rather than a button until a line is removed. */
.btn.is-disabled,
.btn[aria-disabled="true"] {
  opacity: 0.45;
  cursor: default;
  pointer-events: auto;
}

/* -------------------------------------------------------- checkout */

.checkout-panel {
  display: grid;
  gap: var(--s4);
}

.checkout-panel h2 {
  margin: 0;
  font-size: 17px;
  font-weight: 600;
  letter-spacing: -0.01em;
}

.checkout-panel > p,
.checkout-panel fieldset {
  margin: 0;
}

.checkout-panel fieldset {
  border: 0;
  padding: 0;
  display: grid;
  gap: var(--s3);
}

.checkout-addresses {
  display: grid;
  gap: var(--s2);
}

.checkout-address small {
  line-height: 1.5;
}

.checkout-review {
  display: grid;
  gap: var(--s2);
  margin: 0;
  padding-block: var(--s3);
  border-block: 1px solid var(--line);
}

.checkout-review > div {
  display: grid;
  grid-template-columns: minmax(0, 12rem) minmax(0, 1fr);
  gap: var(--s3);
  font-size: 13px;
}

@media (max-width: 700px) {
  .checkout-review > div {
    grid-template-columns: minmax(0, 1fr);
    gap: 2px;
  }
}

.checkout-review dt {
  color: var(--mute);
}

.checkout-review dd {
  margin: 0;
  color: var(--ink-3);
}

.checkout-signedout h2,
.checkout-done h2 {
  margin-block: 0 var(--s2);
  font-size: 17px;
  font-weight: 600;
}

.checkout-done-ref {
  font-size: 14px;
}

.checkout-done-ref b {
  font-size: 15px;
}

/* ----------------------------------------------------------- order */

.order-detail {
  display: grid;
  gap: var(--s6);
  margin-block-start: var(--s5);
}

.order-head {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: var(--s4);
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r3);
  background: var(--surface);
  box-shadow: var(--e1);
}

.order-head p {
  margin: 0;
}

.order-head-label {
  font-size: 11.5px;
  font-weight: 500;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--mute-2);
}

.order-head-ref,
.order-head-when,
.order-head-status {
  margin-block-start: 4px !important;
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
}

.order-section {
  display: grid;
  gap: var(--s3);
}

.order-section h2 {
  margin: 0;
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
}

.order-section p {
  margin: 0;
}

.order-facts {
  display: grid;
  gap: var(--s2);
  margin: 0;
}

.order-facts > div {
  display: grid;
  grid-template-columns: minmax(0, 12rem) minmax(0, 1fr);
  gap: var(--s3);
  padding-block-end: var(--s2);
  border-bottom: 1px solid var(--line);
  font-size: 13px;
}

@media (max-width: 700px) {
  .order-facts > div {
    grid-template-columns: minmax(0, 1fr);
    gap: 2px;
  }
}

.order-facts dt {
  color: var(--mute);
}

.order-facts dd {
  margin: 0;
  color: var(--ink-3);
  overflow-wrap: anywhere;
}

.order-totals dd {
  font-weight: 600;
  color: var(--ink);
}

.order-totals-sum {
  border-bottom: 0 !important;
}

.order-totals-sum dt,
.order-totals-sum dd {
  font-size: 15px;
  font-weight: 700;
  color: var(--ink);
}

.order-pay {
  padding: var(--s4);
  border: 1px solid var(--line-strong);
  border-radius: var(--r3);
  background: var(--wash);
}

/* The configured bank details arrive as one block of text a human typed, and
   an IBAN that has been re-wrapped is an IBAN somebody mistypes. */
.order-bank {
  margin: 0;
  padding: var(--s3);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  background: var(--surface);
  font-family: var(--mono);
  font-size: 12.5px;
  line-height: 1.7;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.order-more {
  margin-block-start: var(--s6);
}

/* ------------------------------------------ add to cart, on a card */

.product-card-added,
.product-added {
  margin: 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--mute);
}

.product-card-added {
  padding-inline: var(--s4);
  padding-block-end: var(--s4);
}

.product-added-link {
  display: inline-block;
  margin-inline-start: 4px;
  font-weight: 600;
  color: var(--ink);
}

/* ------------------------------------------- pinning a delivery point

   The address above it is what goes on the shipping documents; this is
   the point a driver navigates to. So it reads as an addition to the
   form rather than as a second address: hairline box, no fill, and no
   colour except the one refusal in it.

   Nothing here is drawn by JavaScript. locate.js paints the tiles onto
   a canvas and the rest is these rules, which is the same division
   journey-steps.js keeps for its progress meter.
   ------------------------------------------------------------ */

.locate {
  display: grid;
  gap: var(--s2);
  margin-block-start: var(--s4);
  padding: var(--s4);
  border: 1px solid var(--line);
  border-radius: var(--r3);
}

.locate p {
  margin: 0;
}

.locate-actions {
  margin-block-start: var(--s2) !important;
}

.locate-readout {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s2);
  font-size: 12.5px;
  color: var(--mute);
}

.locate-readout b {
  font-size: 13px;
  color: var(--ink);
}

/* A fix this vague is a guess made from wifi, and a buyer who is not told so
   will trust a pin that is a suburb wide. Encoded in words as well as colour,
   like every other verdict on this platform. */
.locate-warn {
  padding: var(--s2) var(--s3);
  border: 1px solid var(--gap-line);
  border-radius: var(--r2);
  background: var(--gap-bg);
  font-size: 12px;
  line-height: 1.55;
  color: var(--ink-3);
}

.locate-map {
  display: grid;
  gap: var(--s2);
}

.locate-map-viewport {
  position: relative;
  block-size: 260px;
  border: 1px solid var(--line-strong);
  border-radius: var(--r2);
  overflow: hidden;
  background: var(--wash);
  cursor: grab;
  /* The map is dragged, so the browser must not claim the gesture for a scroll
     — and a two-finger page scroll still works because only panning is taken. */
  touch-action: none;
}

.locate-map-viewport.is-dragging {
  cursor: grabbing;
}

.locate-map-viewport:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

.locate-map-canvas {
  display: block;
  inline-size: 100%;
  block-size: 100%;
}

/* The pin is the centre of the viewport and does not move; the map moves under
   it. Pointer events pass through it so it cannot swallow a drag that begins
   on it. */
.locate-pin {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  inline-size: 18px;
  block-size: 18px;
  margin-block-start: -9px;
  margin-inline-start: -9px;
  border: 3px solid var(--ink);
  border-radius: 50%;
  background: var(--surface);
  box-shadow: 0 0 0 2px var(--surface);
  pointer-events: none;
}

.locate-map-bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
}

/* A term of the tile server's use, not decoration. It stays legible. */
.locate-credit {
  font-size: 11px;
  color: var(--mute-2);
}

.locate-credit a {
  color: var(--mute);
}
