/* ============================================================================
   components.css — the shared primitives. design.md specified this file in
   July, alongside tokens.css; tokens.css shipped and this one did not, which
   left colour fixed everywhere and everything colour sits inside still
   hand-written per page.

   Measured before this file (2026-08-17): 2,392 lines of per-page <style>
   across 26 pages, 848 inline `style=` attributes, 27 distinct `.btn-*`
   classes, and 76 separately styled <table> elements. The component library
   already existed and was well built — it was trapped in index.css, which
   exactly one page loads.

   LOAD ORDER
     tokens.css        every colour, font, space and radius
     site-nav.css      the rail
     components.css    THIS FILE — page content primitives
     <style>           whatever is genuinely unique to that page
     site-header.css   the header, last, so shared chrome always wins

   Page styles load after this file on purpose: a page can still override a
   primitive when it truly needs to. What a page may NOT do is declare a bare
   element selector — `button {}`, `table {}`, `input {}` — because those reach
   past <main> into the rail and the header. Ten pages did, which is why the
   sidebar's Collapse control read COLLAPSE on user access and Collapse
   everywhere else. Scope page rules to a class or to `main`.

   Tokens only. No hex literals here, and none in a page.
   ============================================================================ */

/* ---------------------------------------------------------------------------
   1. Base
   --------------------------------------------------------------------------- */

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui);
}

/* ONE content width. There were five in use — 1040, 1100, 1180, 1200 and
   unbounded — and no page had a reason for its own. 1200 because the densest
   surfaces (Products, Performance, Customers) are wide tables that need it,
   and because prose caps its own measure below (`.sub`, `.intro`) rather than
   riding the container. */
main {
  max-width: 1200px;
  margin: 0 auto;
  padding: var(--space-6);
}

/* ---------------------------------------------------------------------------
   2. Type
   --------------------------------------------------------------------------- */

h1, h2, h3 { font-family: var(--font-display); font-weight: 400; }
h2 { font-size: 18px; letter-spacing: 0.02em; margin: 0 0 var(--space-3); }
h3 { font-size: 15px; letter-spacing: 0.02em; margin: 0 0 var(--space-2); }

/* A page's standfirst. Capped at a readable measure rather than running the
   full 1200 — a 1200px line of 14px text is about 170 characters. */
.sub, .intro {
  color: var(--muted);
  font-size: 13px;
  line-height: 1.55;
  margin: 0 0 var(--space-5);
  max-width: 80ch;
}

/* Explanation, one click away.
   -----------------------------------------------------------------------
   Marketing opened with a paragraph about the page, then a row of tabs, then
   a second paragraph about the tab, then a row of module buttons, then a third
   paragraph about the module: roughly fifteen lines of prose and three rows of
   controls before any data. Customers did a shorter version, including how to
   operate the table ("expand a tea for its SKUs", "click the name for repeat
   behaviour").

   None of it is wrong and none of it is deleted. It is reference, and reference
   belongs where you go to read it — the knowledge bundle under /knowledge and
   /guide.html both exist for this — so on the working surface it collapses to
   a line you can open. One sentence of orientation stays in the reading path.

   Operating instructions in particular are a sign that an interaction needs to
   be more obvious, not that it needs a caption. */
details.explainer {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-row);
  margin: 0 0 var(--space-5);
  font-size: 13px;
  line-height: 1.55;
  color: var(--muted);
}
details.explainer > summary {
  cursor: pointer;
  padding: 8px 12px;
  color: var(--gold-dim);
  font-size: 12px;
  letter-spacing: 0.03em;
  list-style: none;
}
details.explainer > summary::-webkit-details-marker { display: none; }
details.explainer > summary::before { content: "▸ "; }
details.explainer[open] > summary::before { content: "▾ "; }
details.explainer > summary:hover { color: var(--gold); }
details.explainer > summary:focus-visible { outline: 2px solid var(--border-active); outline-offset: -2px; }
details.explainer > :not(summary) {
  padding: 0 12px 12px;
  margin: 0;
  max-width: 80ch;
}

.muted   { color: var(--muted); }
.empty   { color: var(--muted); padding: var(--space-5) var(--space-3); font-style: italic; }

/* Visible to a screen reader, to nothing else.
   -----------------------------------------------------------------------
   The standard clip-rect utility, here rather than per page because the six
   controls that needed it are on two different screens and the next one will
   be on a third. `clip-path` alone is not enough on older WebKit; both are
   kept, which is why every published version of this snippet has both.

   Use it for a <label> whose control is already obvious to a sighted user —
   the /products filter row, where five selects sat with no accessible name at
   all and a screen reader announced five identical unnamed combo boxes. Do
   NOT use it to hide something a sighted user also needs. */
.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------------------
   Waiting, without moving the page
   ---------------------------------------------------------------------------
   19 of 27 pages rendered a bare centred "Loading…" into a container with no
   height, then swapped in a table several thousand pixels tall. Everything
   below moved, which is the whole of what a layout-shift score measures — and
   what jambinet-dashboard's own pass found: the problem was never paint, it
   was "the page rearranging itself under the reader".

   You cannot reserve three thousand pixels. You do not have to. A shift only
   counts if it is VISIBLE, so reserving one screenful is enough: content that
   arrives below the fold pushes nothing anybody can see. 60vh, not 100vh, so a
   page whose real content is short does not open with a screen of nothing. */
.loading {
  color: var(--muted);
  text-align: center;
  padding: 50px;
  min-height: 60vh;
}

/* A reserved-and-blank box reads as a card that failed rather than one that is
   working, so the reservation can be filled with bars at the pitch of a real
   table row. Widths are fixed per position, not random: a placeholder that
   reflows while it waits is a smaller copy of the problem it exists to solve.

   Opacity, not a translating shimmer — it composites on the GPU, and this runs
   on screens that stay open all day. The reduced-motion block at the bottom of
   this file flattens it.

   Retuned 2026-08-18. The bars did already pulse — the audit called them static,
   which was wrong — but 0.05→0.11 over 1.4s with every row in lockstep is under
   the threshold where anyone reads it as motion, so the effect was the same as
   having none: a reserved box that looks like a card that failed. Three changes,
   all cheap: 1.6s instead of 1.4 (slower reads as breathing, faster reads as
   loading-bar), 0.05→0.14 so the swing is actually visible on a 14px bar, and a
   six-step delay cascade so it travels DOWN the page instead of blinking as one
   block. The products payload takes about three seconds; this is what fills it. */
.skeleton { min-height: 60vh; padding: var(--space-3) 0; }
.skeleton-row {
  height: 14px;
  margin: 0 0 26px;
  border-radius: 3px;
  background: var(--text);
  opacity: 0.07;
  animation: skeleton-pulse 1.6s ease-in-out infinite;
}
.skeleton-row:nth-child(3n)   { width: 82%; }
.skeleton-row:nth-child(3n+1) { width: 96%; }
.skeleton-row:nth-child(3n+2) { width: 68%; }
/* The wave. Period of six, so it works for a nine-row skeleton and for any
   other count a page reserves without anyone having to count rows. */
.skeleton-row:nth-child(6n+1) { animation-delay: 0s; }
.skeleton-row:nth-child(6n+2) { animation-delay: 0.11s; }
.skeleton-row:nth-child(6n+3) { animation-delay: 0.22s; }
.skeleton-row:nth-child(6n+4) { animation-delay: 0.33s; }
.skeleton-row:nth-child(6n+5) { animation-delay: 0.44s; }
.skeleton-row:nth-child(6n)   { animation-delay: 0.55s; }
@keyframes skeleton-pulse {
  0%, 100% { opacity: 0.05; }
  50%      { opacity: 0.14; }
}

/* ---------------------------------------------------------------------------
   3. Buttons
   --------------------------------------------------------------------------- */

/* One base plus modifiers, replacing 27 `.btn-*` classes and ten bare
   `button {}` rules.

   Scoped to `main`, and that is the point of the scope. Ten pages styled the
   bare `button` element, which reached past <main> into the rail and the
   header — that is why Collapse read COLLAPSE on user access. `main button`
   gives every page its buttons back without a markup change, and cannot touch
   shared chrome. `.btn` is the same thing by class, for a control that lives
   outside <main> or is an <a>. */
main button, .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  background: var(--gold);
  color: var(--bg);
  border: 1px solid transparent;
  border-radius: var(--radius);
  padding: 9px 16px;
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
main button:hover:not(:disabled), .btn:hover:not(:disabled) { background: var(--gold-hover); }
main button:disabled, .btn:disabled, .btn[aria-disabled="true"] { opacity: 0.45; cursor: not-allowed; }
main button:focus-visible, .btn:focus-visible { outline: 2px solid var(--border-active); outline-offset: 2px; }

/* Bordered. The second-most-common action on a page: it is a real control, it
   is just not the one the page is for. */
main button.ghost, .btn--ghost {
  background: transparent;
  color: var(--gold);
  border-color: var(--border);
}
main button.ghost:hover:not(:disabled), .btn--ghost:hover:not(:disabled) { background: var(--gold-bg); border-color: var(--border-active); }

main button.danger, .btn--danger {
  background: transparent;
  color: var(--red);
  border-color: var(--red-border);
}
main button.danger:hover:not(:disabled), .btn--danger:hover:not(:disabled) { background: var(--red-bg); }

/* Reads as text, behaves as a button. For the third action in a row, where a
   bordered box would make a toolbar out of a footnote. */
.btn--link {
  background: transparent;
  color: var(--gold-dim);
  border-color: transparent;
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
  padding: 4px 6px;
}
.btn--link:hover:not(:disabled) { color: var(--gold); background: transparent; }

.btn--sm { padding: 5px 10px; font-size: 11px; }

/* An action group. Filters change what you see; these change what exists, so
   they belong together and away from the filter row. */
.actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-4);
}

/* ---------------------------------------------------------------------------
   4. Controls — three shapes, three meanings, and nothing else
   ---------------------------------------------------------------------------
   Before this there were five ways to draw a control and three things they
   meant, and the mapping was scrambled: Performance used pills to filter by
   category and, four rows down, pills that looked identical to jump to a
   section. Marketing put underlined tabs above pills that behaved like tabs.
   The date window was `365d 90d 30d` on Performance and `90 DAYS 365 DAYS` on
   Customers — different shape, different label format, same job.

     .segmented   pick exactly ONE of a small set. Date windows, view
                  switches, metric switches. A radiogroup: mark the chosen one
                  `aria-checked="true"`.
     .tabs        switch which panel of THIS page you are looking at.
     .chip        a multi-select facet. Toggles independently; `aria-pressed`.

   Anything that navigates is a link and looks like a link. It does not borrow
   one of these.
   --------------------------------------------------------------------------- */

.segmented {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
.segmented > * {
  background: transparent;
  border: 0;
  border-right: 1px solid var(--border);
  color: var(--muted);
  font-family: var(--font-ui);
  font-size: 12px;
  letter-spacing: 0.04em;
  padding: 6px 14px;
  cursor: pointer;
  white-space: nowrap;
  text-decoration: none;
  transition: background 0.12s, color 0.12s;
}
.segmented > *:last-child { border-right: 0; }
.segmented > *:hover { color: var(--text); background: var(--bg-row); }
.segmented > [aria-checked="true"],
.segmented > .active {
  background: var(--gold-bg);
  color: var(--gold);
  font-weight: 600;
}
.segmented > *:focus-visible { outline: 2px solid var(--border-active); outline-offset: -2px; }

.tabs {
  display: flex;
  gap: var(--space-6);
  border-bottom: 1px solid var(--border);
  margin-bottom: var(--space-5);
  flex-wrap: wrap;
}
/* Six tabs do not wrap gracefully on a phone — a second row of tabs under a
   1px rule reads as two separate controls. Below the sidebar's drawer
   breakpoint they scroll sideways in place instead. */
@media (max-width: 860px) {
  .tabs { flex-wrap: nowrap; overflow-x: auto; -webkit-overflow-scrolling: touch; gap: var(--space-4); }
  .tabs > * { flex: 0 0 auto; }
}
.tabs > * {
  background: transparent;
  border: 0;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  padding: 11px 0 9px;
  color: var(--muted);
  font-family: var(--font-ui);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
}
.tabs > *:hover { color: var(--text); }
.tabs > [aria-selected="true"],
.tabs > .active { color: var(--gold); border-bottom-color: var(--gold); }
.tabs > *:focus-visible { outline: 2px solid var(--border-active); outline-offset: 2px; }

/* A second level, for a page that genuinely has tabs inside a tab (Marketing:
   three modules, and Shopify S&D has seven sub-modules under one of them).
   Smaller and sentence case, so the two rows read as a hierarchy rather than
   as two controls doing the same thing. */
.tabs--sub {
  gap: var(--space-5);
  margin-bottom: var(--space-4);
}
.tabs--sub > * {
  font-size: 12.5px;
  font-weight: 400;
  letter-spacing: 0.01em;
  text-transform: none;
  padding: 8px 0 7px;
}

.chips { display: flex; flex-wrap: wrap; gap: var(--space-2); align-items: center; }
.chip {
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius-pill);
  color: var(--muted);
  font-family: var(--font-ui);
  font-size: 11px;
  letter-spacing: 0.03em;
  padding: 4px 12px;
  cursor: pointer;
  white-space: nowrap;
}
.chip:hover { color: var(--text); border-color: var(--border-active); }
.chip[aria-pressed="true"], .chip.active {
  background: var(--gold-bg);
  border-color: var(--gold-border);
  color: var(--gold);
}
.chip:focus-visible { outline: 2px solid var(--border-active); outline-offset: 2px; }
/* The count that rides along with a facet.
   `--muted`, not `opacity: 0.65`. The opacity was multiplying an already
   translucent token: --muted (alpha 0.75) composited over the active chip's
   gold fill and then faded again resolved to #898473 on #2e2916 — 3.88:1,
   measured by axe on /performance. The token on its own is 7.0:1 and still
   reads as secondary, because --muted is already dimmer than --text.
   Stacking `opacity` on a translucent token is the single root cause behind
   every contrast failure in the 2026-08-17 audit. Do not reintroduce it. */
.chip .n { color: var(--muted); margin-left: 5px; font-variant-numeric: tabular-nums; }

/* ---------------------------------------------------------------------------
   5. Fields
   --------------------------------------------------------------------------- */

.input, .select, .textarea,
input[type="text"], input[type="search"], input[type="email"],
input[type="number"], input[type="date"], select, textarea {
  background: var(--input-bg);
  color: var(--text);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px 11px;
  font-family: var(--font-ui);
  font-size: 13px;
}
/* .field is a form ROW — the wrapper around a label and its control — used that way by
   every consumer (products.js ×53, calculator.html). It sat in the input selector list
   above until 2026-08-21, which drew a second border, fill, and padding around every
   already-bordered input: each form row rendered as a box inside a box. A row gets
   spacing only; the control inside it carries the border. */
.field { margin: 0 0 12px; }
input:focus, select:focus, textarea:focus {
  outline: none;
  border-color: var(--border-active);
}
input::placeholder, textarea::placeholder { color: var(--muted); }
label { font-size: 12px; color: var(--muted); }

/* ---------------------------------------------------------------------------
   6. Cards
   --------------------------------------------------------------------------- */

.card {
  background: var(--bg-card);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  padding: var(--space-5) var(--space-6);
  margin-bottom: var(--space-5);
}
.card-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

/* ---------------------------------------------------------------------------
   7. Tables
   --------------------------------------------------------------------------- */

/* One implementation, replacing 76 separately styled <table> elements across
   14 files. Scoped to `main` for the same reason the buttons are: it gives
   every existing table the shared look with no markup change, and it cannot
   reach the rail or the header. `.data-table` is the same thing by class.

   The print windows several pages open with `document.write` are separate
   documents and inherit none of this; they carry their own tiny stylesheet. */
main table, .data-table { width: 100%; border-collapse: collapse; font-size: 13px; }
main table th, .data-table th {
  text-align: left;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
  padding: 9px 10px;
  border-bottom: 1px solid var(--border);
  white-space: nowrap;
}
main table td, .data-table td {
  padding: 10px;
  border-bottom: 1px solid var(--border);
  vertical-align: middle;
}
main table tbody tr:hover td, .data-table tbody tr:hover td { background: var(--bg-row); }
main table tbody tr:last-child td, .data-table tbody tr:last-child td { border-bottom: 0; }
/* Digits that line up in a column read as a column. */
main table .num, .data-table .num, .data-table td.num, .data-table th.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
}
/* A wide table scrolls inside its own box; the page never pans sideways. */
.table-scroll { overflow-x: auto; }

/* ---------------------------------------------------------------------------
   8. Status pills
   ---------------------------------------------------------------------------
   There were two implementations and they disagreed about what a status looks
   like. index.css's `.badge.status-*` gave each of the six workflow states its
   own semantic token; the queue pages' `.pill.*` collapsed them onto three raw
   rgba values, so `partial` was blue on Products and amber on Labels, and
   `received` was purple on one and green on the other.

   index.css's mapping wins — it is the system of record and it is the one
   built on tokens. Both class names are kept so no page has to be re-marked-up;
   they now paint the same thing.
   --------------------------------------------------------------------------- */

.badge, .pill {
  display: inline-block;
  font-size: 9px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 2px 7px;
  border-radius: var(--radius);
  font-weight: 600;
  border: 1px solid transparent;
  white-space: nowrap;
}
.badge.status-on_order,     .pill.on_order     { background: var(--neutral-bg); color: var(--neutral); border-color: var(--neutral-border); }
.badge.status-in_transit,   .pill.in_transit   { background: var(--gold-bg);    color: var(--gold-fg); border-color: var(--gold-border); }
.badge.status-partial,      .pill.partial      { background: var(--blue-bg);    color: var(--blue);    border-color: var(--blue-border); }
.badge.status-received,     .pill.received     { background: var(--purple-bg);  color: var(--purple);  border-color: var(--purple-border); }
.badge.status-packed,       .pill.packed       { background: var(--amber-bg);   color: var(--amber);   border-color: var(--amber-border); }
.badge.status-published,    .pill.published    { background: var(--green-bg);   color: var(--green);   border-color: var(--green-border); }
.badge.status-concept,      .pill.concept      { background: var(--neutral-bg); color: var(--neutral); border-color: var(--neutral-border); }
.badge.status-in_production,.pill.in_production{ background: var(--gold-bg);    color: var(--gold-fg); border-color: var(--gold-border); }

/* ---------------------------------------------------------------------------
   9. Queue grouping — the shape five queue pages already share
   --------------------------------------------------------------------------- */

.group { margin-bottom: var(--space-8); }
.group-head {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  border-bottom: 1px solid var(--border);
  padding-bottom: var(--space-2);
  margin-bottom: var(--space-1);
}
.group-title { font-family: var(--font-display); font-size: 18px; font-weight: 400; margin: 0; }
.group-sub   { color: var(--muted); font-size: 12px; }
.group-count { margin-left: auto; color: var(--muted); font-size: 13px; font-variant-numeric: tabular-nums; }

/* ---------------------------------------------------------------------------
   10. Inline result messages
   --------------------------------------------------------------------------- */

.msg {
  padding: 10px 14px;
  border-radius: var(--radius);
  margin-top: var(--space-3);
  font-size: 13px;
  line-height: 1.5;
  border: 1px solid var(--border);
}
.msg.success { background: var(--green-bg); color: var(--green); border-color: var(--green-border); }
.msg.error   { background: var(--red-bg);   color: var(--red);   border-color: var(--red-border); }
.msg.warn    { background: var(--amber-bg); color: var(--amber); border-color: var(--amber-border); }

/* A one-line status readout that fills in after an action. Reserves its own
   height so the row below it does not jump when it does. */
.status { font-size: 12px; color: var(--muted); min-height: 18px; margin-bottom: var(--space-3); }

/* ---------------------------------------------------------------------------
   11. Links to a product card, from anywhere
   --------------------------------------------------------------------------- */

a.tea-link { color: inherit; text-decoration: none; }
a.tea-link:hover { color: var(--gold); text-decoration: underline; }

/* ---------------------------------------------------------------------------
   12. Motion
   --------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
