/* Optishake Venue Dashboard styles.
   Design tokens + component classes ported from the main site's
   src/styles/global.css so the dashboard matches optishake.info.
   Plain CSS (no Tailwind) — same approach as the main site. */
@import url('https://fonts.googleapis.com/css2?family=Archivo:ital,wdth,wght@0,62..125,400..900;1,62..125,400..900&family=Caveat:wght@600&display=swap');

:root {
  --orange:#ff6446; --orange-mid:#af4234; --darkred:#64231e;
  --black:#000000; --ink:#1a1411; --green:#324b46; --protein:#e1d2aa;
  --cream:#f6f0e3; --paper:#ffffff; --muted:#6f655a; --line:#e7ddc8;
  --disp:'Archivo','Helvetica Neue',Arial,sans-serif;
  /* Archivo's real width axis (62-125). 'Archivo Expanded' was never a
     Google family - the request 200s with that family silently dropped,
     so every display line rendered at normal width. Tune here, once. */
  --disp-stretch:125%;
  --font:'Archivo','Helvetica Neue',Arial,sans-serif;
  --maxw:1180px;
}

/* ── Two schemes, one set of names ────────────────────────────────────────
   The names above are the brand's (cream, paper, ink, line) and every rule in
   these sheets reaches for them by ROLE — cream is the page, paper is a card,
   ink is text. Dark mode therefore re-points the names rather than every
   rule: in the dark set "cream" is the near-black page, "paper" the lifted
   card, "ink" the cream text. The values are Backstage's dark palette (Admin
   app, constants/theme.ts) so the two staff surfaces read as one product — a
   real elevation ramp and ink lettering on orange, because cream-on-orange
   washes out against dark. The ramp was warm brown (hue ~26°) until
   2026-09-09; on real screens it read as autumn rather than as the lights
   being down, so the page/card/hover/line values are now the machine app's
   near-black (Optishake-android-app values/colors.xml), which Backstage moved
   to on the same day. The hue tints below still carry their own colour — a
   warning chip has to look like one — and the sand tint keeps just enough
   warmth to say "from Optishake".

   A pinned choice stamps data-theme on <html> before first paint
   (shared/theme_head.html) and wins both ways; with nothing stamped the device
   decides. The few colours that never flip — the PII banner, the test/demo
   stripe, map markers — are literals on purpose. Tints below replace the
   hand-picked hexes the chips and notices used to carry, so a warning chip is
   the same warning chip in both schemes. */
:root {
  --scheme: light;
  --on-orange: #fff; --on-ink: #fff; --on-darkred: #f6f0e3;
  --hover: #f6f0e3; --line-strong: #b9ae9c; --notice-bg: #fbf3e7;
  --tint-red: #fdeae5; --tint-red-line: #f3c4b8;
  --tint-amber: #fff6dd; --tint-amber-line: #eedfa8; --amber-ink: #7a5a00;
  --tint-green: #e7f0ea; --tint-green-line: #c9dcd2;
  --tint-blue: #e8eef5; --tint-blue-line: #cddced; --blue-ink: #2c4a6e;
  --tint-grey: #eeeae0; --tint-orange: #fff4ee; --tint-orange-strong: #fff8f0;
  --tint-purple: #ece7f5; --tint-purple-line: #d5cbe8; --purple-ink: #4a3a6e;
  --map-bg: #e8eef0;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --scheme: dark;
    --cream: #131212; --paper: #201e1d; --line: #3a3634; --ink: #f6f0e3; --muted: #b8afa6;
    --darkred: #f2a48f; --orange-mid: #ff8a72; --green: #8fc4b5; --protein: #322c22;
    --on-orange: #1a1411; --on-ink: #131212; --on-darkred: #1a1411;
    --hover: #33302e; --line-strong: #55504d; --notice-bg: #2a2724;
    --tint-red: #4a221c; --tint-red-line: #6b322a;
    --tint-amber: #3d2f14; --tint-amber-line: #5a4520; --amber-ink: #e2b45c;
    --tint-green: #1d3a2b; --tint-green-line: #2f5240;
    --tint-blue: #1f2d3d; --tint-blue-line: #2f4459; --blue-ink: #8fb4dc;
    --tint-grey: #2a2724; --tint-orange: #3a2419; --tint-orange-strong: #33201a;
    --tint-purple: #2e2540; --tint-purple-line: #463a5e; --purple-ink: #b9a6e0;
    --map-bg: #2c2a26;
  }
}
:root[data-theme="dark"] {
  --scheme: dark;
  --cream: #131212; --paper: #201e1d; --line: #3a3634; --ink: #f6f0e3; --muted: #b8afa6;
  --darkred: #f2a48f; --orange-mid: #ff8a72; --green: #8fc4b5; --protein: #322c22;
  --on-orange: #1a1411; --on-ink: #131212; --on-darkred: #1a1411;
  --hover: #33302e; --line-strong: #55504d; --notice-bg: #2a2724;
  --tint-red: #4a221c; --tint-red-line: #6b322a;
  --tint-amber: #3d2f14; --tint-amber-line: #5a4520; --amber-ink: #e2b45c;
  --tint-green: #1d3a2b; --tint-green-line: #2f5240;
  --tint-blue: #1f2d3d; --tint-blue-line: #2f4459; --blue-ink: #8fb4dc;
  --tint-grey: #2a2724; --tint-orange: #3a2419; --tint-orange-strong: #33201a;
  --tint-purple: #2e2540; --tint-purple-line: #463a5e; --purple-ink: #b9a6e0;
  --map-bg: #2c2a26;
}
html { color-scheme: var(--scheme); }

* { box-sizing:border-box; }
body { margin:0; font-family:var(--font); color:var(--ink); background:var(--cream);
  line-height:1.55; -webkit-font-smoothing:antialiased; overflow-wrap:break-word; }
h1,h2,h3 { margin:0 0 .35em; line-height:1.05; letter-spacing:-.02em; }
p { margin:0 0 1rem; }
a { color:inherit; text-decoration:none; }

/* ── Shared primitives (names match the main site) ─────────────────────────── */
.disp { font-family:var(--disp); font-stretch:var(--disp-stretch); font-weight:900; font-style:italic; letter-spacing:-.02em; }
.container { width:100%; max-width:var(--maxw); margin:0 auto; padding:0 clamp(20px,5vw,34px); }
.kicker { display:inline-block; font-size:.76rem; font-weight:800; letter-spacing:.2em;
  text-transform:uppercase; color:var(--orange-mid); margin-bottom:14px; }
.h2 { font-size:clamp(1.5rem,3.2vw,2.2rem); color:var(--ink); }
.lead { font-size:1.06rem; color:var(--muted); max-width:60ch; }
.muted { color:var(--muted); }

.btn { display:inline-flex; align-items:center; gap:10px; font-weight:800; font-size:1.02rem;
  padding:16px 32px; border-radius:999px; border:2px solid transparent; cursor:pointer;
  font-family:var(--font); transition:transform .15s ease, box-shadow .2s ease, background .15s ease, color .15s ease; }
.btn-primary { background:var(--orange); color:var(--on-orange); box-shadow:0 12px 32px rgba(255,100,70,.4); }
.btn-primary:hover { transform:translateY(-3px) scale(1.02); box-shadow:0 18px 44px rgba(255,100,70,.55); }
.btn-dark { background:var(--ink); color:var(--on-ink); }
.btn-dark:hover { transform:translateY(-3px); }
.btn-sm { padding:10px 24px; font-size:.95rem; }
.btn:disabled { opacity:.5; transform:none; box-shadow:none; cursor:default; }

/* ── Top bar ───────────────────────────────────────────────────────────────── */
.topbar { background:var(--paper); border-bottom:1px solid var(--line); }
.topbar-inner { display:flex; align-items:center; min-height:64px; }
.brand { font-family:var(--disp); font-stretch:var(--disp-stretch); font-style:italic; font-weight:900; font-size:1.3rem; color:var(--ink); }
.brand::after { content:''; display:inline-block; width:8px; height:8px; border-radius:50%;
  background:var(--orange); margin-left:4px; vertical-align:.1em; }

.page { padding:40px 0 80px; }
.page-head { margin-bottom:28px; }
.page-head h1 { font-size:clamp(1.9rem,4vw,3rem); color:var(--ink); margin:4px 0 0; }
.crumb { color:var(--muted); font-size:.85rem; font-weight:600; margin-bottom:16px; }
.crumb a:hover { color:var(--orange-mid); }

/* ── Section tools ─────────────────────────────────────────────────────────
   Shakeventory is four pages, not one: the report queue, and three errands
   beside it. The three spent their first days as grey links folded inside
   "Puls warehouse stock" — a shelf, which says nothing about a postcode or a
   parcel, and which nobody hunting for either would think to open. They are
   peers of the queue, so they sit beside it on all four pages, and they look
   like what they are. */
.tool-nav { display:flex; flex-wrap:wrap; gap:8px; margin:0 0 24px; }
.tool-nav a { display:inline-flex; align-items:center; font-weight:800;
  font-size:.92rem; color:var(--ink); background:var(--paper);
  border:1px solid var(--line); border-radius:999px; padding:9px 18px;
  transition:border-color .15s ease, color .15s ease, box-shadow .2s ease, transform .15s ease; }
.tool-nav a:hover { border-color:var(--orange); color:var(--darkred);
  transform:translateY(-1px); box-shadow:0 8px 20px rgba(0,0,0,.06); }
.tool-nav a:focus-visible { outline:none; border-color:var(--orange);
  box-shadow:0 0 0 3px rgba(255,100,70,.22); }
/* The page you are already on is a marker, not a button: it stays in the row
   so the set reads as one, and stops offering a click that goes nowhere. */
.tool-nav a.is-current { background:var(--ink); border-color:var(--ink);
  color:var(--on-ink); cursor:default; }
.tool-nav a.is-current:hover { transform:none; box-shadow:none;
  border-color:var(--ink); color:var(--on-ink); }

/* ── A fold that is a feature ──────────────────────────────────────────────
   <details> is two different things in this app, and they must not look
   alike. A control that is sometimes in the way — the queue's filter — folds
   away as muted text, and should: it is chrome. But reading Puls's shelf, or
   adding a gym Odoo has never heard of, is a feature, folded only because it
   is an occasional errand; "collapsible" is a fact about the markup, not
   about what it is for. A feature drawn as grey small print is a feature
   nobody finds. So this template gives the summary the weight of a button,
   which to a human is what it is. */
.feature-fold { margin:10px 0 14px; }
/* baseline, not centre: the summary wraps to two lines on a phone, and a
   marker centred against the block floats beside the gap between them. */
.feature-fold > summary { display:inline-flex; align-items:baseline; gap:9px;
  max-width:100%; cursor:pointer; list-style:none; text-align:left;
  font-weight:800; font-size:.92rem; color:var(--darkred);
  background:var(--paper); border:1px solid var(--line); border-radius:999px;
  padding:9px 18px;
  transition:border-color .15s ease, box-shadow .2s ease, transform .15s ease; }
.feature-fold > summary::-webkit-details-marker { display:none; }
.feature-fold > summary::before { content:'▸'; flex:none; color:var(--orange); }
.feature-fold[open] > summary::before { content:'▾'; }
.feature-fold > summary:hover { border-color:var(--orange);
  transform:translateY(-1px); box-shadow:0 8px 20px rgba(0,0,0,.06); }
.feature-fold > summary:focus-visible { outline:none; border-color:var(--orange);
  box-shadow:0 0 0 3px rgba(255,100,70,.22); }
.feature-fold[open] > summary { margin-bottom:12px; }

/* ── Cards ─────────────────────────────────────────────────────────────────── */
.card { background:var(--paper); border:1px solid var(--line); border-radius:18px; padding:22px; }
.card + .card, .stack > * + * { margin-top:22px; }
.panel-title { font-family:var(--disp); font-stretch:var(--disp-stretch); font-style:italic; font-weight:800; font-size:1.1rem;
  color:var(--darkred); margin:0 0 16px; }

/* details (venue info) */
details.card > summary { cursor:pointer; font-weight:800; color:var(--ink); list-style:none; }
details.card > summary::-webkit-details-marker { display:none; }
details.card > summary::before { content:'▸'; color:var(--orange-mid); margin-right:8px; }
details.card[open] > summary::before { content:'▾'; }

/* ── Metrics ───────────────────────────────────────────────────────────────── */
.grid { display:grid; gap:16px; }
.grid-3 { grid-template-columns:repeat(3,1fr); }
.grid-4 { grid-template-columns:repeat(4,1fr); }
.grid-2 { grid-template-columns:repeat(2,1fr); }

.metric { background:var(--paper); border:1px solid var(--line); border-radius:18px; padding:18px 20px; }
.metric-label { font-size:.72rem; font-weight:800; letter-spacing:.12em; text-transform:uppercase; color:var(--muted); }
.metric-value { font-family:var(--disp); font-stretch:var(--disp-stretch); font-style:italic; font-weight:900;
  font-size:clamp(1.4rem,2.4vw,1.9rem); color:var(--ink); margin-top:4px; line-height:1.05; }

.stat { display:flex; flex-direction:column; align-items:center; gap:4px; }
.stat-num { font-family:var(--disp); font-stretch:var(--disp-stretch); font-style:italic; font-weight:900; font-size:1.8rem; color:var(--ink); }
.stat-unit { font-size:.78rem; font-weight:700; color:var(--muted); text-transform:uppercase; letter-spacing:.08em; }

/* ── Tables ────────────────────────────────────────────────────────────────── */
.table-wrap { background:var(--paper); border:1px solid var(--line); border-radius:18px; overflow:hidden; }
.table-wrap.scroll { overflow-x:auto; }
.data-table { width:100%; border-collapse:collapse; font-size:.9rem; }
.data-table thead th { background:var(--cream); text-align:left; font-weight:800; color:var(--darkred);
  padding:11px 14px; font-size:.78rem; letter-spacing:.04em; text-transform:uppercase; white-space:nowrap; }
.data-table tbody td { padding:11px 14px; border-top:1px solid var(--line); }
.data-table tbody tr:hover { background:var(--hover); }
.num { text-align:right; font-variant-numeric:tabular-nums; }

/* ── Forms / filter bar ──────────────────────────────────────────────────────── */
.filter-bar { display:flex; flex-wrap:wrap; align-items:flex-end; gap:18px; }
.field { display:flex; flex-direction:column; gap:6px; }
.field > span { font-size:.74rem; font-weight:800; letter-spacing:.08em; text-transform:uppercase; color:var(--muted); }
.input, select.input, input.input { font-family:var(--font); font-size:.95rem; color:var(--ink);
  background:var(--paper); border:1px solid var(--line); border-radius:10px; padding:10px 12px; }
.input:focus { outline:none; border-color:var(--orange); box-shadow:0 0 0 3px rgba(255,100,70,.18); }

.notice { background:var(--notice-bg); border:1px solid var(--line); border-left:4px solid var(--orange);
  border-radius:12px; padding:14px 16px; color:var(--darkred); }

/* ── Tabs ──────────────────────────────────────────────────────────────────── */
.tabs { display:flex; gap:6px; border-bottom:1px solid var(--line); margin-bottom:22px; }
.tab-btn { font-family:var(--font); font-weight:800; font-size:.95rem; color:var(--muted);
  background:none; border:none; border-bottom:3px solid transparent; padding:12px 18px;
  margin-bottom:-1px; cursor:pointer; transition:color .15s ease, border-color .15s ease; }
.tab-btn:hover { color:var(--ink); }
.tab-btn.active { color:var(--darkred); border-bottom-color:var(--orange); }
.tab-panel { display:none; }
.tab-panel.active { display:block; }

/* ── Charts ────────────────────────────────────────────────────────────────── */
/* One aspect ratio everywhere (inline + popup) so the chart reads the same on
   any screen. The frame owns the size; the Vega spec is "container"-sized. */
:root { --chart-ar:1.6; }

.chart-frame { position:relative; width:100%; aspect-ratio:var(--chart-ar);
  cursor:zoom-in; -webkit-tap-highlight-color:transparent; }
.chart-canvas { position:absolute; inset:0; width:100%; height:100%; }
.chart-canvas .vega-embed { width:100%; height:100%; }
.chart-canvas .vega-embed svg, .chart-canvas .vega-embed canvas { max-width:100%; }

.chart-expand { position:absolute; top:8px; right:8px; z-index:2;
  display:inline-flex; align-items:center; gap:6px; font-family:var(--font);
  font-size:.72rem; font-weight:800; letter-spacing:.06em; text-transform:uppercase;
  color:var(--muted); background:var(--paper); border:1px solid var(--line);
  border-radius:999px; padding:5px 11px; cursor:zoom-in;
  box-shadow:0 2px 8px rgba(26,20,17,.06); transition:color .15s ease, border-color .15s ease; }
.chart-frame:hover .chart-expand { color:var(--darkred); border-color:var(--orange); }

/* Fullscreen popup */
.chart-modal { position:fixed; inset:0; z-index:1000; display:flex;
  align-items:center; justify-content:center; padding:clamp(12px,3vw,32px);
  background:rgba(26,20,17,.62); -webkit-backdrop-filter:blur(3px); backdrop-filter:blur(3px);
  opacity:0; transition:opacity .18s ease; }
.chart-modal[hidden] { display:none; }
.chart-modal.open { opacity:1; }
.chart-modal-card { position:relative; background:var(--paper); border:1px solid var(--line);
  border-radius:18px; padding:clamp(18px,3vw,26px); max-width:min(1150px,100%);
  box-shadow:0 30px 80px rgba(26,20,17,.35); }
/* Fit the frame within the viewport at the fixed aspect ratio (no stretch,
   no overflow) — width is capped by both the screen width and the height the
   screen can give at this aspect ratio. */
.chart-modal-card .chart-frame { cursor:default;
  width:min(88vw, calc((100dvh - 200px) * var(--chart-ar))); max-width:100%; }

.chart-modal-close { position:absolute; top:10px; right:10px; z-index:3;
  display:inline-flex; align-items:center; justify-content:center; width:34px; height:34px;
  font-size:1.05rem; line-height:1; color:var(--muted); background:var(--cream);
  border:1px solid var(--line); border-radius:999px; cursor:pointer;
  transition:color .15s ease, border-color .15s ease; }
.chart-modal-close:hover { color:var(--darkred); border-color:var(--orange); }

/* ── HTMX loading indicator ──────────────────────────────────────────────────── */
.htmx-indicator { opacity:0; transition:opacity 150ms ease-in; }
.htmx-request.htmx-indicator, .htmx-request .htmx-indicator { opacity:1; }
.loading-text { color:var(--muted); font-weight:600; }

@media (max-width:1000px) { .grid-4 { grid-template-columns:repeat(2,1fr); } .grid-3 { grid-template-columns:repeat(2,1fr); } }
@media (max-width:680px) { .grid-4, .grid-3, .grid-2 { grid-template-columns:1fr; } }

/* ── Shakeventory additions ─────────────────────────────────────────── */
.topbar-user { display: flex; align-items: center; gap: 12px; }
.inline-form { display: inline; }
.btn-row { display: flex; gap: 10px; margin-top: 12px; flex-wrap: wrap; }
.small { font-size: 0.85rem; }
.muted { color: var(--muted); }

.login-card { max-width: 420px; margin: 8vh auto; text-align: center;
  display: flex; flex-direction: column; gap: 14px; align-items: center; }

.queue-stats { margin: 14px 0; }
.row-link { cursor: pointer; }
.row-link:hover { background:var(--hover); }

.chip { display: inline-block; padding: 2px 10px; border-radius: 999px;
  font-size: 0.78rem; font-weight: 600; border: 1px solid var(--line);
  background: var(--paper); white-space: nowrap; }
.chip-unhandled { background: var(--tint-red); color: var(--darkred); border-color: var(--tint-red-line); }
.chip-in_progress { background: var(--tint-amber); color: var(--amber-ink); border-color: var(--tint-amber-line); }
.chip-handled { background: var(--tint-green); color: var(--green); border-color: var(--tint-green-line); }
.chip-ok { background: var(--tint-green); color: var(--green); border-color: var(--tint-green-line); }
.chip-warn { background: var(--tint-amber); color: var(--amber-ink); border-color: var(--tint-amber-line); }
.chip-bad { background: var(--tint-red); color: var(--darkred); border-color: var(--tint-red-line); }
.chip-info { background: var(--tint-blue); color: var(--blue-ink); border-color: var(--tint-blue-line); }
.chip-free { background: var(--protein); color: var(--ink); }
.chip-pick { cursor: pointer; }
.chip-pick:hover { background:var(--hover); }

.notice.error { background: var(--tint-red); border: 1px solid var(--tint-red-line); color: var(--darkred); }

/* ── What the counter wrote ────────────────────────────────────────────────
   The note is the one part of a report written by a person rather than a
   machine, and it is where a gym says the thing that is not in any field —
   the door code changed, the machine leaks, stop sending chocolate. It used
   to sit as one line in the meta list and readers walked past it. So: its own
   card, at the top, in the brand orange, at a size that is hard to skim over.
   Loud on purpose; that is the whole point of the change. */
.inventory-notes {
  display:flex; gap:16px; align-items:flex-start;
  background:linear-gradient(180deg,var(--tint-orange) 0%,var(--paper) 100%);
  border:1px solid var(--tint-red-line); border-left:6px solid var(--orange);
  box-shadow:0 10px 30px rgba(255,100,70,.13);
}
.inventory-notes-mark {
  font-size:1.7rem; line-height:1; color:var(--orange);
  background:var(--paper); border:1px solid var(--tint-red-line); border-radius:12px;
  padding:10px 12px; flex:0 0 auto;
}
.inventory-notes-title {
  margin:0 0 6px; font-size:.78rem; font-weight:800; letter-spacing:.14em;
  text-transform:uppercase; color:var(--orange-mid);
}
.inventory-notes-body {
  margin:0; font-size:clamp(1.05rem,1.9vw,1.3rem); line-height:1.45;
  color:var(--ink); font-weight:600; white-space:pre-wrap;
}
@media (max-width:520px) {
  .inventory-notes { gap:12px; }
  .inventory-notes-mark { font-size:1.3rem; padding:8px 10px; }
}

.meta-list { margin: 0; }
.meta-list > div { display: flex; gap: 10px; padding: 4px 0; }
.meta-list dt { min-width: 130px; color: var(--muted); font-size: 0.9rem; }
.meta-list dd { margin: 0; }

.stock-red td { background: var(--tint-red); }
.stock-amber td { background: var(--tint-amber); }

.order-entry-row { display: flex; gap: 10px; align-items: flex-start; margin: 8px 0 16px; }
.order-entry-row textarea { flex: 1; }

.order-line { display: grid; grid-template-columns: 1fr 90px 90px 120px 40px;
  gap: 8px; align-items: center; padding: 8px 0; border-bottom: 1px solid var(--line); }
.order-line .line-meta { grid-column: 1 / -1; display: flex; gap: 8px;
  flex-wrap: wrap; align-items: center; }
.order-line-status { grid-template-columns: 1fr; }

@media (max-width: 680px) {
  .order-line { grid-template-columns: 1fr 70px; }
}

/* ── Brand lockup (mirrors the Admin app's BrandHeader) ─────────────── */
.brand-lockup { display: flex; align-items: flex-end; gap: 0; text-decoration: none; }
.wordmark { display: inline-block; width: 150px; height: 24px;
  background-color: var(--orange);
  -webkit-mask: url('wordmark.png') no-repeat left center / contain;
  mask: url('wordmark.png') no-repeat left center / contain; }
.brand-script { font-family: 'Caveat', cursive; font-weight: 600;
  font-size: 1.5rem; line-height: 1; color: var(--ink); margin: 0 0 -3px -2px; }

/* ── Umbrella nav ───────────────────────────────────────────────────── */
/* staff.optishake.info hosts more than one app now, so the topbar carries a
   way between them. Lives here rather than with either app because both wear
   it. On a phone the brand lockup plus two links plus sign-out will not fit,
   so the nav drops to its own line under the bar. */
.topbar-nav { display: flex; gap: 18px; margin-left: 28px; font-weight: 700;
  font-size: .95rem; }
.topbar-nav a { color: var(--muted); padding: 4px 0;
  border-bottom: 2px solid transparent; white-space: nowrap; }
.topbar-nav a:hover { color: var(--ink); }
.topbar-nav a.is-current { color: var(--darkred); border-bottom-color: var(--orange); }
@media (max-width: 680px) {
  .topbar-inner { flex-wrap: wrap; padding-bottom: 6px; }
  .topbar-nav { order: 3; width: 100%; margin: 0 0 2px; gap: 16px; font-size: .9rem; }
}

/* ── Report-page rhythm ─────────────────────────────────────────────── */
.back-link { margin: 10px 0 0; }
.stack { margin-bottom: 18px; }
.grid.stack { gap: 18px; }
.grid-2 { align-items: stretch; }
.action-card { display: flex; flex-direction: column; }
.action-card-buttons { margin-top: auto; display: flex; gap: 10px; padding-top: 12px; }

/* ── Order entry ────────────────────────────────────────────────────── */
.order-toolbar { display: flex; align-items: center; gap: 16px; margin: 12px 0 16px; flex-wrap: wrap; }
/* Adding a product and pricing it in Odoo are one job, so they sit together at
   the left; the show-all switch is a setting rather than an action and goes to
   the far end. On a narrow screen the row wraps and the spacer collapses. */
.order-toolbar-spacer { flex-grow: 1; }
/* Slide switch, mirroring the Admin app's StaffToggle / drink on-off switch:
   a fixed-size track that fills with accent, and a thumb that slides. The
   native checkbox cannot be used as-is — the mobile-polish reset below strips
   `appearance` from every input, which left this control with no visible
   state at all — so the real checkbox stays for state and keyboard focus and
   the track is drawn beside it. */
.switch { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
.switch > input { position: absolute; opacity: 0; width: 0; height: 0; }
.switch-track { position: relative; flex: none; width: 40px; height: 24px;
  border-radius: 12px; background: var(--line); transition: background .15s ease; }
.switch-thumb { position: absolute; top: 3px; left: 3px; width: 18px; height: 18px;
  border-radius: 50%; background: var(--paper); transition: left .15s ease;
  box-shadow: 0 1px 3px rgba(26,20,17,.25); }
.switch > input:checked + .switch-track { background: var(--orange); }
.switch > input:checked + .switch-track .switch-thumb { left: 19px; }
.switch > input:focus-visible + .switch-track {
  box-shadow: 0 0 0 3px rgba(255,100,70,.18); }
/* Label follows the state, as in the Admin app: ink when on, muted when off. */
.switch-label { color: var(--muted); transition: color .15s ease; }
.switch > input:checked ~ .switch-label { color: var(--ink); font-weight: 700; }
.order-line { grid-template-columns: 1fr 90px 90px 40px; }
.order-line .line-sku { min-width: 0; }
.order-line-flash { animation: line-flash 1.2s ease-out; }
@keyframes line-flash {
  0% { background: var(--tint-orange); }
  100% { background: transparent; }
}
.order-entry-row textarea { resize: vertical; }
.field .input { width: 100%; box-sizing: border-box; }
.topbar-user { margin-left: auto; }

/* ── Queue page ─────────────────────────────────────────────────────── */
/* Says out loud which filter the queue is currently narrowed to. */
.filter-active { color: var(--orange-mid); font-weight: 700; }
.metric-sub { color: var(--muted); font-size: 0.78rem; margin-top: 2px; }
.region-cell { white-space: nowrap; }
.machine-tag { white-space: nowrap; }
.loading-notice { border-left-color: var(--line); background: var(--paper);
  color: var(--muted); font-style: italic; }

/* ── Report page v3 ─────────────────────────────────────────────────── */
.status-line { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 6px; }
.next-inv-form { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-top: 6px; }
.next-inv-form .input-days { width: 70px; }
.extra-item { white-space: nowrap; }
.btn-row-spacer { flex-grow: 1; }
.chip-empty { background: var(--tint-grey); color: var(--muted); border-color: var(--line); }

/* ── Raw report layout ──────────────────────────────────────────────── */
.raw-report { border-top: 1px solid var(--line); padding-top: 14px; margin-top: 14px; }
.raw-report:first-child { border-top: 0; margin-top: 0; }
.raw-report-id { margin: 0 0 8px; }
.raw-fields { display: grid; grid-template-columns: 1fr 1fr; column-gap: 32px; }
.raw-longtext { display: flex; gap: 12px; align-items: baseline; margin: 8px 0;
  padding: 8px 12px; border-radius: 10px; background: var(--cream); }
.raw-longtext-filled { background: var(--tint-amber); border: 1px solid var(--tint-amber-line); }
.raw-longtext-label { color: var(--muted); font-size: 0.9rem; min-width: 100px; flex-shrink: 0; }
.raw-inventory { margin-top: 10px; }
@media (max-width: 680px) {
  .raw-fields { grid-template-columns: 1fr; }
}

/* ── Empty queue celebration ────────────────────────────────────────── */
.empty-state { text-align: center; padding: 48px 24px; }
.empty-state .lead { max-width: 480px; margin: 10px auto; }
.empty-emoji { font-size: 3.2rem; display: inline-block;
  animation: shake-it 1.6s ease-in-out 2; transform-origin: 50% 90%; }
@keyframes shake-it {
  0%, 100% { transform: rotate(0); }
  15% { transform: rotate(-14deg); }
  30% { transform: rotate(12deg); }
  45% { transform: rotate(-10deg); }
  60% { transform: rotate(8deg); }
  75% { transform: rotate(-5deg); }
  90% { transform: rotate(3deg); }
}

/* ── Mobile polish ──────────────────────────────────────────────────── */
/* iOS renders un-reset buttons/inputs in its own blue — normalize.
   
   Checkboxes and radios are EXCLUDED, and that exclusion is load-bearing. The reset
   was written for text fields and buttons, but `input` caught every type: a bare
   checkbox lost its box, its tick and its focus ring and rendered as nothing at all.
   Five of them across the three apps were invisible — the locator's map filters, two
   "show all products" toggles, and the demote column on CTRL's retire page, which had
   copy telling people to tick a control that was not there. Shakeventory's `.switch`
   works around the same reset by hiding its input on purpose and drawing a track
   beside it; that still works, because it sets opacity:0 itself. */
button, .btn, .input, select, textarea,
input:not([type="checkbox"]):not([type="radio"]) {
  -webkit-appearance: none;
  appearance: none;
  font: inherit;
  color: var(--ink);
}

/* Give the ones we just handed back their box a size worth hitting and the brand
   colour, rather than the browser default blue. */
input[type="checkbox"], input[type="radio"] {
  width: 18px; height: 18px; margin: 0;
  accent-color: var(--orange);
  cursor: pointer;
}
.btn { background-clip: padding-box; }
@media (max-width: 680px) {
  /* the greeting is a nicety; the sign-out button is the function */
  .topbar-user > .muted { display: none; }
  .topbar-inner { gap: 8px; }
  .brand-script { font-size: 1.3rem; }
}
select.input, .filter-bar select {
  background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%236f655a' stroke-width='1.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
  padding-right: 28px;
}
/* Base .btn never declared a background, so every browser painted its own
   default — gray on desktop, blue on mobile. Make it explicit. White with a
   visible border rather than the earlier grey wash: the grey read as
   disabled, and "Save to Odoo" was being walked past as inactive. */
.btn { background: var(--paper); color: var(--ink); border-color: var(--line-strong); }
/* Hover scoped away from the filled variants: an unscoped .btn:hover outranks
   .btn-primary/.btn-dark and would paint their white labels on a white fill. */
.btn:not(.btn-primary):not(.btn-dark):hover { background: var(--paper); border-color: var(--ink); }
.btn-primary { background: var(--orange); color:var(--on-orange); border-color: transparent; }
.btn-dark { background: var(--ink); color:var(--on-ink); border-color: transparent; }
/* The keyboard ring in brand orange rather than the browser's blue. */
.btn:focus-visible { outline: 3px solid rgba(255,100,70,.5); outline-offset: 2px; }
.trend { font-weight: 700; white-space: nowrap; }
.trend-up { color: var(--green); }
.trend-down { color: var(--darkred); }
.trend-flat { color: var(--muted); }

/* ── Gym search ─────────────────────────────────────────────────────── */
.gym-search { display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  margin: 0 0 4px; }
.gym-search-label { font-size: .74rem; font-weight: 800; letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted); }
.gym-search-input { flex: 1 1 320px; max-width: 460px; }
/* Safari draws its own clear button on type=search; the reset above eats it,
   so ask for it back — clearing the box is how you get the queue back. */
.gym-search-input::-webkit-search-cancel-button { -webkit-appearance: searchfield-cancel-button; }

.search-summary { margin: 0 0 14px; }
.gym-result + .gym-result { margin-top: 18px; }
.gym-result-head { display: flex; align-items: baseline; gap: 10px;
  flex-wrap: wrap; margin-bottom: 14px; }
.gym-result-head h2 { font-size: 1.25rem; margin: 0; }
.gym-result .no-reports { margin: 0; }

/* ── Message the gym ────────────────────────────────────────────────── */
.gym-messages-sent { margin-top: 12px; }
.gym-messages-sent .notice { background: var(--tint-green); border-left-color: var(--green); }

/* ── Combined (multi-machine) handling ──────────────────────────────── */
.chip-group { background: var(--tint-purple); color: var(--purple-ink); border-color: var(--tint-purple-line);
  cursor: pointer; margin-left: 4px; }
.chip-group:hover { background: var(--tint-purple-line); }
.group-notice { display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  border-left-color: var(--purple-ink); background: var(--tint-purple); color: var(--purple-ink); margin-bottom: 18px; }
/* The sentence must be ONE flex item. Left bare, its <strong> became a flex
   item of its own and the line broke either side of "2 machines". */
.group-notice-text { flex: 1 1 320px; }
.group-notice .btn { margin-left: auto; flex: none; }
/* ── Handled = read-only ─────────────────────────────────────────────── */
/* The banner is a <form> so Reopen can live inside it; same one-flex-item
   rule as .group-notice-text above. */
.locked-notice { display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  border-left-color: var(--green); margin-bottom: 18px; }
.locked-notice-text { flex: 1 1 320px; }
.locked-notice .btn { margin-left: auto; flex: none; }
/* Sits straight under the line list, so it needs no rule of its own above. */
.filed-quotations.filed-readonly { margin-top: 0; border-top: 0; padding-top: 0; }
/* Per-machine figures behind a merged row — present for auditing, quiet
   enough that the combined number stays the one you read. */
.breakdown { display: block; margin-top: 2px; }

/* ── Compose: internal note vs message to the gym ───────────────────── */
/* The one thing this layout has to make obvious is which box leaves the
   building. Same shape, deliberately different colour, audience named in the
   kicker — so it reads without anyone having to remember a convention. */
.compose { display: grid; grid-template-columns: 1fr 1fr; gap: 16px;
  align-items: stretch; margin: 20px 0 6px; }
.compose-solo { grid-template-columns: 1fr; }
.compose-block { display: flex; flex-direction: column; gap: 8px;
  padding: 14px 16px; border: 1px solid var(--line); border-left-width: 4px;
  border-radius: 14px; }
.compose-internal { background: var(--cream); border-left-color: var(--line-strong); }
.compose-outbound { background: var(--tint-orange); border-left-color: var(--orange); }
.compose-kicker { display: flex; align-items: center; gap: 7px; margin: 0;
  font-size: .72rem; font-weight: 800; letter-spacing: .1em;
  text-transform: uppercase; }
.compose-internal .compose-kicker { color: var(--muted); }
.compose-outbound .compose-kicker { color: var(--orange-mid); }
/* The textarea grows so both columns end level however long the hints run. */
.compose-block textarea { flex: 1 1 auto; width: 100%; box-sizing: border-box;
  background: var(--paper); resize: vertical; }
.compose-hint { margin: 0; font-size: .8rem; line-height: 1.45; color: var(--muted); }
.compose-outbound .compose-hint strong { color: var(--darkred); font-weight: 800; }

.tracking-note { margin: 2px 0 0; }
.tracking-note .muted { margin-right: 8px; text-transform: uppercase;
  letter-spacing: .08em; font-weight: 800; }

@media (max-width: 680px) { .compose { grid-template-columns: 1fr; } }

/* ── Icon buttons ───────────────────────────────────────────────────── */
/* .btn is a pill sized for words: its 24px side padding overflowed the 40px
   grid column the remove button sits in, pushing the glyph off-centre. An
   icon button is a square with no padding at all — the flex centring does
   the work. */
.btn-icon { width: 34px; height: 34px; padding: 0; gap: 0; flex: none;
  justify-content: center; align-items: center; justify-self: center;
  border-radius: 50%; font-size: .95rem; line-height: 1; }

/* The narrow-screen order line. This has to live at the END of the file: the
   earlier @media rule for .order-line is declared BEFORE the desktop
   four-column one, so it never won and the product select collapsed to the
   width of its own dropdown arrow on a phone. Product gets its own row;
   qty / unit / remove share the next. Status lines keep their single column. */
@media (max-width: 680px) {
  .order-line:not(.order-line-status) { grid-template-columns: 1fr 88px 34px; }
  .order-line:not(.order-line-status) .line-sku { grid-column: 1 / -1; }
  /* A grid item's min-width is auto, and an <input>'s intrinsic width is ~177px
     — so without this the qty field refuses to shrink and pushes the row past
     the screen edge. */
  .order-line:not(.order-line-status) > * { min-width: 0; }
}

/* ── Odoo: the order as Puls receives it ────────────────────────────── */
/* The panel is Puls's world inside our order card: one cream surface on
   which everything is either a fact (the receipt), a warning (the advice
   boxes) or a lever (swap, file, undo). */
/* The block sits between the order lines and the notes now, and carries no
   rule or padding of its own: with nothing filed and the panel unopened it is
   an empty div, and a border there would draw a line across the card for no
   reason. Spacing comes from the panel and the hint themselves. */
.filed-hint { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 0 0 4px; }

.odoo-panel { margin-top: 14px; background: var(--cream);
  border: 1px solid var(--line); border-radius: 14px; padding: 18px 20px; }
.odoo-panel:empty { display: none; }
.odoo-panel > p:last-child { margin-bottom: 0; }
.odoo-state { margin: 0 0 8px; display: flex; gap: 8px; flex-wrap: wrap; }
.odoo-error-lead { margin: 0 0 8px; font-weight: 600; max-width: 62ch; }
.odoo-error-raw { opacity: .8; }
.odoo-panel code { font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: .85em; background: var(--paper); border: 1px solid var(--line);
  border-radius: 6px; padding: 1px 6px; white-space: nowrap; }
/* Direct child only: this is the error state's Try-again form, and without
   the > it reached the last candidate card, the threshold Save and the
   filed-row Cancel, nudging each out of line with its siblings. */
.odoo-panel > form:last-child { margin-top: 10px; }

.odoo-head { display: flex; align-items: flex-start; justify-content: space-between;
  gap: 12px; margin-bottom: 14px; }
.odoo-kicker { margin: 0 0 2px; font-size: .72rem; font-weight: 800;
  letter-spacing: .14em; text-transform: uppercase; color: var(--orange-mid); }
.odoo-title { margin: 0; font-weight: 800; }
.odoo-title .muted { font-weight: 400; margin-left: 6px; }
/* Quiet on purpose: re-answering "which customer is this?" is a rare repair,
   not one of the panel's two ordinary buttons, and it must not compete with
   them for the eye. */
.odoo-relink { margin-top: 4px; }
.odoo-relink button { background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; font-size: .82rem; color: var(--muted);
  text-decoration: underline; text-underline-offset: 2px; }
.odoo-relink button:hover { color: var(--ink); }

/* The receipt. Numbers right and tabular; the product owns the width. */
.odoo-lines { width: 100%; border-collapse: collapse; font-size: .95rem;
  margin-bottom: 12px; }
.odoo-lines td { padding: 8px 10px; vertical-align: baseline; }
.odoo-line td { border-top: 1px solid var(--line); }
.odoo-lines tbody tr:first-child td { border-top: 0; }
.odoo-qty { white-space: nowrap; color: var(--muted); width: 1%; }
.odoo-prod { width: 100%; }
.odoo-prod .chip { margin-left: 6px; white-space: normal; }
.odoo-cost { white-space: nowrap; font-variant-numeric: tabular-nums; }
.odoo-total td { border-top: 2px solid var(--ink); font-weight: 800; padding-top: 10px; }
.odoo-total .num { white-space: nowrap; font-variant-numeric: tabular-nums; }
.odoo-savings { margin: 0 0 12px; color: var(--green); }

/* Advice: the stock warning and its substitutes, full width under the line
   they belong to rather than squeezed into a fourth column. */
.odoo-advice-row td { padding-top: 0; border-top: 0; }
.odoo-advice { background: var(--paper); border: 1px solid var(--line);
  border-radius: 12px; padding: 12px 14px; margin: 2px 0 10px; }
.odoo-advice-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.threshold-form { display: flex; align-items: center; gap: 8px; margin-left: auto; }
.threshold-form label { display: inline-flex; align-items: center; gap: 6px;
  white-space: nowrap; }
.input-tiny { width: 64px; padding: 6px 8px; }
.threshold-form .btn-sm { padding: 6px 14px; font-size: .82rem; }
.odoo-advice-hint { margin: 10px 0 8px; }
.odoo-advice > p:last-child { margin-bottom: 0; }

/* Pick-cards: up to ten substitutes (or gym candidates) in a grid that
   stays a grid — name on one line, the numbers under it, click to act. */
.pick-grid { display: grid; gap: 8px; align-items: stretch;
  /* min() lets the cards shrink below 220px when the container itself is
     narrower — without it the grid props the whole table open on a phone. */
  grid-template-columns: repeat(auto-fill, minmax(min(220px, 100%), 1fr)); }
/* Candidate buttons live in their own forms. The form is a real flex box —
   not display:contents — because the busy dim below paints opacity on the
   form, and a boxless element has nothing to paint on. */
.pick-grid form { display: flex; }
.pick-grid form .pick-card { flex: 1 1 auto; }
.pick-card { display: flex; flex-direction: column; gap: 2px; text-align: left;
  background: var(--paper); border: 1px solid var(--line); border-radius: 10px;
  padding: 9px 12px; cursor: pointer; font-family: var(--font);
  transition: border-color .15s ease, box-shadow .15s ease; }
.pick-card:hover { border-color: var(--orange);
  box-shadow: 0 2px 10px rgba(255,100,70,.15); }
.pick-card-name { font-weight: 700; font-size: .9rem; color: var(--ink); }
/* Cards sharing a grid row share a height (stretch above), and the stats
   line sits on the card's bottom edge — a one-line name next to a two-line
   one still shows its numbers level with its neighbours'. */
.pick-card-meta { margin-top: auto; font-size: .8rem; color: var(--muted);
  font-variant-numeric: tabular-nums; display: flex; align-items: center;
  gap: 8px; }
/* Popularity against the list's favourite — a bar says popular-or-niche
   without making anyone read five-digit pour counts. */
.pop-bar { flex: none; width: 52px; height: 6px; border-radius: 3px;
  background: var(--line); overflow: hidden; }
.pop-fill { display: block; height: 100%; border-radius: 3px;
  background: var(--orange); }

.odoo-skipped { margin: 0 0 12px; }
.odoo-skipped-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 0 0 6px; }
.odoo-skipped ul { margin: 0; padding-left: 20px; }

/* Folded forms: correct the address, create the gym. Both are features and
   fold on the shared .feature-fold template, so only their own grid of
   labelled fields lives here — placeholders are not labels. */
.odoo-gym-form { display: grid; grid-template-columns: repeat(2, 1fr);
  gap: 10px 12px; margin-top: 10px; }
.field-wide { grid-column: 1 / -1; }
.odoo-gym-form-submit { grid-column: 1 / -1; }
.odoo-file { display: flex; gap: 10px; flex-wrap: wrap; margin: 4px 0 0; }

/* Filed quotations: what already went to Puls, and where it stands. */
.filed-quotations { margin-top: 16px; border-top: 1px solid var(--line);
  padding-top: 12px; }
.filed-title { margin: 0 0 8px; font-size: .72rem; font-weight: 800;
  letter-spacing: .14em; text-transform: uppercase; color: var(--muted); }
.filed-row { display: flex; align-items: flex-start; justify-content: space-between;
  gap: 6px 12px; padding: 10px 0; flex-wrap: wrap; }
.filed-row + .filed-row { border-top: 1px solid var(--line); }
.filed-main { flex: 1 1 320px; min-width: 0; }
.filed-id { margin: 0 0 2px; display: flex; align-items: center; gap: 8px;
  flex-wrap: wrap; }
.filed-total { font-variant-numeric: tabular-nums; }
.filed-delivery { margin: 0 0 2px; overflow-wrap: anywhere; }
.tracking-link { color: var(--orange-mid); text-decoration: underline;
  text-underline-offset: 2px; }
.tracking-link:hover { color: var(--darkred); }
.filed-meta { margin: 0; }
.filed-actions { display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
  flex: none; max-width: 100%; }

/* Anything in the panel that is mid-flight says so. */
.odoo-panel form.htmx-request { opacity: .55; pointer-events: none; }

@media (max-width: 680px) {
  .odoo-panel { padding: 14px 12px; }
  .odoo-lines td { padding: 8px 4px; }
  .odoo-advice { padding: 10px; }
  .odoo-gym-form { grid-template-columns: 1fr; }
  .threshold-form { margin-left: 0; }
}

/* ---- Puls warehouse stock, on the queue page ------------------------- */

.stock-controls { display: flex; align-items: center; gap: 12px;
  flex-wrap: wrap; margin: 0 0 10px; }
.stock-search { flex: 1 1 260px; max-width: 420px; }
.stock-search::-webkit-search-cancel-button { -webkit-appearance: searchfield-cancel-button; }

/* Segmented control: the radios themselves are hidden, the labels are the
   buttons. :has keeps the "on" look honest when the browser restores a
   checked radio we did not render as selected. */
.stock-scope { display: inline-flex; border: 1px solid var(--line);
  border-radius: 999px; overflow: hidden; background: var(--paper); }
.stock-scope-option { cursor: pointer; padding: 7px 14px; font-size: .88rem;
  color: var(--muted); white-space: nowrap; }
.stock-scope-option + .stock-scope-option { border-left: 1px solid var(--line); }
.stock-scope-option input { position: absolute; opacity: 0; pointer-events: none; }
.stock-scope-option.is-on, .stock-scope-option:has(input:checked) {
  background: var(--cream); color: var(--ink); font-weight: 700; }
.stock-scope-option:focus-within { box-shadow: inset 0 0 0 2px rgba(255,100,70,.35); }

.stock-summary { margin: 0 0 12px; }
/* Count, state and box hint in one right-aligned stack, so the state is
   never the thing that falls off a narrow screen. */
.stock-shelf { display: flex; flex-wrap: wrap; gap: 4px 8px;
  align-items: baseline; justify-content: flex-end; }
.stock-count { font-weight: 700; }
/* Fixed layout, or a long Odoo product name pushes the count column off the
   card — .table-wrap clips rather than scrolls, so "off" means "gone". */
.stock-table { table-layout: fixed; }
.stock-table th:last-child, .stock-table td:last-child { width: 170px; }
.stock-table td:first-child { overflow-wrap: anywhere; }
.stock-boxes { flex-basis: 100%; text-align: right; }
.stock-group + .stock-group { margin-top: 14px; }
.stock-group .panel-title { margin: 0 0 8px; }
/* Odoo's name and the Internal Reference are secondary to ours: on one line
   under it on a phone, trailing it on a wide screen. */
.stock-odoo-name, .stock-code { display: inline-block; margin-left: 8px; }
.stock-code { font-variant-numeric: tabular-nums; }
.stock-panel form.htmx-request { opacity: .55; pointer-events: none; }

@media (max-width: 680px) {
  .stock-search { flex: 1 1 100%; max-width: none; }
  .stock-table th, .stock-table td { padding-left: 10px; padding-right: 10px; }
  .stock-table th:last-child, .stock-table td:last-child { width: 38%; }
  .stock-scope-option { padding: 7px 10px; }
  .stock-odoo-name, .stock-code { display: block; margin-left: 0; }
}

/* ---------------------------------------------------------------- mapping
   The product-mapping panel. Built on the warehouse table's classes wherever
   the two agree — same chips, same search controls, same card — so the two
   pages read as one tool. Only what is genuinely new lives here. */
.mapping-controls { margin-bottom: 12px; }
/* Three columns, and the middle one holds Odoo's names, which run long. */
.mapping-table { table-layout: fixed; }
.mapping-table th:last-child, .mapping-table td:last-child { width: 200px; }
.mapping-table td { overflow-wrap: anywhere; }
.mapping-odoo-name { font-weight: 600; }
.mapping-history { display: block; }

/* The editor sits in a row of its own under the product it edits, with no
   top border, so the pair reads as one block rather than two rows. */
.mapping-editor-row > td { border-top: 0; padding-top: 0; }
.mapping-editor > summary { cursor: pointer; padding: 2px 0; }
.mapping-editor[open] > summary { margin-bottom: 8px; }
.mapping-form { padding: 4px 0 10px; }
.mapping-search { display: flex; gap: 8px; align-items: center; }
.mapping-search .input { flex: 1 1 auto; min-width: 0; }
.mapping-results:empty { display: none; }
.mapping-results { margin-top: 8px; }

/* Candidates are a table inside a table cell: no card, lighter rules, and the
   signals wrap rather than push the button off the edge. */
.mapping-candidates { background: transparent; }
.mapping-candidates td { padding: 6px 8px; vertical-align: top; }
.mapping-candidates tr + tr td { border-top: 1px solid var(--line); }
.mapping-signals { display: flex; flex-wrap: wrap; gap: 4px 6px;
  align-items: baseline; }

.notice-ok { border-left-color: var(--green, #2f7a4f); }
.notice-bad { background: var(--tint-red); border: 1px solid var(--tint-red-line);
  border-left: 4px solid var(--darkred); color: var(--darkred); }

@media (max-width: 680px) {
  /* Three columns do not fit a phone: at 375px the header labels collide and
     the product name wraps to one letter a line. So the row stacks — our
     name, what it points at, then the state — which is the order somebody
     reads it in anyway. The header goes with them, since a stacked row has no
     columns for it to label. */
  .mapping-table thead { display: none; }
  /* Undo the desktop 200px last column, or the state cell stays 200px wide
     while its stacked siblings are full width — and the row highlight ends
     mid-row. */
  .mapping-table th:last-child, .mapping-table td:last-child { width: auto; }
  .mapping-table, .mapping-table tbody, .mapping-table tr,
  .mapping-table td { display: block; width: auto; }
  .mapping-table tr { padding: 10px 0; }
  /* .data-table tbody td wins on specificity, so match it: stacked cells with
     their own rules read as four separate rows, which is the opposite of the
     point. The row keeps one rule to separate it from the next product. */
  .mapping-table tbody td { border-top: 0; padding: 2px 12px; }
  .mapping-table tbody tr.mapping-editor-row + tr { border-top: 1px solid var(--line); }
  /* Right-aligning a full-width block just strands the chips off on their
     own; stacked, they read as part of the row. */
  .mapping-table .stock-shelf { justify-content: flex-start; margin-top: 4px; }
  .mapping-table .num { text-align: left; }
  /* One column on a phone: the signals are the reason to look, so they get
     the width, and the button goes underneath rather than beside. */
  .mapping-candidates, .mapping-candidates tbody, .mapping-candidates tr,
  .mapping-candidates td { display: block; width: auto; }
  .mapping-candidates td { padding: 4px 0; border-top: 0; }
  .mapping-candidates tr { padding: 8px 0; }
  .mapping-candidates tr + tr { border-top: 1px solid var(--line); }
  .mapping-search { flex-wrap: wrap; }
}

/* ── Correcting a filed report ───────────────────────────────────────── */
/* Folded away by default: most reports are right, and the panel is only
   opened when a gym has told us otherwise. Inside, one row per counted
   product — the filed figure on the left, the replacement on the right, so
   what is being changed and what it is being changed to never have to be
   held in the head at once. */
.correct-summary { cursor: pointer; font-weight: 700; }
.correct-provenance { margin-top: 12px; }
.correct-provenance .muted { display: block; margin-top: 4px; }

.correct-rows { margin: 14px 0 4px; }
/* The filed figure hugs the box that would replace it — two equal fluid
   columns put 400px of nothing between the number and its correction. */
.correct-row { display: grid; align-items: center; gap: 8px;
  grid-template-columns: minmax(0, 1fr) minmax(0, auto) 92px 84px;
  padding: 7px 0; }
.correct-row + .correct-row { border-top: 1px solid var(--line); }
/* A grid item's min-width is auto and an <input>'s intrinsic width is ~177px,
   so without this the amount box refuses to shrink and pushes the row past
   the card. Same trap as the order line. */
.correct-row > * { min-width: 0; }
.correct-product { font-weight: 600; }
.correct-filed { text-align: right; }
.correct-amount, .correct-unit { padding: 7px 9px; }
.correct-unit-fixed { text-align: center; }

.correct-tools { display: flex; align-items: center; gap: 10px;
  flex-wrap: wrap; margin: 12px 0; }
.correct-tools .muted { flex: 1 1 260px; }
.correct-reason { width: 100%; box-sizing: border-box; }

/* On a phone the four columns become two rows: what the report says, then
   what it should say. The filed figure keeps its own line rather than being
   squeezed beside the product name — it is the number being disputed. */
@media (max-width: 680px) {
  .correct-row { grid-template-columns: 1fr 84px; gap: 6px; padding: 10px 0; }
  .correct-product { grid-column: 1 / -1; }
  .correct-filed { grid-column: 1 / -1; text-align: left; }
}

/* A card whose title shares its line with an action. The action is the point
   of the ✎ one: staff notice a figure is wrong while looking at the table, so
   the way to fix it belongs beside the table's own heading — not folded away
   below it among the other <details>, which is where it spent its first day. */
.card-head { display: flex; align-items: center; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; }
.card-head h3 { margin: 0; }

/* ── the gym's previous orders ───────────────────────────────────────────────
   Context for the order being built below it, so it reads as a receipt rather
   than a form: visits side by side where there is room, each one a date, a
   name, and what went out. Nothing here is editable. */
.prev-orders { display: grid; gap: 16px 24px;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }
.prev-order { border-top: 2px solid var(--line); padding-top: 10px; min-width: 0; }
.prev-order-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  margin: 0 0 6px; }
.prev-order-lines { list-style: none; margin: 0 0 6px; padding: 0; }
.prev-order-lines li { padding: 2px 0; overflow-wrap: anywhere; }
.prev-order-qty { font-weight: 800; font-variant-numeric: tabular-nums;
  margin-right: 4px; white-space: nowrap; }
.prev-order-none, .prev-order-note, .prev-order-links { margin: 0 0 4px; }
.prev-order-links a, .prev-order-lines .muted { white-space: nowrap; }

/* ── the staff home: a launcher ──────────────────────────────────────────── */
.launch { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 16px; align-items: stretch; }
.launch-card { display: flex; flex-direction: column; gap: 6px; min-height: 180px;
  background: var(--paper); border: 1px solid var(--line); border-radius: 18px;
  padding: 22px; color: var(--ink); text-decoration: none;
  transition: transform .15s ease, box-shadow .2s ease; }
a.launch-card:hover { transform: translateY(-2px); box-shadow: 0 12px 30px rgba(0,0,0,.08); }
.launch-icon { font-size: 1.6rem; line-height: 1; }
.launch-name { font-family: var(--disp); font-stretch: var(--disp-stretch);
  font-style: italic; font-weight: 800; font-size: 1.25rem; color: var(--darkred); }
.launch-line { color: var(--muted); font-size: .95rem; line-height: 1.4; }
.launch-links { margin-top: auto; padding-top: 12px; display: flex; flex-wrap: wrap;
  gap: 6px 14px; font-weight: 700; font-size: .9rem; }
.launch-links > span { white-space: nowrap; }
.launch-links > span:first-child { color: var(--orange); }
.launch-links > span[data-href]:hover { text-decoration: underline; }
.launch-form { gap: 8px; }
.launch-form .input { flex: 1 1 140px; min-width: 0; }
.launch-form .btn-sm { padding: 8px 18px; white-space: nowrap; }

/* ------------------------------------------------------------- contacts
   Gym delivery addresses in Odoo. Same controls, chips and card as /mapping —
   the two are one tool with two questions — so only the row itself is new. */
.contacts-table td { overflow-wrap: anywhere; }
.contacts-table .num:last-child { width: 92px; }

/* An archived address is still readable — somebody is looking at it to decide
   whether to put it back — but it must never be mistaken for a live one. */
.contacts-table tr.is-archived td { opacity: .62; }
.contacts-table tr.is-editing > td { background: var(--cream); }

.contact-form { padding: 6px 0 4px; }
.contact-form-head { grid-column: 1 / -1; margin: 0 0 2px; }
.contact-form-note { grid-column: 1 / -1; margin: 2px 0 0; }

@media (max-width: 680px) {
  /* Six columns do not fit a phone. Same treatment as .mapping-table: the row
     stacks in reading order and the header goes with it, since a stacked row
     has no columns left for it to label. */
  .contacts-table thead { display: none; }
  .contacts-table .num:last-child { width: auto; }
  .contacts-table, .contacts-table tbody, .contacts-table tr,
  .contacts-table td { display: block; width: auto; }
  .contacts-table tr { padding: 10px 0; }
  .contacts-table tbody td { border-top: 0; padding: 2px 12px; }
  .contacts-table .num { text-align: left; }
}

/* ------------------------------------------------------------ deliveries
   Our orders in Odoo and where their parcels are. Same controls, chips and
   card as /mapping and /contacts; only the parcel line is new. */
.deliveries-table td { overflow-wrap: anywhere; vertical-align: top; }
.deliveries-table .num:last-child { white-space: nowrap; }

/* A cancelled order is history, not work: readable, and unmistakably not a
   row anybody needs to act on. */
.deliveries-table tr.is-cancelled td { opacity: .55; }

/* One parcel per line, its chips and its tracking number wrapping together
   rather than pushing the column wide. An order can have three. */
.parcel { display: flex; flex-wrap: wrap; gap: 4px 8px; align-items: baseline;
  padding: 3px 0; }
.parcel + .parcel { border-top: 1px dashed var(--line); }

@media (max-width: 680px) {
  /* Six columns do not fit a phone. Same treatment as .mapping-table. */
  .deliveries-table thead { display: none; }
  .deliveries-table, .deliveries-table tbody, .deliveries-table tr,
  .deliveries-table td { display: block; width: auto; }
  .deliveries-table tr { padding: 10px 0; }
  .deliveries-table tbody td { border-top: 0; padding: 2px 12px; }
  .deliveries-table .num { text-align: left; }
}
