/* Laryze Technical — application stylesheet.
 *
 * One file, hand-written, no build step (see stack.md). Loaded once from
 * base.html, so a change here is a change everywhere.
 *
 * The palette comes from the company logo: brushed silver and black around a
 * single orange dome. That is the whole system — warm neutrals carrying the
 * page, and one hue that means something wherever it appears.
 *
 *   - Orange is the brand, the primary action, and where-you-are.
 *   - Red is destructive and out-of-stock, and appears nowhere else.
 *   - Green is complete, and appears nowhere else.
 *
 * The ground is a warm off-white rather than a neutral grey, tinted a trace
 * of the logo's own hue so the orange sits in it rather than on it. Three
 * tones do the layering: the rail is the deepest, the page sits above it, and
 * cards are plain white on top.
 *
 * Built for a warehouse bench on shared hardware, not a desk, which decides
 * three things that would otherwise go the other way:
 *
 *   1. Body text is 16px. Read standing, at arm's length, in mixed light.
 *   2. Anything tappable is at least 44px. Sometimes gloved.
 *   3. Control edges are darker than a fashionable hairline. The boundary of
 *      a field is the only thing saying where it begins — see --line-control.
 *
 * Two rules hold throughout:
 *
 *   - ONE typeface. Figures get tabular numerals from that same face rather
 *     than a monospace, which read as a second design stitched into the
 *     tables.
 *   - State is never carried by colour alone. Every chip has a word in it.
 */

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

:root {
  /* Warm off-white, not grey. The canvas carries a trace of the logo's hue,
     the rail sits a step deeper so it reads as its own band, and cards come
     up to plain white. */
  --bg:          #faf6ef;   /* canvas — the page   */
  --surface:     #ffffff;   /* paper  — cards      */
  --surface-2:   #f3ede2;   /* rail, table heads   */
  --field:       #ffffff;

  /* Warm-tinted ink. A neutral black on a cream ground looks like a mistake
     — the two disagree about what colour the light is. */
  --text:        #1a1714;
  --text-2:      #3d372f;
  --text-3:      #6b6257;
  --text-4:      #8a8071;

  --line:         #e8e0d3;
  --line-soft:    #f0eae0;
  --line-strong:  #d9cfbe;
  /* The edge of a control you can type in or press. Held apart from --line
     because the two have different jobs and different bars: a divider only
     has to be seen, but a boundary that is the ONLY thing telling you where
     a field or an unfilled button begins has to clear 3:1 against what
     surrounds it. A fashionable hairline is about 1.4:1 and does not, which
     is invisible on a bench under warehouse lighting. */
  --line-control: #857b6c;

  /* The rail keeps its own scale and stays dark whatever the page does. It
     is chrome, not content: a black band down the edge tells you where the
     app's furniture ends and the work begins, which a tonal step of cream
     never quite did. Warm black, not neutral — #000 beside cream reads as a
     hole cut in the page. */
  --side:        #1c1814;
  --side-text:   #e4ddd0;
  --side-muted:  #9a9082;
  --side-line:   #2f2921;
  --side-hover:  #292319;
  /* The page accent is carried down for legibility on cream and is far too
     dark on the rail, so the rail gets the brighter end of the same hue. */
  --side-accent: #f08a3a;

  /* The logo dome, carried down just far enough to clear 3:1 as a UI mark on
     every surface it lands on. The mark straight off the logo, #ef6c1a, is
     2.8:1 and does not — which matters because on a narrow screen this
     colour is the only thing marking the current page. Marks only, never
     text. */
  --brand:        #d95d0e;
  /* The same hue carried further down until white text sits on it above
     4.5:1, and it holds as link text on every surface. Fills and text. Two
     tokens rather than one because a mark wants to be bright and a label
     wants to be legible, and one value cannot be both. */
  --accent:       #b4530b;
  --accent-hover: #93430a;
  --accent-soft:  #fdf0e4;
  --accent-on:    #ffffff;

  /* Status hues, both far from the brand so three warm colours never carry
     three different meanings at shelf distance. "Low" and "Not priced" have
     no hue at all — they are outline badges that read by their word.

     --warn survives only as the left rule on a warning strip, never on a
     chip: a notice appears one at a time with nothing beside it to be
     confused with, where a chip sits in a column next to the orange of an
     action and the red of a problem. */
  --ok:          #136c33;
  --ok-soft:     #edf6ef;
  --danger:      #b8000a;
  --danger-soft: #fdecec;
  --warn:        #9a5b06;
  --warn-soft:   #fdf7ec;

  /* Rectangles. Capsules made every field look like a pill dropped on the
     page; a right angle with the corner just knocked off reads as something
     you type into. Cards get a little more, and nothing gets more than that. */
  --r-card:  12px;
  --r-input: 6px;
  --r-chip:  4px;

  --tap: 44px;
  --side-w: 208px;

  --shadow-card:
    0 1px 2px rgba(60, 46, 26, 0.05),
    0 1px 3px rgba(60, 46, 26, 0.04);

  /* No webfont. A system stack ships zero bytes and paints on the first
     frame with no layout shift, which matters more on warehouse Android
     hardware over mobile data than a licensed face does. IBM Plex Sans is
     named first so a machine that already has it uses it. */
  --f-ui: "IBM Plex Sans", ui-sans-serif, system-ui, -apple-system,
          "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  /* Deliberately the only face in the file. A second one for figures made
     the tables read as two designs stitched together, and the alignment it
     bought is available from this face anyway — see .num. */
}

/* This app is light, full stop.
 *
 * It used to switch on prefers-color-scheme, which meant anyone whose device
 * was set to dark — the default on a lot of Android handsets, and on the
 * machine this was built on — got the dark palette and had no way back,
 * because there is no theme control anywhere in the UI. The light design was
 * simply never reachable for them.
 *
 * The dark tokens below stay as an explicit opt-in so a toggle is a small job
 * later, but nothing sets data-theme today, so nothing here applies. Delete
 * this block if that toggle never gets built. */
:root[data-theme="dark"] {
  --bg:          #14120f;
  --surface:     #1c1a16;
  --surface-2:   #23201b;
  --field:       #1c1a16;

  --text:        #f5f1ea;
  --text-2:      #ded8cd;
  --text-3:      #a89f92;
  --text-4:      #8a8071;

  --line:         #322e28;
  --line-soft:    #282520;
  --line-strong:  #423d35;
  --line-control: #756c5f;

  --side:        #1a1814;
  --side-text:   #ded8cd;
  --side-muted:  #a89f92;
  --side-line:   #322e28;
  --side-hover:  #262218;
  --side-accent: #ff9c57;

  --brand:        #ff8a3d;
  --accent:       #ff9c57;
  --accent-hover: #ffb37d;
  --accent-soft:  #2e1b0c;
  --accent-on:    #1a0d03;

  --ok:          #4ade80;
  --ok-soft:     #10241a;
  --danger:      #ff6b6b;
  --danger-soft: #2b1213;
  --warn:        #e0b341;
  --warn-soft:   #241d0e;

  --shadow-card: 0 0 0 1px rgba(255, 255, 255, 0.04);
}

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

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

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--f-ui);
  font-size: 16px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; height: auto; }

/* No border-radius here. Setting one overrides the corner of whatever takes
   focus; an outline already follows the element's own. */
:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* The caption: 12px uppercase, tracked open a little because caps set solid
   are hard to read. The only place tracking goes positive. Nav group
   headings, table headers and stat captions. */
.label,
.side-label,
thead th,
.stat-label {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--text-3);
}

/* ------------------------------------------------------------- skip link */

.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 100;
  background: var(--accent);
  color: var(--accent-on);
  padding: 12px 22px;
  border-radius: var(--r-input);
  font-weight: 500;
}
.skip:focus { left: 8px; top: 8px; }

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

.app {
  display: grid;
  grid-template-columns: var(--side-w) minmax(0, 1fr);
  min-height: 100vh;
}
/* An error page shown to someone signed out has no nav to draw. */
.app-anon { grid-template-columns: minmax(0, 1fr); }

/* --------------------------------------------------------------- sidebar */

/* No border-right. The rail is a tonal step deeper than the canvas, and that
   difference is the whole separation — a rule on top of it would say the
   same thing twice. */
.side {
  background: var(--side);
  padding: 20px 0 32px;
  display: flex;
  flex-direction: column;
  gap: 24px;
}

.side-brand {
  padding: 0 20px;
  text-decoration: none;
  display: block;
}
/* The logo's own lockup: LARYZE in graphite, TECHNICAL in orange. Not a
   headline with one word accented for emphasis — it is how the mark is
   drawn, so splitting it anywhere else would be the wrong reading. */
.side-brand-name {
  display: block;
  color: var(--side-text);
  font-size: 16px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.side-brand-name em {
  color: var(--side-accent);
  font-style: normal;
}
.side-brand-sub {
  display: block;
  margin-top: 2px;
  color: var(--side-muted);
  font-size: 12px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}

.side-group { display: flex; flex-direction: column; gap: 2px; }
.side-label {
  color: var(--side-muted);
  margin: 0 0 6px;
  padding: 0 20px;
}

/* Inset from the rail's edge so the current page can be marked by a filled
   block rather than a full-bleed band. */
.side-link {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: var(--tap);
  margin: 0 10px;
  padding: 0 14px;
  border-radius: var(--r-input);
  color: var(--side-text);
  text-decoration: none;
  font-size: 15px;
}
/* The active marker fills in on the current page, so where-you-are never
   rests on text colour alone. */
.side-link::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  flex: none;
  border: 1px solid var(--text-4);
}
.side-link:hover { background: var(--side-hover); }
/* Lifted onto paper — the same surface stack the cards use, one step up from
   the rail it sits in. */
.side-link[aria-current="page"] {
  background: var(--surface);
  color: var(--text);
  font-weight: 600;
  box-shadow: var(--shadow-card);
}
.side-link[aria-current="page"]::before {
  background: var(--brand);
  border-color: var(--brand);
}

.side-badge {
  margin-left: auto;
  min-width: 20px;
  padding: 1px 6px;
  border-radius: var(--r-chip);
  background: var(--accent);
  color: var(--accent-on);
  font-variant-numeric: tabular-nums;
  font-size: 12px;
  text-align: center;
}

@media (max-width: 860px) {
  .app { grid-template-columns: minmax(0, 1fr); }
  /* Two auto rows inside a 100vh grid share the leftover height between them,
     so on a short page — the archive screen with nothing yet to archive — the
     nav strip inflated to fill half the screen, while a long page looked
     correct because there was no slack to hand out. Pinning the strip to its
     content and letting the content row take the rest fixes it for every
     page. Not applied to .app-anon, which has no strip and only one row. */
  .app:not(.app-anon) { grid-template-rows: auto 1fr; }
  .side {
    flex-direction: row;
    align-items: center;
    gap: 0;
    /* Containing block as well as scroller, so the .visually-hidden spans in
       the nav links are clipped with the strip instead of extending the page.
       Without it the whole document scrolled sideways. */
    position: relative;
    overflow-x: auto;
    padding: 0 8px;
    /* The one place a rule earns its keep: scrolled sideways, the strip and
       the content beneath it are the same tone with nothing between them. */
    border-bottom: 1px solid var(--side-line);
  }
  .side-brand { padding: 10px 12px 10px 8px; flex: none; }
  .side-brand-sub { display: none; }
  .side-group { flex-direction: row; }
  .side-label { display: none; }
  .side-link {
    margin: 0;
    padding: 0 12px;
    border-radius: 0;
    white-space: nowrap;
  }
  .side-link::before { display: none; }
  /* The strip is dark, and the wide rail marks the current page by lifting it
     onto white — so the ink that reads on that white block is invisible here.
     The bar underneath already says where you are, so the text simply does
     not change colour; the weight still does, which keeps a second cue that
     is not colour. */
  .side-link[aria-current="page"] {
    background: none;
    color: var(--side-text);
    box-shadow: inset 0 -3px 0 var(--side-accent);
  }
}

/* --------------------------------------------------------------- topbar */

.topbar {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px 16px;
  padding: 12px 26px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}
.crumb {
  font-size: 14px;
  color: var(--text-3);
  display: flex;
  align-items: center;
  gap: 7px;
}
.crumb b { color: var(--text); font-weight: 600; }
.crumb-sep { color: var(--text-4); }

.who {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 9px;
}
.avatar {
  width: 30px;
  height: 30px;
  border-radius: 50%;
  background: var(--accent-soft);
  color: var(--accent);
  border: 1px solid var(--accent);
  display: grid;
  place-items: center;
  font-size: 13px;
  font-weight: 700;
  flex: none;
}
.who-name { font-size: 14px; font-weight: 600; }
.who-role { font-size: 13px; color: var(--text-3); }

/* --------------------------------------------------------------- layout */

.shell { padding: 26px 26px 72px; }
.narrow { max-width: 640px; }

@media (max-width: 620px) {
  .topbar { padding: 10px 16px; }
  .shell { padding: 18px 16px 56px; }
}

/* Tracking tightens as the type grows. A grotesque set large at its default
   spacing looks loose and unfinished; pulling it in is what makes a heading
   read as a heading rather than as big body text. Never outside ±0.05em. */
h1 {
  font-size: 30px;
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1.2;
  margin: 0 0 6px;
  text-wrap: balance;
}
h2 {
  font-size: 24px;
  font-weight: 600;
  letter-spacing: -0.025em;
  line-height: 1.33;
  margin: 36px 0 12px;
}
h3 {
  font-size: 18px;
  font-weight: 600;
  letter-spacing: -0.01em;
  margin: 24px 0 10px;
}
p { margin: 0 0 12px; }

.lede { color: var(--text-3); margin-bottom: 22px; font-size: 15px; }
.muted { color: var(--text-3); }

a { color: var(--accent); text-underline-offset: 2px; }

/* Figures line up column to column so a mistyped amount shows as a ragged
   edge. ₦45,000 and ₦450,000 look alike; stacked in a column they do not. */
/* Marks a figure. Not a different typeface — the same one throughout.
   ₦45,000 and ₦450,000 look alike in a sentence; stacked in a right-aligned
   column with equal-width digits, the ragged edge gives a mistyped amount
   away. Most UI faces (IBM Plex Sans and Segoe UI included) already draw
   tabular figures by default, so this asks for them explicitly only to keep
   that true wherever the stack falls through to a face that doesn't. */
.num {
  font-variant-numeric: tabular-nums;
  /* A figure is one token. "ORD-00023" broken across two lines, or a
     timestamp split after the month, is harder to read than a wider column
     is to scroll — and the table already scrolls in its own box. */
  white-space: nowrap;
}

/* A password shown exactly once, to be copied onto paper before the page is
   left. Large, monospaced and letter-spaced because it is transcribed by
   hand: the characters that get confused are the ones this is sized to
   separate, and accounts/passwords.py has already removed the worst of them
   (0/O, 1/l/I) from the alphabet. */
.issued-password {
  margin: 0 0 12px;
}
.issued-password code {
  display: inline-block;
  font-size: 24px;
  letter-spacing: 0.12em;
  padding: 10px 16px;
  border: 1px solid var(--line);
  border-radius: var(--r-chip);
  background: var(--surface-2);
  user-select: all;   /* one tap or click selects the whole thing */
}

/* The name badge in the corner is a link to your own account. Styled as
   plain content rather than as a link: it sits in the chrome, where an
   underline and a colour would read as a stray anchor rather than as the
   name it already looks like. The affordance is the hover and the focus
   ring, which is enough for something you find by looking for your name. */
.who-link {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 4px 8px;
  margin: -4px -8px;
  border-radius: var(--r-chip);
  color: inherit;
  text-decoration: none;
}
.who-link:hover { background: var(--surface-2); }
.who-link:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* Forms whose fields hold short, known values — a name, a username, an
   11-digit phone. At the panel's full width a phone number sits in a 600px
   box, which reads as "type a lot here" and leaves a mistyped digit floating
   in empty space where it is harder to spot.

   One cap for every field rather than a width per field: five boxes of five
   different widths would look like a form assembled from spare parts, and the
   longest value here (an email) needs most of this anyway.
   Capped on the field WRAPPER, not on the input. .pw-field is the positioning
   context for the show/hide eye, so narrowing only the input left the eye
   anchored to the old full-width edge, floating in space beside the box it
   belongs to. Narrowing the wrapper takes the input, its help text and the
   eye's frame of reference together. */
.form-compact p { max-width: 22rem; }

@media (max-width: 640px) {
  /* Nothing to gain from capping below the panel's own width on a phone, and
     a short box floating in a narrow card reads as broken rather than tidy. */
  .form-compact p { max-width: none; }
}

/* Row actions sit side by side. The reset control is a <form> (so the POST
   cannot be a link a browser might prefetch) and Edit is an <a>; both are
   block-level by default, which stacked them and made a two-action row twice
   the height of a one-action row. */
.row-buttons {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
}
.row-buttons form { margin: 0; }

/* ---------------------------------------------------------------- chips */

/* Three variants for the three things a chip here has to say: a soft fill
   for a neutral label, an outline for a flag worth noticing that is not yet
   a problem, and a tinted fill for the two that are genuinely states. */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 12.5px;
  font-weight: 500;
  line-height: 1.33;
  padding: 3px 10px;
  border-radius: var(--r-chip);
  border: 1px solid transparent;
  white-space: nowrap;
}
/* Soft: a label, not a state. */
.chip-draft { color: var(--text-2); background: var(--surface-2); border-color: var(--line); }
.chip-info  { color: var(--text-2); background: var(--surface-2); border-color: var(--line); }
/* Outline: a flag worth noticing that is not yet a problem. This is where
   the amber used to be. Orange is the brand and red is trouble, so a third
   warm hue in between made all three harder to read at a glance across a
   warehouse — and the word in the chip was always doing the real work. */
.chip-caution { color: var(--text-2); background: none; border-color: var(--text-4); }
/* Unread. The one place the brand colour marks a row rather than an action,
   because "which of these have I not seen" is the only question this page
   exists to answer. */
.chip-new { color: var(--accent); background: var(--accent-soft); border-color: currentColor; }
/* The two that are genuinely states, in the only two status hues left. */
.chip-done { color: var(--ok);     background: var(--ok-soft);     border-color: currentColor; }
.chip-out  { color: var(--danger); background: var(--danger-soft); border-color: currentColor; }

/* ------------------------------------------------------------- messages */

.messages {
  list-style: none;
  margin: 0 0 20px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
/* The rule on the left carries the kind; the fill stays close to paper. A
   fully tinted bar would be a second block of colour competing with whatever
   the message is about. */
.messages li {
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--text-4);
  border-radius: var(--r-input);
  padding: 11px 14px;
  font-size: 15px;
}
.messages li.error   { border-left-color: var(--danger); background: var(--danger-soft); }
.messages li.warning { border-left-color: var(--warn);   background: var(--warn-soft); }
.messages li.success { border-left-color: var(--ok);     background: var(--ok-soft); }

.notice {
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--warn);
  border-radius: var(--r-input);
  padding: 13px 16px;
  margin: 0 0 20px;
  font-size: 15px;
}
.notice-title { font-weight: 600; color: var(--text); margin-bottom: 2px; }

/* --------------------------------------------------------------- panels */

/* A hairline draws the edge and the shadow only lifts it a hair off the
   cream. The shadow is warm, not grey: a neutral shadow on a warm ground
   reads as dirt. */
.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-card);
  padding: 20px;
}
.panel > :first-child { margin-top: 0; }
.panel > :last-child { margin-bottom: 0; }

.facts { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 8px 20px; }
.facts dt { font-size: 13.5px; color: var(--text-3); }
.facts dd { margin: 0; font-size: 15px; }

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

/* Django's as_p emits <p><label>…<input><span class="helptext">. Styled as
   rendered rather than replaced, because as_p already associates every label
   with its field and that is worth keeping. */
form p {
  display: flex;
  flex-direction: column;
  gap: 5px;
  margin: 0 0 16px;
}
form label { font-weight: 600; font-size: 14px; }
.helptext { font-size: 13px; color: var(--text-3); }

/* White fields on the cream page, with a real border at rest rather than one
   that appears on focus: a field someone has to find while holding a
   delivery note needs an edge before it is touched. */
input[type="text"], input[type="password"], input[type="number"],
input[type="date"], input[type="search"], input[type="email"],
input[type="tel"], select, textarea {
  font: inherit;
  font-size: 15px;
  min-height: var(--tap);
  width: 100%;
  padding: 9px 16px;
  background: var(--field);
  color: var(--text);
  border: 1px solid var(--line-control);
  border-radius: var(--r-input);
}
input:hover, select:hover, textarea:hover { border-color: var(--text); }
/* The one multi-line control. Same corner as every other field. */
textarea {
  min-height: 88px;
  padding: 12px 16px;
  border-radius: var(--r-input);
  resize: vertical;
}
select { cursor: pointer; }

input[type="number"], #id_unit_price, #id_quantity {
  font-variant-numeric: tabular-nums;
}

/* The column flex above stretches its children, which is right for inputs and
   wrong for buttons — without this every button inside a form <p> spans the
   full width of the page. */
form p > button { align-self: flex-start; }

/* The eye, injected into every password field by _password_toggle.html.
   The span is what makes the field a positioning context; without scripting
   neither it nor the button exists and the input is left alone. */
.pw-field { position: relative; display: block; }
/* Room for the button, so a long password never runs under it. */
.pw-field input[type="password"],
.pw-field input[type="text"] { padding-right: 50px; }

.pw-toggle {
  position: absolute;
  top: 50%;
  right: 3px;
  transform: translateY(-50%);
  width: 38px;
  height: 38px;
  min-height: 0;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: none;
  color: var(--text-3);
}
.pw-toggle:hover { background: var(--surface-2); color: var(--text); }
.pw-toggle:focus-visible { outline-offset: -2px; }
/* aria-pressed carries the state for assistive tech; this makes it visible,
   so "the password is currently on screen" is not conveyed by the glyph
   swap alone. */
.pw-toggle[aria-pressed="true"] { color: var(--accent); }

form p:has(> input[type="checkbox"]) {
  flex-direction: row;
  align-items: center;
  gap: 9px;
  flex-wrap: wrap;
}
input[type="checkbox"] {
  width: 20px;
  height: 20px;
  min-height: 0;
  accent-color: var(--accent);
  flex: none;
}
form p:has(> input[type="checkbox"]) label { font-weight: 500; }
form p:has(> input[type="checkbox"]) .helptext { flex-basis: 100%; }

.errorlist {
  list-style: none;
  margin: 0 0 8px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.errorlist li {
  background: var(--danger-soft);
  border-left: 3px solid var(--danger);
  border-radius: var(--r-input);
  color: var(--text);
  padding: 7px 12px;
  font-size: 14px;
}

/* Inline filter bars read across, not down. */
.filterbar {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 10px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-card);
  padding: 10px 14px;
  margin: 0 0 18px;
}
.filterbar p { margin: 0; min-width: 160px; gap: 3px; }

/* Filter controls run shorter than the 44px the rest of the app uses. That
   number is the AAA target (WCAG 2.5.5) and it is the right default for the
   things an operator taps mid-job — Record order, Mark read, a row's Edit.
   A filter is not one of those: it is chosen once and then left alone, and at
   full height a two-dropdown bar plus its button was taking a sixth of a
   phone screen before a single row of data. 36px still clears the AA minimum
   (2.5.8, 24px) with room to spare. */
.filterbar label { font-size: 13px; }
.filterbar select,
.filterbar input[type="search"],
.filterbar input[type="text"],
.filterbar input[type="date"] {
  min-width: 160px;
  min-height: 36px;
  padding: 6px 11px;
  font-size: 14px;
}
.filterbar button, .filterbar .btn {
  min-height: 36px;
  padding: 6px 15px;
  font-size: 14px;
}
/* width:auto beats the base input rule's width:100%. Inside a flex row a
   full-width field claims the whole line and shoulders its own button onto a
   second one, which is what made this bar three rows tall. */
.filterbar .row { display: flex; align-items: center; flex-wrap: nowrap; gap: 8px; }
.filterbar .row input, .filterbar .row select { width: auto; }

/* Pushes what follows to the right edge. The search on the stock screen sits
   opposite its filters rather than after them: it answers a different
   question ("where is this one thing") from the filters beside it ("show me
   this kind of thing"), and the gap between them says so without a label. */
.filterbar-end > :last-child { margin-left: auto; }
.filterbar-search { min-width: 0; }
.filterbar-search input[type="search"] { min-width: 200px; }

@media (max-width: 640px) {
  /* Nothing to push against once the bar wraps to one control per line, and
     a right-aligned box on its own row reads as a mistake. */
  .filterbar-end > :last-child { margin-left: 0; }
  .filterbar-search { width: 100%; }
  .filterbar-search .row { width: 100%; }
  /* The field gives way, not the buttons. width:100% here would have the
     input claim the row and push Search and Clear off the right edge — 11px
     of document overflow on a 374px screen, which is a whole page that
     swipes sideways for nothing. Buttons keep their size; the box takes what
     is left. */
  .filterbar-search input[type="search"] {
    flex: 1 1 auto;
    width: auto;
    min-width: 0;
  }
}

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

/* The primary action wears the brand colour, carried down from the logo's
   brightness until white sits on it above 4.5:1. No shadow — tonal contrast
   does the work. */
button, .btn {
  font: inherit;
  font-weight: 500;
  font-size: 15px;
  min-height: var(--tap);
  padding: 10px 22px;
  background: var(--accent);
  color: var(--accent-on);
  border: 1px solid var(--accent);
  border-radius: var(--r-input);
  cursor: pointer;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
}
button:hover, .btn:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
button:disabled { opacity: 0.45; cursor: not-allowed; }
button:disabled:hover { background: var(--accent); border-color: var(--accent); }

/* Secondary actions are outlined rather than filled: these mostly sit
   directly on the canvas, where a soft fill the same value as the page
   leaves nothing to aim at. */
.btn-quiet, button.btn-quiet {
  background: var(--surface);
  color: var(--text);
  border-color: var(--line-control);
}
.btn-quiet:hover, button.btn-quiet:hover { background: var(--bg); border-color: var(--text); }

/* The outline variant, for a control that sits inside a card next to
   filled ones. */
.btn-row, button.btn-row {
  white-space: nowrap;
  background: none;
  border: 1px solid var(--line-control);
  color: var(--text-2);
  font-size: 14px;
  font-weight: 500;
  min-height: 36px;
  padding: 5px 14px;
}
.btn-row:hover, button.btn-row:hover {
  background: var(--bg);
  color: var(--text);
  border-color: var(--text);
}
.btn-danger, button.btn-danger {
  color: var(--danger);
  border-color: var(--danger);
  background: none;
}
.btn-danger:hover, button.btn-danger:hover {
  background: var(--danger);
  border-color: var(--danger);
  color: #fff;
}

.actions { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 0 0 18px; }

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

/* A card that happens to hold a table, so it takes the card's treatment.
   The overflow clips the corners, which is what keeps the header's fill from
   squaring off the top of the radius. */
.table-scroll {
  /* Also a containing block, not only a scroller — see .visually-hidden. */
  position: relative;
  overflow-x: auto;
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-card);
  background: var(--surface);
  margin: 0 0 14px;
}

table { border-collapse: collapse; width: 100%; min-width: 520px; }

thead th {
  text-align: left;
  background: var(--surface-2);
  padding: 10px 16px;
  border-bottom: 1px solid var(--line);
  white-space: nowrap;
  position: sticky;
  top: 0;
}
tbody td {
  padding: 12px 16px;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: top;
  font-size: 15px;
}
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover td { background: var(--bg); }

tfoot th {
  font-size: 15px;
  font-weight: 700;
  text-align: left;
  text-transform: none;
  letter-spacing: normal;
  color: var(--text);
  padding: 12px 16px;
  border-top: 1px solid var(--line-strong);
  background: var(--surface-2);
}

th.col-num, td.col-num { text-align: right; white-space: nowrap; }

.sub { display: block; font-size: 13px; color: var(--text-3); margin-top: 2px; }

/* ----------------------------------------------------------- pagination */

/* Numbered pages rather than one long scroll: the page you are on is a
   landmark you can come back to, which a scroll position is not. Django's
   elided range keeps the control a fixed width however many pages there are. */
.pager {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  margin: 0 0 24px;
}
.pager-count {
  margin-right: auto;
  font-size: 13.5px;
  color: var(--text-3);
}
.pager a, .pager span {
  min-width: var(--tap);
  min-height: var(--tap);
  padding: 0 10px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--line-control);
  border-radius: var(--r-input);
  background: var(--surface);
  color: var(--text-2);
  font-size: 15px;
  text-decoration: none;
  font-variant-numeric: tabular-nums;
}
.pager a:hover { background: var(--bg); color: var(--text); border-color: var(--text); }
.pager .is-current {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-on);
  font-weight: 600;
}
/* The gap in an elided range, and the disabled ends. Not buttons — no border,
   nothing to aim at. */
.pager .is-gap, .pager .is-off {
  border-color: transparent;
  background: none;
  color: var(--text-4);
}

@media (max-width: 620px) {
  .pager-count { margin-right: 0; width: 100%; }
}

/* --------------------------------------------------------- launch tiles */

.tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 12px;
  margin: 0 0 26px;
}
.tile {
  display: flex;
  flex-direction: column;
  gap: 4px;
  min-height: 92px;
  padding: 20px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  box-shadow: var(--shadow-card);
  color: var(--text);
  text-decoration: none;
}
.tile:hover { border-color: var(--text-4); }
.tile-name { font-weight: 600; font-size: 16px; letter-spacing: -0.01em; }
.tile-note { font-size: 13.5px; color: var(--text-3); }

/* The one filled thing on the home screen, and the only orange block on it.
   Recording an order is the hourly job — an operator typing in what just came
   through on WhatsApp — so it is where the eye is sent first and the one
   place the brand colour is spent. */
.tile-primary {
  grid-column: span 2;
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-on);
  min-height: 116px;
}
.tile-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); }
.tile-primary .tile-name { font-size: 24px; letter-spacing: -0.025em; }
.tile-primary .tile-note { color: color-mix(in oklch, var(--accent-on) 78%, transparent); }

@media (max-width: 560px) {
  .tiles { grid-template-columns: 1fr; }
  .tile-primary { grid-column: span 1; }
}

/* ------------------------------------------------------------ line prep */

.line-preview:not(:empty) {
  display: block;
  font-variant-numeric: tabular-nums;
  font-size: 15px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--brand);
  border-radius: var(--r-input);
  padding: 11px 14px;
  margin: 0 0 16px;
}

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

/* Centred both ways, via auto margins on the card rather than align-items.
   They do the same thing until the card is taller than the screen — a short
   laptop, or three validation errors stacked above the fields — and then
   align-items:center overflows upward and cuts the top off where auto
   margins collapse to zero and leave it scrollable. */
.login-page {
  min-height: 100vh;
  display: flex;
  padding: 40px 16px;
}
.login-card { width: 100%; max-width: 400px; margin: auto; }
.login-brand { margin-bottom: 26px; text-align: center; }

/* The one screen with nothing else on it, so the form is given room rather
   than packed to the density the list screens need. */
.login-card .panel { padding: 30px 28px 32px; }
.login-card form p { margin-bottom: 22px; }
.login-card form label { font-size: 15px; }
.login-brand-name {
  display: block;
  font-size: 20px;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.login-brand-name em { color: var(--accent); font-style: normal; }
.login-brand-sub {
  display: block;
  margin-top: 2px;
  color: var(--text-3);
  font-size: 12px;
  letter-spacing: 0.05em;
  text-transform: uppercase;
}
.login-card h1 { font-size: 20px; letter-spacing: -0.01em; margin-bottom: 22px; }
.login-card button[type="submit"] {
  width: 100%;
  min-height: 48px;
  margin-top: 8px;
}
.login-foot {
  margin-top: 22px;
  font-size: 13.5px;
  color: var(--text-3);
}

/* ------------------------------------------------------------ utilities */

/* Read aloud, never drawn.

   position:absolute with no positioned ancestor means the containing block is
   the viewport, so these escape any scroller they sit inside: the sidebar's
   nav strip scrolls sideways on mobile, and a span sitting 730px along it was
   pushing the DOCUMENT's scrollWidth to 738 against a 372px viewport. The
   result was a page that swiped left into a screen and a half of empty
   background. Anything with overflow-x that can hold one of these therefore
   has to be a containing block too — .side and .table-scroll below. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.back { display: inline-block; margin-top: 22px; font-size: 15px; }

details > summary {
  cursor: pointer;
  min-height: var(--tap);
  display: flex;
  align-items: center;
  color: var(--danger);
  font-weight: 500;
  font-size: 15px;
}

hr { border: none; border-top: 1px solid var(--line); margin: 28px 0; }

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