/* ============================================================
   Nexus stylesheet
   Every colour comes from a token. Light is the default palette;
   dark is the same tokens re-pointed, so themes never need
   per-component overrides.
   ============================================================ */
:root{
  color-scheme: light dark;

  /* Light palette */
  --bg:#f4f7fb;
  --bg-accent:#e7eefb;
  --panel:#ffffff;
  --panel-raised:#fbfcfe;
  --line:#d9e1ee;
  --text:#101726;
  --muted:#5a6880;
  --accent:#009475;          /* Gema brand green */
  --accent-deep:#0e4828;     /* Gema dark green */
  --brand-red:#ee2e23;       /* Gema logo red - identity only, never an action */
  --brand-rose:#e55e75;      /* Gema accent */
  --accent-contrast:#ffffff;
  --field:#ffffff;
  --row:#fbfcfe;
  --row-hover:#f1f5fb;
  --backdrop:#0f172a66;
  --shadow:0 10px 30px #0f172a14;

  --danger:#c0322f; --danger-line:#e3a9a7; --danger-bg:#fdf2f2;
  --warn:#8a5a06;   --warn-line:#e4c489;  --warn-bg:#fdf6e7;
  --ok:#009475;     --ok-line:#9fd9c7;
  --info:#1f5fa8;   --info-line:#a8c6e8;
  --neutral-line:#c6d0e0;

  --gap:14px; --gap-lg:24px; --radius:12px; --radius-lg:18px; --motion:160ms ease;
}

/* Dark palette: same tokens, different values. */
@media (prefers-color-scheme: dark){
  :root:not([data-theme="light"]){
    --bg:#0b1020; --bg-accent:#182644; --panel:#141b2d; --panel-raised:#172039;
    --line:#26304a; --text:#f4f7ff; --muted:#9ca8c3;
    --accent:#4fd1ab; --accent-deep:#9fe7cf; --brand-rose:#f38fa2; --accent-contrast:#04251c;
    --field:#0b1020; --row:#131a2c; --row-hover:#18203a;
    --backdrop:#04070fcc; --shadow:0 18px 50px #0004;
    --danger:#ff8b8b; --danger-line:#7a3030; --danger-bg:#2a1212;
    --warn:#ffc75a;   --warn-line:#765d1b;  --warn-bg:#3b2d0d;
    --ok:#4fd1ab;     --ok-line:#3aa78a;
    --info:#8fc2ff;   --info-line:#3d6ea8;
    --neutral-line:#3a4560;
  }
}
:root[data-theme="dark"]{
  --bg:#0b1020; --bg-accent:#182644; --panel:#141b2d; --panel-raised:#172039;
  --line:#26304a; --text:#f4f7ff; --muted:#9ca8c3;
  --accent:#4fd1ab; --accent-deep:#9fe7cf; --brand-rose:#f38fa2; --accent-contrast:#04251c;
  --field:#0b1020; --row:#131a2c; --row-hover:#18203a;
  --backdrop:#04070fcc; --shadow:0 18px 50px #0004;
  --danger:#ff8b8b; --danger-line:#7a3030; --danger-bg:#2a1212;
  --warn:#ffc75a;   --warn-line:#765d1b;  --warn-bg:#3b2d0d;
  --ok:#4fd1ab;     --ok-line:#3aa78a;
  --info:#8fc2ff;   --info-line:#3d6ea8;
  --neutral-line:#3a4560;
}

*{box-sizing:border-box}
body{margin:0;background:radial-gradient(circle at top right,var(--bg-accent) 0,var(--bg) 42%);
  color:var(--text);font:15px/1.5 Questrial,'Avenir Next',Avenir,'Segoe UI',ui-sans-serif,system-ui,-apple-system,sans-serif;min-height:100vh}

/* ---- Chrome ---- */
.topbar{height:64px;padding:0 max(24px,5vw);display:flex;align-items:center;justify-content:space-between;
  border-bottom:1px solid var(--line);background:color-mix(in srgb,var(--bg) 80%,transparent);
  backdrop-filter:blur(18px);position:sticky;top:0;z-index:5;gap:16px}
.brand{font-weight:800;letter-spacing:.04em}
.topbar a{color:var(--text);text-decoration:none}
.topbar nav{display:flex;gap:18px;align-items:center}
.topbar nav a{display:inline-flex;align-items:center;gap:7px}
.dev-banner{padding:8px 24px;display:flex;align-items:center;justify-content:center;gap:8px;
  background:var(--warn-bg);color:var(--warn);border-bottom:1px solid var(--warn-line);font-size:12px}
.shell{max-width:1180px;margin:auto;padding:48px 24px}

/* ---- Headings ---- */
.hero{max-width:760px;margin-bottom:40px}
.hero h1,.page-head h1{font-size:clamp(34px,6vw,64px);line-height:1.02;margin:.1em 0}
.eyebrow{color:var(--accent);font-size:12px;font-weight:800;letter-spacing:.16em;margin:0}
.page-head{display:flex;justify-content:space-between;align-items:flex-end;gap:var(--gap-lg);flex-wrap:wrap}
.page-head__actions{display:flex;flex-wrap:wrap;gap:var(--gap);justify-content:flex-end}

/* ---- Surfaces ---- */
.grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px}
.card,.panel{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius-lg);
  padding:24px;box-shadow:var(--shadow)}
.card span,.muted,small{color:var(--muted)}
.card strong{display:block;font-size:40px;margin-top:8px}
.panel{margin-top:24px;display:flex;justify-content:space-between;gap:20px;align-items:center}
.panel--block{display:block}
/* Same specificity as the UA stylesheet's [hidden]{display:none}, and this
   rule is declared later, so without it `.panel--block[hidden]` stayed
   visible — the Notifications card set `hidden` for an unsupported browser
   ("hide, don't grey out") and the browser drew it anyway. */
.panel--block[hidden]{display:none}
.columns{display:grid;grid-template-columns:360px 1fr;gap:24px}
.columns .panel{display:block;margin:0}
/* Two columns, sized by the longest label rather than by a guess. The key had
   `min-width:90px` and "Cargo ready by" is wider than that, so its value sat
   further right than every other one. `display:contents` on the row puts both
   spans into the summary's own grid, so the column is shared by every row. */
.summary{display:grid;grid-template-columns:max-content 1fr;gap:6px 16px}
.summary p{display:contents;margin:0}
.summary__key{color:var(--muted)}
.summary__value{min-width:0;overflow-wrap:anywhere}

@media(max-width:420px){
  /* Too narrow for two columns once a value is a date and a note. */
  .summary{grid-template-columns:1fr;gap:2px}
  .summary__value{margin-bottom:8px}
}

/* ---- Forms ---- */
.stack{display:grid;gap:var(--gap)}
label{display:grid;gap:6px;color:var(--muted)}
/* `min-width:0` and `max-width:100%` are load-bearing for date inputs, not
   tidiness. Chromium gives `<input type="date">` an intrinsic minimum width
   derived from the locale's formatted date string, and without `min-width:0`
   that minimum beats `width:100%` — so in the two-column modal grid on a phone
   the date field renders wider than the text inputs beside it. */
input,select,textarea{width:100%;min-width:0;max-width:100%;padding:12px;
  border:1px solid var(--line);border-radius:10px;
  background:var(--field);color:var(--text);font:inherit}
input:disabled,select:disabled,textarea:disabled{opacity:.6}

/* A checkbox is a control, so it is drawn like one. These were left native: a
   13px box whose hairline all but disappeared on white, sitting beside inputs
   with a 10px radius, a 12px pad and a green focus ring — which reads as a
   control somebody forgot. Styled once here rather than in the role picker and
   the column picker separately, because two copies drift.

   `color-mix` gives the unchecked border a mid-tone from `--muted` in either
   theme without a fourth token to keep in step; the plain `--muted` before it
   is the fallback where `color-mix` is not understood. */
input[type=checkbox]{
  appearance:none;-webkit-appearance:none;
  width:18px;height:18px;flex:0 0 18px;padding:0;margin:0;
  border:1.5px solid var(--muted);
  border-color:color-mix(in srgb,var(--muted) 62%,transparent);
  border-radius:5px;background:var(--field);
  display:inline-grid;place-content:center;cursor:pointer;
  transition:background-color .12s ease,border-color .12s ease}
input[type=checkbox]::after{
  content:"";width:5px;height:9px;margin-top:-2px;
  border:solid var(--accent-contrast);border-width:0 2px 2px 0;
  transform:rotate(45deg) scale(0);transition:transform .12s ease}
input[type=checkbox]:hover:not(:disabled){border-color:var(--muted)}
input[type=checkbox]:checked{background:var(--accent);border-color:var(--accent)}
input[type=checkbox]:checked::after{transform:rotate(45deg) scale(1)}
input[type=checkbox]:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
input[type=checkbox]:disabled{cursor:not-allowed}

/* A picklist too long to ship arrives as a search box above its select. The
   spinner shares the search box's grid cell rather than taking a row of its
   own: a spinner that appears as its own row pushes the select down on every
   keystroke, which is exactly the layout jump the rest of this file avoids. */
label:has(> .picklist-search){position:relative}
.picklist-search{padding-right:34px}
.picklist-search + .spinner{grid-row:2;grid-column:1;justify-self:end;
  align-self:center;margin-right:12px;color:var(--muted)}
textarea{min-height:96px;resize:vertical}

/* iOS Safari gives a date input a native intrinsic min-width of its own, which
   ignores `min-width:0` and overrides `width:100%`. Stripping the native
   appearance makes WebKit lay it out as a plain text input and respect the
   width. WebKit also keeps centring the text afterwards — `appearance:none`
   resets the widget, not the alignment — which puts a date in the middle of its
   box beside left-aligned selects.

   Chromium's iOS device emulation is still Blink, so none of this reproduces
   anywhere except a real device. Ported from ClassPulse, which found all three
   of these the hard way across three separate reports. */
input[type=date],input[type=time],input[type=datetime-local],
input[type=month],input[type=week]{-webkit-appearance:none;appearance:none;text-align:left}

/* WebKit does not render `::-webkit-calendar-picker-indicator` at all, so after
   `appearance:none` an empty date field on iOS is a blank box with nothing to
   say it can be tapped — which is what was reported from an iPhone on 18
   September. `-webkit-touch-callout` is implemented only by WebKit on iOS, so
   this query targets exactly the browsers missing the indicator (iOS Chrome and
   Firefox included, since both are WebKit there) without drawing a second icon
   beside Chromium's native one.

   The glyph is a fixed slate that holds contrast in both themes: a data: URI
   has no access to custom properties, so it cannot follow the theme. */
@supports (-webkit-touch-callout:none){
  input[type=date],input[type=time],input[type=datetime-local],
  input[type=month],input[type=week]{
    /* Flex rather than a line-height. `min-height:44px` means the box can grow
       — a longer localised date, a larger accessibility text size — and a
       line-height tuned to 44px mis-centres the moment it does. */
    display:flex;align-items:center;
    padding-right:34px;
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='4' width='18' height='18' rx='2'/%3E%3Cpath d='M16 2v4M8 2v4M3 10h18'/%3E%3C/svg%3E");
    background-repeat:no-repeat;background-position:right 12px center;background-size:16px 16px}
}
.field-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--gap)}
.hint{color:var(--muted);font-size:13px;margin:0}
/* A status line with nothing to say yet takes no room. */
.hint:empty{display:none}
/* The push card: button, status and note spaced as one column, and the
   device list below it only once there is one. */
.push-device{display:grid;gap:10px;justify-items:start;margin-bottom:var(--gap)}
.push-device:last-child{margin-bottom:0}
.push-device [data-push-body]{display:grid;gap:10px;justify-items:start}
.empty-state{padding:18px;border:1px dashed var(--line);border-radius:var(--radius);margin:0}

/* ---- Buttons ---- */
.button{display:inline-flex;align-items:center;gap:8px;border:0;border-radius:10px;padding:11px 16px;
  background:var(--accent);color:var(--accent-contrast);font-weight:800;text-decoration:none;cursor:pointer;
  transition:filter var(--motion)}
.button:hover:not(:disabled){filter:brightness(1.08)}
.button--danger{background:var(--danger);color:var(--accent-contrast)}
/* `text-decoration:none` is not tidiness. A ghost button is a <button> in most
   places and an <a> in others — Clear, Export, Previous, Next — and without
   this the anchors render underlined beside identical-looking buttons that do
   not. The difference is invisible in the markup and invisible in the class
   name, which is why it went unnoticed until paging added two more. */
.ghost-button{display:inline-flex;align-items:center;gap:8px;background:transparent;color:var(--text);
  border:1px solid var(--line);padding:8px 12px;border-radius:8px;cursor:pointer;font:inherit;
  text-decoration:none;transition:border-color var(--motion)}
.ghost-button:hover:not(:disabled){border-color:var(--accent)}
.ghost-button--danger:hover:not(:disabled){border-color:var(--danger-line);color:var(--danger)}
.icon-button{background:transparent;border:0;color:var(--muted);font-size:26px;line-height:1;cursor:pointer;padding:0 4px}
/* Matched at the same weight, as above, rather than `!important`: `display`
   here ties the UA stylesheet's `[hidden]{display:none}` on specificity, and
   this rule is declared later, so a `.button`/`.ghost-button` given `hidden`
   by script (the Notifications card swaps "Turn on" for "Send a test" /
   "Turn off" this way) stayed visible regardless. */
.button[hidden],.ghost-button[hidden]{display:none}
button:disabled{opacity:.6;cursor:progress}
:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.stack > .button,.stack > .ghost-button{justify-self:start}

/* ---- Rows ---- */
.entity-row{display:flex;justify-content:space-between;align-items:center;gap:var(--gap);
  padding:16px;border:1px solid var(--line);border-radius:var(--radius);background:var(--row)}
.entity-row__main{min-width:0}
.entity-row__main small{display:block;color:var(--muted);margin-top:4px}
.entity-row__aside{display:flex;align-items:center;gap:10px;flex-shrink:0;flex-wrap:wrap;justify-content:flex-end}
/* Matched at the same weight (see the buttons' `[hidden]` rule above): the
   Notifications card's on/off controls share this class and are toggled with
   `hidden`, which otherwise tied with this rule's own `display` and lost. */
.entity-row__aside[hidden]{display:none}
/* Wrapping, because a shipment carries five actions and a phone is 360px wide.
   The row already stacks at this width; the actions were still one unwrappable
   line inside it, which is what pushed the card past the screen. */
.entity-row__actions{display:flex;gap:6px;flex-wrap:wrap}
.entity-row--link{text-decoration:none;color:var(--text);transition:border-color var(--motion)}
.entity-row--link:hover{border-color:var(--accent)}

/* ---- Status pills: colour carries the meaning ---- */
.pill{border:1px solid var(--ok-line);color:var(--ok);border-radius:999px;padding:4px 9px;font-size:12px;white-space:nowrap}
.pill--draft,.pill--booking_requested,.pill--closed{border-color:var(--neutral-line);color:var(--muted)}
.pill--booking_confirmed,.pill--arrived{border-color:var(--ok-line);color:var(--ok)}
.pill--in_transit,.pill--factory_assigned{border-color:var(--info-line);color:var(--info)}
.pill--cancelled{border-color:var(--danger-line);color:var(--danger)}
.pill--held,.pill--warn{border-color:var(--warn-line);color:var(--warn)}
.pill--warn{margin-left:6px}
/* Nothing has happened yet, which is a real answer and not a warning. Greyed
   rather than coloured, so a scan down the column finds the coloured ones. */
.pill--muted{border-color:var(--line);color:var(--muted)}

/* What a freshly raised order still needs, on its row in the list. Warning
   colour rather than muted: it is not a detail, it is the reason the order
   cannot go to a factory yet. */
.row-needs{display:block;color:var(--warn);margin-top:3px}
.row-needs__go{text-decoration:underline;font-weight:600}
.panel--next{border-color:var(--warn-line)}
.panel--next h2{color:var(--warn)}

/* ---- Column picker ---- */
/* Two columns of checkboxes so twenty of them fit without a scroll, collapsing
   to one on a phone. The control is the label, so the whole row is a tap
   target rather than the 13px box. */
.column-picker{border:1px solid var(--line);border-radius:10px;padding:14px 16px;margin:0}
.column-picker legend{padding:0 6px;color:var(--muted);font-size:12px;
  letter-spacing:.08em;text-transform:uppercase}
.column-picker__option{display:flex;align-items:center;gap:10px;min-height:32px;
  color:var(--text);cursor:pointer}
.column-picker__option input{min-width:0;margin:0}
.column-picker{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:2px 18px}
.column-picker legend{grid-column:1/-1}
@media(max-width:480px){.column-picker{grid-template-columns:1fr}}

/* ---- Single boolean toggle ---- */
/* The same hidden+checkbox pattern as `.column-picker__option`, for one
   checkbox on its own rather than a `<fieldset>` of several — a form's "is
   this an assortment pack" switch, not a picklist. */
.checkbox-field{display:flex;align-items:center;gap:10px;cursor:pointer}
.checkbox-field input{min-width:0;margin:0}

/* ---- Section headers ---- */
.section-head{display:flex;justify-content:space-between;align-items:flex-start;gap:var(--gap);
  margin-bottom:var(--gap-lg);flex-wrap:wrap}
.section-head h2{margin:0}
.section-head p{margin:4px 0 0}
/* Stacked actions fill their row.
   Wrapping stopped the overflow and left them ragged: two, two, one, each only
   as wide as its own label. On a phone they share the row in equal columns, and
   on the narrowest screens each takes the full width — unambiguous, and the
   easiest thing to hit with a thumb. */
@media(max-width:760px){
  .entity-row__actions,.section-head__actions{display:grid;width:100%;
    grid-template-columns:repeat(auto-fit,minmax(8rem,1fr))}
  .entity-row__actions > *,.section-head__actions > *{width:100%;justify-content:center}
  .entity-row__aside{width:100%}
}

@media(max-width:480px){
  .entity-row__actions,.section-head__actions{grid-template-columns:1fr}
}

/* Wrapping, for the same reason `.entity-row__actions` does: three buttons at
   the 44px tap size do not fit beside a heading on a 360px screen, and a row
   that refuses to shrink pushes them off the card instead. Fixing one and not
   the other is how this came back on a different card. */
.section-head__actions{display:flex;gap:8px;flex-shrink:0;flex-wrap:wrap}

/* ---- Timeline ---- */
.timeline{display:grid;gap:16px;list-style:none;margin:0;padding:0}
.timeline small{display:block;color:var(--muted)}
/* Icon and text on one row, the icon in a fixed column so every entry's text
   starts at the same place — a ragged left edge is what makes a list hard to
   skim, which is the whole reason the icons are here. */
.timeline li{display:grid;grid-template-columns:28px 1fr;align-items:start;gap:10px}
.timeline li.muted{display:block}
.timeline__icon{display:grid;place-items:center;width:28px;height:28px;border-radius:50%;
  border:1px solid var(--line);color:var(--muted);flex:0 0 auto}

/* ---- Instant feedback ---- */
.spinner{display:none;width:14px;height:14px;border:2px solid currentColor;border-right-color:transparent;
  border-radius:50%;animation:spin .6s linear infinite}
.htmx-request .spinner,.htmx-request.spinner{display:inline-block}
@keyframes spin{to{transform:rotate(360deg)}}
@media (prefers-reduced-motion:reduce){.spinner{animation-duration:2s}.button,.ghost-button{transition:none}}

/* The click landed. A page navigation fires no htmx event, so without this
   the pager and the filter bar are the only controls in the app that do
   nothing visible until the next page paints. It creeps rather than
   completes: the bar reaching the end would promise an arrival this has no
   way of knowing about. */
html[data-navigating]::before {
  content: ""; position: fixed; inset: 0 0 auto 0; height: 3px; z-index: 60;
  background: var(--accent); transform-origin: 0 50%;
  animation: nav-progress 10s cubic-bezier(.15,.85,.25,1) forwards;
}
@keyframes nav-progress { from { transform: scaleX(0) } to { transform: scaleX(.95) } }
@media (prefers-reduced-motion:reduce) {
  html[data-navigating]::before { animation: none; transform: scaleX(1); opacity: .5 }
}

/* ---- Modal ---- */
dialog{border:1px solid var(--line);border-radius:var(--radius-lg);background:var(--panel);color:var(--text);
  padding:0;width:min(680px,calc(100vw - 32px));max-height:calc(100vh - 64px);box-shadow:var(--shadow)}
dialog::backdrop{background:var(--backdrop);backdrop-filter:blur(3px)}
.modal-body{padding:24px;display:grid;gap:var(--gap-lg)}
.modal-head{display:flex;justify-content:space-between;align-items:flex-start;gap:var(--gap)}
.modal-head h2{margin:0 0 6px}
.modal-foot{display:flex;justify-content:flex-end;gap:10px}
.modal-section{border-top:1px solid var(--line);padding-top:var(--gap-lg)}
.modal-section h3{margin:0 0 var(--gap)}
.action-row{display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.inline-form{display:flex;gap:8px;align-items:center}
.inline-form input{width:auto}
.modal-error:empty{display:none}
.error-text{margin:0;padding:12px;border:1px solid var(--danger-line);border-radius:var(--radius);
  background:var(--danger-bg);color:var(--danger)}

/* ---- POE list ---- */
.filter-bar{display:flex;gap:var(--gap);align-items:flex-end;flex-wrap:wrap;margin-bottom:var(--gap-lg)}
.filter-bar label{flex:1 1 180px;min-width:0}
.filter-bar__actions{display:flex;flex-wrap:wrap;gap:8px}
.filter-bar__actions > *{white-space:nowrap}
/* A table wider than its box scrolls sideways, and says so. macOS hides
   overlay scrollbars until something moves, so a fourteen-column table looked
   like a table with four columns missing — the bar is forced visible and given
   contrast rather than left to the platform's default. */
.table-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;
  scrollbar-width:thin;scrollbar-color:var(--muted) transparent;
  padding-bottom:4px}
.table-scroll::-webkit-scrollbar{height:10px;-webkit-appearance:none}
.table-scroll::-webkit-scrollbar-track{background:var(--row);border-radius:99px}
.table-scroll::-webkit-scrollbar-thumb{background:var(--muted);border-radius:99px;
  border:2px solid var(--panel)}
.table-scroll::-webkit-scrollbar-thumb:hover{background:var(--text)}
/* The twin above (app.js): an empty strip as wide as the table, so it has a
   scrollbar of its own and nothing else. */
.table-scroll--mirror{padding-bottom:0;margin-bottom:4px}
.table-scroll--mirror[hidden]{display:none}
.table-scroll__strip{height:1px}
.data-table{width:100%;border-collapse:collapse;font-size:14px}
.data-table th,.data-table td{text-align:left;padding:10px 10px;border-bottom:1px solid var(--line);vertical-align:top}
.data-table thead th{position:sticky;top:0;background:var(--panel);color:var(--muted);
  font-size:12px;letter-spacing:.08em;text-transform:uppercase;white-space:nowrap}
.data-table tbody tr:hover{background:var(--row-hover)}
.data-table a{color:var(--accent);text-decoration:none;font-weight:700}
.data-table a:hover{text-decoration:underline}
.data-table .nowrap{white-space:nowrap}
/* A free-text note in a table of dates and references. Capped so one long note
   cannot push the columns after it off the screen, with the full text on the
   cell's title; the date sits under it rather than beside it, because side by
   side is what makes the column wide. */
.data-table .cell-note{max-width:26ch;min-width:14ch}
.data-table .cell-note small{display:block;white-space:nowrap}

@media(max-width:760px){
  .grid,.columns{grid-template-columns:1fr}
  .panel{align-items:flex-start;flex-direction:column}
  .shell{padding-top:32px}
  .field-grid{grid-template-columns:1fr}
  .entity-row{flex-direction:column;align-items:flex-start}
  .entity-row__aside{justify-content:flex-start}
  .modal-foot{flex-direction:column-reverse}
  .modal-foot .button,.modal-foot .ghost-button{width:100%;justify-content:center}
  .filter-bar label{flex:1 1 100%}
  .filter-bar__actions{width:100%}
  /* One to a row on a phone, full width. Four in one row cut the last off at
     the edge and squeezed "View as table" onto three lines; Ari, 24 September:
     "if the buttons can't go next to each other, they should be full width". */
  .filter-bar__actions{flex-direction:column}
  .filter-bar__actions > *{width:100%;justify-content:center}
  .push-device,.push-device [data-push-body]{justify-items:stretch}
  .push-device .button{justify-content:center}
  .topbar nav{gap:12px}
}

/* ---- Brand ---- */
/* Stated here rather than relying on `.topbar a`, which is the only reason it
   has not been underlined so far: the brand stops being a link the moment
   somebody puts it anywhere else. */
.brand{display:inline-flex;align-items:center;gap:8px;font-weight:800;letter-spacing:.02em;
  text-decoration:none}
.brand__logo{width:26px;height:26px;border-radius:6px;background:#fff;padding:2px;
  object-fit:contain;flex:none;box-shadow:0 0 0 1px var(--line)}
.brand__sub{color:var(--muted);font-weight:600;font-size:12px;letter-spacing:.14em;text-transform:uppercase}
.icon{flex:none;vertical-align:-2px}
.button .icon,.ghost-button .icon{margin-right:-1px}
[data-theme-toggle]{display:inline-flex;align-items:center;gap:7px}
[data-theme-icon]{display:inline-flex}
@media(max-width:560px){.brand__sub{display:none}}

/* ---- Dashboard ---- */
.grid--tiles{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}
/* A group of roll-ups under its own heading. The heading is deliberately
   quieter than a panel's -- it separates four short rows of numbers, and at
   section weight it competes with the work below it. */
.tile-group + .tile-group{margin-top:var(--gap-lg)}
.tile-group__heading{font-size:13px;font-weight:700;letter-spacing:.08em;
  text-transform:uppercase;color:var(--muted);margin:0 0 10px}
/* `auto-fill`, not `auto-fit`: the empty tracks are kept, so a group holding
   one number renders one tile-sized card instead of stretching it across the
   page. Grouping turned a single row of four into four rows of one to three,
   and stretched, "Order book" read as a banner rather than a tile. */
.tile-group .grid--tiles{grid-template-columns:repeat(auto-fill,minmax(220px,1fr))}
.card--tile{display:block;text-decoration:none;color:var(--text);transition:border-color var(--motion)}
.card--tile:hover{border-color:var(--accent)}
/* Shrinks rather than spills. A tile is 200px at its narrowest and a figure
   is whatever the business is worth, so the size is fluid and the text is
   allowed to break — a number that overflows its card is worse than a small
   one. `compact()` keeps the string short; this makes it impossible. */
.card--tile strong{font-size:clamp(22px,4.4vw,34px);line-height:1.1;
  display:block;min-width:0;overflow-wrap:anywhere}
.card--tile small{display:block;margin-top:4px}
.role-preview{display:flex;align-items:flex-end;gap:8px}
.role-preview label{min-width:200px}
.section-head h2{display:flex;align-items:center;gap:8px}

/* ---- Touch targets ---- */
.button,.ghost-button,.topbar nav a,select,input[type=date],[data-theme-toggle]{min-height:44px}
.ghost-button,.button{padding-block:10px}
.icon-button{min-width:44px;min-height:44px;display:inline-flex;align-items:center;justify-content:center}

/* ---- Tablet ---- */
@media(max-width:1024px){
  .columns{grid-template-columns:1fr}
  .shell{padding:32px 20px}
  .hero h1,.page-head h1{font-size:clamp(30px,5vw,48px)}
  .grid--tiles{grid-template-columns:repeat(auto-fit,minmax(170px,1fr))}
}

/* ---- Narrow: navigation collapses behind the toggle ----
   Below 1100px, not 760. Between the two the bar tried to fit every item on
   one row and wrapped onto two (Mayer, 25 Sep, in split screen), and the
   Shipping and Create GPO buttons made it wider still. ---- */
.nav-toggle{display:none}
@media(max-width:1100px){
  .topbar{height:auto;padding-block:10px;gap:10px}
  .nav-toggle{display:inline-flex;align-items:center;justify-content:center;min-width:44px}
  /* The menu floats over the page instead of growing the bar and pushing
     everything down — opening it used to shove the heading you were reading off
     the screen, and closing it jumped you back. Out of the topbar's flow, so
     the page beneath does not move at all.

     `100dvh` rather than `100vh`: on a phone the address bar is part of `vh`,
     so a long menu is cut off behind it exactly when it is tallest. */
  .topbar nav{display:none;position:absolute;top:100%;left:0;right:0;
    flex-direction:column;align-items:stretch;gap:2px;
    padding:10px max(24px,5vw) 16px;background:var(--panel);
    border-bottom:1px solid var(--line);box-shadow:var(--shadow);
    max-height:calc(100dvh - 100%);overflow-y:auto;overscroll-behavior:contain}
  .topbar nav[data-open="true"]{display:flex}
  .topbar nav a,.nav-group > summary{padding:12px 4px;border-radius:8px}
  .topbar nav a:hover,.nav-group > summary:hover{background:var(--row-hover)}
  [data-theme-toggle]{justify-content:flex-start;border:0;padding-inline:4px}
  .nav-primary,.nav-utility{flex-direction:column;align-items:stretch;width:100%;gap:2px}
  .nav-utility{border-left:0;padding-left:0;margin-left:0;margin-top:6px;padding-top:8px;
    border-top:1px solid var(--line)}
  .nav-group__menu{margin-left:12px}
}
@media(max-width:760px){
  .role-preview{width:100%}
  .role-preview label{min-width:0;width:100%}
  .page-head{align-items:stretch}
  /* ...but not the pill. `stretch` makes every direct child fill the row, and a
     one-line status pill beside an eyebrow and an h1 was being drawn three
     times its own height. */
  .page-head .pill{align-self:flex-start}
  .dev-banner{text-align:left;align-items:flex-start}
}

/* ---- Navigation buttons ---- */
.topbar nav a{border:1px solid var(--line);border-radius:8px;padding:8px 12px;
  transition:border-color var(--motion),background var(--motion)}
.topbar nav a:hover{border-color:var(--accent)}
.ghost-button.icon-only{padding:8px;min-width:44px;justify-content:center}

/* ---- Nav groups: Orders/POE are flat, occasional setup screens hide behind
   a disclosure, and API/Profile/theme sit apart as the utility cluster. Mobile
   first: everything stacks by default, and only a wide viewport floats the
   group's menu as a dropdown. ---- */
.nav-primary,.nav-utility{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.nav-utility{padding-left:10px;margin-left:2px;border-left:1px solid var(--line)}
.nav-group{position:relative}
.nav-group > summary{list-style:none;cursor:pointer;
  display:inline-flex;align-items:center;gap:7px;min-height:44px;
  border:1px solid var(--line);border-radius:8px;padding:8px 12px;
  transition:border-color var(--motion)}
.nav-group > summary::-webkit-details-marker{display:none}
.nav-group[open] > summary,.nav-group > summary:hover{border-color:var(--accent)}
.nav-group__menu{display:flex;flex-direction:column;gap:2px;margin-top:6px}
@media(min-width:1101px){
  .nav-group__menu{position:absolute;top:calc(100% + 6px);left:0;min-width:190px;
    background:var(--panel);border:1px solid var(--line);border-radius:10px;
    padding:6px;box-shadow:var(--shadow);z-index:20}
}
.visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;border:0}

/* ---- Role preview reads as one line ---- */
.role-preview label{display:flex;align-items:center;gap:10px;min-width:0}
.role-preview label select{width:auto;min-width:190px}
.page-head{margin-bottom:var(--gap-lg)}

/* ---- Account ---- */
.auth-panel{max-width:520px}
.permission-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));
  gap:8px;list-style:none;padding:0;margin:0}
.permission-list code{color:var(--muted);font-size:13px}

@media(max-width:1100px){
  .topbar nav a,.nav-group > summary{border:0;padding:12px 4px}
}
@media(max-width:760px){
  .role-preview label{flex-direction:column;align-items:stretch;gap:6px}
  .role-preview label select{width:100%}
}

/* Role picker — a checkbox per seeded role, wrapping on narrow screens. */
.role-picker {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.75rem;
  margin: 0;
}
.role-picker legend {
  padding: 0 0.35rem;
  font-size: 0.8rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.role-picker__option {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.3rem 0;
}
.role-picker__option input { margin: 0; }

/* Checkbox multiselect — a searchable-in-spirit fixed list, e.g. who to Cc.
   Same shape as .role-picker, but using --line and --radius (the real
   tokens; .role-picker references --border and --radius-sm, neither of
   which this file defines, so its own border/radius never actually apply). */
.checkbox-multiselect {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.75rem;
  margin: 0;
}
.checkbox-multiselect legend {
  padding: 0 0.35rem;
  font-size: 0.8rem;
  color: var(--muted);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.checkbox-multiselect__option {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.3rem 0;
}
.checkbox-multiselect__option input { margin: 0; }

/* Sign-in — a centred card, no navigation, nothing about the server on it. */
.auth {
  min-height: 70vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  padding: 2rem 16px;
}
.auth__card {
  width: 100%;
  max-width: 380px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 2rem 1.75rem;
  text-align: center;
}
.auth__mark { border-radius: 12px; margin-bottom: 0.75rem; }
.auth__card h1 { font-size: 1.35rem; margin: 0; }
.auth__sub { color: var(--muted); margin: 0.25rem 0 1.5rem; font-size: 0.9rem; }
.auth__actions { display: flex; flex-direction: column; gap: 0.6rem; }
.auth__actions .button { width: 100%; justify-content: center; }
.button--quiet {
  background: transparent;
  color: var(--text);
  border: 1px solid var(--border);
}
/* The installed app.
   Signing in with single sign-on leaves this origin, and a standalone window
   that leaves its scope gets the browser's controls back. That is deliberate
   and not a thing to route around: the address bar is how somebody sees whose
   page is asking for their password. So the installed app leads with the
   passkey, which is two POSTs to this origin and never leaves it. In a browser
   tab the redirect costs nothing, so the order there is left alone. */
/* The date a row is sorted by, beside the status pill rather than buried in the
   line of detail — it is the reason the row is where it is on the page. */
/* The few facts that decide whether an attention row is the urgent one. Read
   as one line, so they are separated rather than boxed. */
/* The footer figures of a product list — what a purchase order totals to. */
.summary__total {
  margin: var(--gap) 0 0;
  padding-top: var(--gap);
  border-top: 1px solid var(--line);
  color: var(--muted);
}
.summary__total strong { color: var(--text); }

/* A step that never applied. Present, and visibly not an achievement — colour
   carrying the meaning rather than a pill repeating the words beside it. */
.entity-row--muted { opacity: 0.72; }
.entity-row--muted strong { font-weight: 600; }

/* Page controls. Only rendered when there is another page, so an empty pager
   never occupies space explaining that there is nothing more. */
.pager { display: flex; gap: 8px; margin-top: var(--gap); justify-content: flex-end }
@media(max-width:480px){ .pager{justify-content:stretch} .pager .ghost-button{flex:1;justify-content:center} }

.row-facts { color: var(--muted); }
.row-facts--serious { color: var(--danger); }

.row-when {
  color: var(--muted);
  font-size: 13px;
  white-space: nowrap;
}

.leaves-the-app { display: none; }

@media (display-mode: standalone) {
  .auth__actions [data-passkey-signin] {
    order: -1;
    background: var(--accent);
    color: var(--accent-contrast);
    border-color: transparent;
  }
  .auth__actions .button--primary {
    background: transparent;
    color: var(--text);
    border: 1px solid var(--border);
  }
  .leaves-the-app { display: block; }
}

.auth__dev {
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--border);
  text-align: left;
}
.auth__dev summary {
  cursor: pointer;
  color: var(--muted);
  font-size: 0.85rem;
  margin-bottom: 0.75rem;
}
.auth__foot { font-size: 0.85rem; }

/* A once-only secret, shown in the page because a modal can dismiss itself. */
.reveal {
  border: 1px solid var(--accent, var(--border));
  border-radius: var(--radius);
  padding: 1.25rem;
  margin-bottom: 1rem;
  background: var(--panel);
}
.reveal__head { display: flex; gap: 0.75rem; align-items: flex-start; margin-bottom: 1rem; }
.reveal__head strong { display: block; }
.reveal__head p { margin: 0.2rem 0 0; font-size: 0.88rem; }
.field { margin-bottom: 1rem; }
.field__label {
  font-size: 0.78rem; text-transform: uppercase; letter-spacing: 0.04em;
  color: var(--muted); margin-bottom: 0.35rem;
}
.copyable {
  display: flex; align-items: center; gap: 0.5rem;
  background: var(--surface-soft, rgba(127,127,127,0.08));
  border: 1px solid var(--border); border-radius: var(--radius-sm);
  padding: 0.5rem 0.6rem;
}
.copyable code {
  flex: 1; word-break: break-all; font-size: 0.85rem; background: none; padding: 0;
}
.copyable--block { align-items: flex-start; }
.copyable--block code { white-space: pre; overflow-x: auto; }
.copyable button { flex: none; }
.copyable button[data-copied]::after { content: " ✓"; }
/* Numbered instructions inside a help modal. One definition, both modals. */
.steps { margin: 0; padding-left: 1.3rem; display: grid; gap: 0.9rem; font-size: 0.92rem; }
.steps li { line-height: 1.5; }
.steps .copyable { margin-top: 0.5rem; }
.steps__action { margin-top: 0.55rem; }

.consent-list { text-align: left; margin: 1rem 0; padding-left: 1.1rem; font-size: 0.9rem; }
.consent-list li { margin-bottom: 0.4rem; }

/* A one-time value shown for copying: monospace so a token can be read back
   character by character, and full width because wrapping a URL mid-token is
   how somebody copies half of one. */
.sign-in-link{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:13px}

/* A sortable header and the search box under it. The header is a link, so
   sorting survives a reload, a bookmark and the back button — every state this
   table can be in is a URL.

   The arrow is always there, faint, rather than appearing on hover: a control
   you can only discover by touching it is one most people never find, and a
   column that shifts by a few pixels when the mark appears is the layout jump
   the rest of this file avoids. */
.data-table th a.column-sort{color:inherit;text-decoration:none;display:inline-flex;
  align-items:center;gap:5px;white-space:nowrap;border-radius:6px;padding:2px 4px;
  margin:-2px -4px;transition:background-color .12s ease,color .12s ease}
.data-table th a.column-sort:hover{background:var(--row-hover);color:var(--text)}
.column-sort--on,.data-table th a.column-sort--on:hover{color:var(--accent)}
.column-sort__mark{font-size:11px;line-height:1;opacity:.35}
.column-sort--on .column-sort__mark{opacity:1}
.data-table th a.column-sort:hover .column-sort__mark{opacity:.7}

/* Banded rows, because a fourteen-column table is read across and the eye needs
   a rail. Hover marks the row under the cursor for the same reason. */
.data-table tbody tr:nth-child(even){background:var(--row)}
.data-table tbody tr:hover{background:var(--row-hover)}

/* One line per row. A counterparty called "Walk Customer f87623bd" wrapped over
   three lines, so fourteen columns became a wall forty rows deep and nothing
   could be scanned across — which is most of why this did not read as a grid.
   Cut with an ellipsis and carry the full value in `title`, which is what a
   dense table does; the cells that are already short (a pill, a date) are left
   to size themselves. */
.data-table tbody td{vertical-align:middle;white-space:nowrap;overflow:hidden;
  text-overflow:ellipsis;max-width:200px}
.data-table tbody td.cell-note{max-width:260px}
/* The identity column carries two numbers and is the one everybody reads first,
   so it gets room the others do not and never wraps. */
.data-table tbody td.cell-identity{max-width:none;white-space:nowrap}

/* The back link sits above the page title, small and quiet: it is a way out,
   not an action. The chevron is rotated a quarter turn to point left — one icon
   set, used at the angle the direction needs. */
.back-link{display:inline-flex;align-items:center;gap:4px;color:var(--muted);
  font-size:13px;text-decoration:none;margin-bottom:4px;min-height:32px}
.back-link:hover{color:var(--accent)}
.back-link svg{transform:rotate(90deg)}

/* The search on the home screen: one pill holding the magnifier, the field and
   the button, so it reads as a single control rather than three things that
   happen to be in a row. The border lights on `:focus-within` — the whole bar
   responds, which is what makes it feel like one object. */
.search-hero{display:flex;align-items:center;gap:8px;background:var(--field);
  border:1px solid var(--line);border-radius:999px;padding:5px 5px 5px 18px;
  box-shadow:var(--shadow);margin-bottom:var(--gap-lg)}
.search-hero:focus-within{border-color:var(--accent);
  box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 22%,transparent)}
.search-hero__icon{display:flex;color:var(--muted);flex:0 0 auto}
.search-hero:focus-within .search-hero__icon{color:var(--accent)}
/* Overrides the base input rule: inside the pill the field has no chrome of its
   own, or there is a box drawn inside a box. */
.search-hero input{flex:1 1 auto;min-width:0;border:0;background:transparent;
  padding:12px 0;font-size:16px;border-radius:0}
.search-hero input:focus{outline:none}
.search-hero input::-webkit-search-cancel-button{cursor:pointer}
.search-hero .button{flex:0 0 auto;border-radius:999px;min-height:40px}
@media(max-width:480px){
  /* Below the phone breakpoint the pill would squeeze both into a row too
     narrow for either, so it becomes a stack and keeps its rounded card. */
  .search-hero{flex-wrap:wrap;border-radius:var(--radius-sm,12px);padding:12px}
  .search-hero__icon{display:none}
  .search-hero input{flex-basis:100%;padding:10px 0}
  .search-hero .button{width:100%;justify-content:center}}

/* The other way in, under the pill: the search above finds orders, this finds
   one style across all of them. Written here, at the top level, because the
   first version of this rule was written INSIDE the block above — where CSS
   discards it, and takes the `border` declaration that followed it with it.
   `tests/unit/web/test_the_stylesheet_parses.py` fails if that returns. */
.search-hero__also{margin:0 0 var(--gap-lg)}
@media(max-width:480px){
  /* Matching the Search button above it, which is full width at this size. */
  .search-hero__also .ghost-button{width:100%;justify-content:center}}

/* A ghost button showing the thing it toggles is already on. */
.ghost-button--on{border-color:var(--accent);color:var(--accent)}

/* The dash joining a GPO to a customer PO: quieter than either number, so the
   eye reads two values rather than three things of equal weight. */
.row-joiner{color:var(--muted)}

/* ---- Phone navigation, after the base rules it has to beat ----
   These live at the end on purpose. The phone block higher up resets
   `.nav-utility`'s left border, and then the base `.nav-utility` rule further
   down set it again — same specificity, later in the file, so a vertical rule
   appeared down the middle of the open menu. Source order is the bug and
   source order is the fix; raising specificity instead would hide it. */
@media(max-width:1100px){
  .topbar nav a,.nav-group > summary{justify-content:flex-start;text-align:left;
    border:0;width:100%}
  /* `summary` is `display:list-item`, so `justify-content` did nothing to it
     until it was made a flex box — it stayed centred beside left-aligned
     links. */
  /* `summary` is `display:list-item`, and the base rule makes it `inline-flex`
     — an inline box that shrink-wraps, so it sat centred in its block while
     every link beside it was full width and left-aligned. Both the details
     element and its summary have to fill the row for the text to start where
     the others do. */
  .nav-group{width:100%}
  .nav-group > summary{display:flex;width:100%;align-items:center;gap:7px}
  .topbar nav a:hover{border:0;background:var(--row-hover)}
  .nav-utility{border-left:0;padding-left:0;margin-left:0}
  .nav-group__menu{margin-left:0;border-left:2px solid var(--line);padding-left:12px}
  /* Two classes beat one attribute, so `.ghost-button.icon-only` kept the theme
     toggle centred. Matched at the same weight rather than reached for
     `!important`. */
  .topbar nav .ghost-button.icon-only{width:100%;justify-content:flex-start;
    padding:12px 4px}
}

/* ---- The suggestion dropdown under the search box ---- */
/* Positioned rather than in flow, so the page below does not jump a row taller
   on every keystroke -- the layout-jump rule applies to something that appears
   and disappears as fast as this does. */
.search-hero__wrap{position:relative}
.suggest{position:absolute;z-index:20;left:0;right:0;margin:6px 0 0;padding:6px;
  list-style:none;background:var(--panel);border:1px solid var(--line);
  border-radius:var(--radius-lg);box-shadow:var(--shadow);max-height:320px;overflow-y:auto}
.suggest a{display:flex;justify-content:space-between;align-items:baseline;gap:12px;
  padding:10px 12px;border-radius:var(--radius);text-decoration:none;color:var(--text)}
/* `--row-hover` rather than a new token: this is a row in a list and it should
   highlight like every other one, in both palettes, without inventing a colour
   that only this control knows about. */
.suggest a:hover,.suggest a:focus-visible{background:var(--row-hover)}
.suggest strong{font-size:15px}

/* ---- Live notice ---- */
/* Sits under the top bar rather than floating: it is a standing fact ("these
   people are waiting"), not a transient toast, and it stays until acted on. */
.live-notice{display:flex;align-items:center;gap:10px;padding:10px 16px;
  background:var(--warn-bg);border-bottom:1px solid var(--warn-line);color:var(--warn)}
.live-notice[hidden]{display:none}
.live-notice a{color:inherit;font-weight:700}
/* The same notice inside a page rather than across its top: a card like the
   search box beneath it, not a strip glued to it. */
.live-notice--card{border:1px solid var(--warn-line);border-radius:var(--radius);
  margin-bottom:var(--gap);flex-wrap:wrap}
.live-notice--card a{margin-left:auto;text-decoration:none;padding:4px 12px;
  border:1px solid var(--warn-line);border-radius:999px}
.live-notice--card a:hover{background:var(--warn-line)}

/* An add form folded behind its own button inside a modal — the modal is the
   one dialog there is, so a second one cannot open over it. The summary looks
   like every other ghost button and loses the disclosure triangle. */
.inline-add{display:grid;gap:10px}
.inline-add > summary{list-style:none;width:fit-content;cursor:pointer}
.inline-add > summary::-webkit-details-marker{display:none}
.inline-add[open] > summary{display:none}

/* The line modal's Photo panel: a dropzone that is also a plain file input
   (accessible label + hidden input), so drag-and-drop is an enhancement,
   not the only way in. `--row-hover` stands in for a dedicated "soft
   accent" token, which this file does not define. */
.style-photo-dropzone{display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:var(--gap);min-height:96px;padding:var(--gap-lg);border:1px dashed var(--line);
  border-radius:var(--radius);cursor:pointer;transition:border-color var(--motion),
  background-color var(--motion)}
.style-photo-dropzone:hover,.style-photo-dropzone:focus-within{border-color:var(--accent)}
.style-photo-dropzone.is-dragover{border-color:var(--accent);background-color:var(--row-hover)}
.style-photo-dropzone input[type="file"]{position:absolute;width:1px;height:1px;overflow:hidden;
  clip:rect(0,0,0,0)}
.style-photo-dropzone__preview{max-width:100%;max-height:120px;border-radius:var(--radius)}
