/* Fanzart Gate Pass — screen styles for the app.
   The printed pass has its own tuned geometry in print.css; nothing here
   touches it. */

/* Slate, not beige. The warm off-white read as washed out under the office
   strip lights, which is where this is actually used — the greys below are
   neutral so the ink keeps its contrast under that light rather than going
   muddy. The brass accent survives as a brand touch, but it no longer carries
   any contrast work. */
:root {
  --ink: #1e293b;             /* slate 800 — primary text and headings      */
  --ink-2: #334155;           /* slate 700 — body text, labels              */
  --text-2: #64748b;          /* slate 500 — subtitles, hints, metadata     */
  --muted: #94a3b8;           /* slate 400 — placeholders, quiet labels     */
  --label: #475569;           /* slate 600 — table headings                 */
  --line: #e2e8f0;            /* slate 200 — card and input borders         */
  --line-soft: #edf2f7;       /* the horizontal rules inside a table        */
  --paper: #f8fafc;           /* slate 50 — the page behind the cards       */
  --card: #ffffff;
  --hover: #f8fafc;           /* row hover, same as the page: a quiet lift  */
  --head-bg: #f1f5f9;         /* slate 100 — table header row               */
  --brand: #0f172a;
  --brand-hover: #1e293b;     /* slate 800 — primary button hover           */
  --accent: #8a6a3a;          /* brass, from the Fanzart wordmark           */
  --accent-soft: #f5efe4;
  --focus: #2563eb;           /* focus: a blue border and a soft blue ring  */
  --focus-soft: rgba(37, 99, 235, .12);
  --link: #2563eb;            /* royal blue — links and interactive text    */
  --field-line: #cbd5e1;      /* slate 300 — outline-button edges           */
  --field-bg: #ffffff;
  --field-optional: #f8fafc;  /* optional fields: a shade back, less urgent */
  --blue: #3b82f6;            /* used only for the drop zone's active state */
  --blue-soft: #eff6ff;
  --ok: #166534;              /* green 800 — legible on its pale badge      */
  --ok-bg: #dcfce7;
  --warn: #b45309;
  --warn-bg: #fef3c7;
  --danger: #b91c1c;
  --danger-bg: #fee2e2;
  --radius: 12px;             /* cards                                      */
  --radius-sm: 8px;           /* fields, buttons, inner groups              */
  /* Soft pill badges. Their own values rather than --warn/--danger, which
     also draw borders, stripes and solid badges elsewhere. */
  --pill-warn-bg: #fef3c7;
  --pill-warn: #d97706;
  --count-bg: #fee2e2;
  --count-fg: #dc2626;
  --shadow: 0 1px 3px 0 rgba(0, 0, 0, .05);
  --shadow-lift: 0 4px 6px -1px rgba(0, 0, 0, .07), 0 2px 4px -2px rgba(0, 0, 0, .05);
  /* Menus and panels that float over the form. Separate from --shadow-lift
     because a popover has to read as ABOVE the page, not merely raised, and
     because it needs a much heavier value in dark mode where a 12%-black
     shadow on a near-black background is not there at all. */
  --pop-shadow: 0 8px 24px rgba(15, 23, 42, .14), 0 2px 6px rgba(15, 23, 42, .08);
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

@media (prefers-color-scheme: dark) {
  :root {
    --pop-shadow: 0 10px 30px rgba(0, 0, 0, .55), 0 2px 8px rgba(0, 0, 0, .4);
    --ink: #e2e8f0;             /* off-white: less glow than pure white */
    --ink-2: #cbd5e1;
    --text-2: #94a3b8;
    --link: #60a5fa;
    --pill-warn-bg: #3a2a08;
    --pill-warn: #fbbf24;
    --count-bg: #3a1618;
    --count-fg: #f87171;
    --muted: #64748b;
    --label: #94a3b8;
    --line: #334155;
    --line-soft: #293548;
    --paper: #0f172a;           /* slate 900 page                        */
    --card: #1e293b;            /* slate 800 raised cards                */
    --hover: #243247;
    --head-bg: #243247;
    --field-bg: #0f172a;        /* fields sit recessed below the card    */
    --field-optional: #172033;
    --brand: #e2e8f0;
    --brand-hover: #cbd5e1;
    --accent: #c9a367;
    --accent-soft: #2a2419;
    --focus: #60a5fa;
    --focus-soft: rgba(96, 165, 250, .2);
    --blue: #60a5fa;
    --blue-soft: #17233b;
    --ok: #4ade80;
    --ok-bg: #14311f;
    --warn: #fbbf24;
    --warn-bg: #3a2a08;
    --danger: #f87171;
    --danger-bg: #3a1618;
  }
}

* { box-sizing: border-box; }

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

body {
  margin: 0;
  /* Inter first; a PC without it falls to its own system face (Segoe UI on
     Windows), which is what it used before. */
  font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
               Helvetica, Arial, sans-serif;
  font-size: 15px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--paper);
  -webkit-font-smoothing: antialiased;
}

/* An element carrying the `hidden` attribute must stay hidden. Any rule that
   sets `display` beats the browser's own [hidden] rule, because author styles
   win over the user-agent stylesheet — and .notice sets display:flex. The
   review screen was drawing an empty bar for a notice marked hidden, which
   went unnoticed only for as long as its tint matched the page behind it. */
[hidden] { display: none !important; }

/* ---------------------------------------------------------------- top bar */

.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 0 24px;
  height: 60px;
  background: var(--card);
  border-bottom: 1px solid var(--line);
}

.brand {
  display: flex;
  flex-direction: column;
  line-height: 1.05;
  text-decoration: none;
  color: var(--brand);
}

/* The lockup is two images stacked in one box: the wordmark with the fan
   erased, and the fan on its own square canvas. Same artwork, same size, same
   position — only the fan can be turned.

   The wrapper has no width of its own; it shrink-wraps whatever .brand-logo is
   sized to. Pinning it to 118px broke the login page, which draws the same
   lockup at 190px: the wrapper stayed 118 and the fan, sized in percentages of
   it, came out two-thirds too small and off its hub.

   The fan's numbers are percentages, not pixels, so it stays registered to the
   wordmark at any size, on a lockup that is 1000x336 in the artwork's own
   coordinates. How they are derived is on .fan-blade below. */
.brand-lockup {
  position: relative;
  display: inline-block;
  line-height: 0;
}

.brand-logo {
  width: 118px;
  height: auto;
  display: block;
}

/* Sized and placed so the blades never reach the wordmark.
   The blades sweep a circle of radius 128.4px (in the 1000px artwork's
   coordinates) around the hub, but the "f" is only 110px to the right of it and
   the tagline only 112px below — so at rest the mark looked right and, once
   turning, it swept straight through both. Worst case measured at 200
   overlapping pixels, at 57 degrees.
   0.89 of the original size, with the hub moved from (101,115) to (86,102),
   clears everything by 10px of artwork — about 1.2px in the top bar and 1.9px
   on the login page. Checked by compositing the rotated fan over the wordmark
   at every single degree: no overlap anywhere, and the gap holds under a 10px
   dilation of the wordmark.
   left/top/width carry the placement, NOT transform: transform belongs to the
   rotation, and a translate or scale there would be overwritten by the
   keyframes on the first frame.
     width = 232 * 0.89 / 1000
     left  = (86 - 232*0.89/2) / 1000
     top   = (102 - 232*0.89/2) / 336     (top % resolves against height) */
.fan-blade {
  position: absolute;
  left: -1.724%;
  top: -0.369%;
  width: 20.648%;
  height: auto;
  /* The canvas is centred on the hub, not on the blades' bounding box — the
     blades are not symmetrical about it, and spinning about the box centre
     makes the mark wobble instead of turn. */
  transform-origin: center;
  animation: fan-spin 8s linear infinite;
  will-change: transform;
}

/* Anticlockwise. The blades are swept, so they have a leading and a trailing
   edge: turned the other way a real fan of this shape would be pushing air
   backwards, and the eye reads it as running in reverse even though the motion
   is perfectly smooth. */
@keyframes fan-spin {
  from { transform: rotate(0deg); }
  to   { transform: rotate(-360deg); }
}

/* Something that never stops moving, in the corner of every screen, all day.
   Anyone who has asked their system to calm animations down gets a still fan. */
@media (prefers-reduced-motion: reduce) {
  .fan-blade { animation: none; }
}

/* The Fanzart artwork is solid black, so in dark mode it is inverted rather
   than sitting in a white box. Both halves, or the fan stays black on a dark
   bar while the wordmark goes white. */
@media (prefers-color-scheme: dark) {
  .brand-logo, .fan-blade { filter: invert(1); }
}
:root[data-theme="light"] .brand-logo,
:root[data-theme="light"] .fan-blade { filter: none; }
:root[data-theme="dark"] .brand-logo,
:root[data-theme="dark"] .fan-blade { filter: invert(1); }

.brand-sub {
  font-size: 8.5px;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--muted);
  margin-top: 2px;
}

.topbar nav {
  display: flex;
  align-items: center;
  gap: 4px;
}

.topbar nav a {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 7px 13px;
  border-radius: 999px;
  font-size: 14px;
  text-decoration: none;
  color: var(--ink-2);
  white-space: nowrap;
  transition: background .15s, color .15s;
}

.topbar nav a:hover { background: var(--line-soft); color: var(--ink); }
/* The icon takes the link's colour, so it turns white in the active pill
   with the word, and follows the theme without a rule of its own. */
.topbar nav a .icon { flex: 0 0 auto; }
/* The icons cost the nav about 160px. Below 1100px that pushed every page
   into a sideways scroll, so there the words carry on alone -- exactly the
   nav it was before the icons -- and the icons return where there is room. */
@media (max-width: 1100px) {
  .topbar nav a .icon { display: none; }
}

/* The active tab is a solid slate pill. Tried a bottom-border accent first;
   with the nav on a white bar it reads as an underlined link rather than as
   "you are here", and it moves the text off the baseline of its neighbours. */
.topbar nav a.active {
  background: var(--brand);
  color: var(--card);
  font-weight: 600;
}

.topbar nav a.active:hover { background: var(--brand-hover); color: var(--card); }

.nav-divider {
  width: 1px;
  height: 20px;
  background: var(--line);
  margin: 0 6px;
}

/* Name and Sign out share one pill so they read as one thing — whose session
   this is — rather than as two more nav items competing with the tabs. */
.user-pill {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 6px 5px 12px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--card);
  white-space: nowrap;
}

.who {
  font-size: 13.5px;
  font-weight: 600;
  color: var(--ink);
  white-space: nowrap;
}

.logout-form { display: inline; margin: 0; }

.user-pill .logout-form button {
  padding: 4px 10px;
  border-radius: 999px;
  font-size: 12.5px;
  border: 1px solid transparent;
  background: var(--head-bg);
  color: var(--ink-2);
}

.user-pill .logout-form button:hover {
  background: var(--danger-bg);
  color: var(--danger);
  border-color: transparent;
}

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

body.login-page {
  display: grid;
  place-items: center;
  min-height: 100vh;
  padding: 24px;
}

.login-shell { width: 100%; max-width: 370px; }

.login-brand {
  display: flex;
  flex-direction: column;
  align-items: center;
  line-height: 1.05;
  margin-bottom: 24px;
}
.login-brand .brand-logo { width: 190px; }
.login-brand .brand-sub { font-size: 10px; }

.login-submit { width: 100%; margin-top: 6px; }

input[type="password"] {
  width: 100%;
  padding: 9px 11px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 14.5px;
  color: var(--ink);
  background: var(--card);
}
input[type="password"]:focus {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.login-foot {
  margin-top: 18px;
  text-align: center;
  font-size: 12.5px;
  color: var(--muted);
}

.login-page code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 12px;
  background: rgba(0, 0, 0, .06);
  padding: 1px 5px;
  border-radius: 4px;
}

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

.container {
  max-width: 960px;
  margin: 0 auto;
  padding: 28px 24px 80px;
}

.page-head { margin-bottom: 20px; }

/* A heading with an action opposite it. Wraps rather than squeezing: on a
   narrow window the button drops under the heading and keeps its size instead
   of becoming a sliver of text at the edge of the screen. */
.page-head-split {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}
.page-head-split h1 { margin-bottom: 0; }

h1 {
  font-size: 24px;
  font-weight: 650;
  letter-spacing: -.02em;
  margin: 0 0 6px;
}

/* Instructional helper text is tertiary: light grey, pushed into the
   background behind the data and the labels. Metadata -- dates, counts,
   sizes -- stays in the secondary grey. */
.hint { color: var(--muted); font-size: 13.5px; margin: 0; }

/* No base link colour existed, so links showed the browser's own blue and
   purple. Any component that styles its links still does. */
a { color: var(--link); }

.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 22px 24px;
  box-shadow: var(--shadow);
}

/* -------------------------------------------------------------- steps bar */

.steps {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 22px;
  font-size: 13px;
  color: var(--text-2);
  flex-wrap: wrap;
}

.step {
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

.step-num {
  display: grid;
  place-items: center;
  width: 21px;
  height: 21px;
  border-radius: 50%;
  border: 1px solid var(--line);
  font-size: 11.5px;
  font-weight: 600;
  color: var(--muted);
  background: var(--card);
}

.step.current { color: var(--ink); font-weight: 600; }
.step.current .step-num {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--card);
}

.step.done .step-num {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent);
}

.step-sep { color: var(--line); }

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

.flashes { list-style: none; padding: 0; margin: 0 0 18px; }

.flashes li {
  display: flex;
  gap: 9px;
  padding: 11px 14px;
  border-radius: var(--radius-sm);
  margin-bottom: 8px;
  font-size: 13.5px;
  border: 1px solid;
  background: var(--accent-soft);
  border-color: var(--line);
  color: var(--ink-2);
}

.flashes li.error { background: var(--danger-bg); border-color: var(--danger); color: var(--danger); }
.flashes li.warn  { background: var(--warn-bg);   border-color: var(--warn);   color: var(--warn); }
.flashes li.ok    { background: var(--ok-bg);     border-color: var(--ok);     color: var(--ok); }

.notice {
  display: flex;
  gap: 10px;
  padding: 12px 14px;
  border-radius: var(--radius-sm);
  font-size: 13.5px;
  background: var(--warn-bg);
  border: 1px solid var(--warn);
  color: var(--warn);
  margin: 0 0 18px;
}

.notice-icon { flex: none; font-size: 15px; line-height: 1.3; }

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

/* Labels are secondary: steel grey, small, uppercase, so the VALUES in the
   boxes are what the eye lands on. Not the placeholder grey -- a caption as
   faint as the hint inside the box gives the eye nothing to hold on to. */
label {
  display: block;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-2);
  margin-bottom: 6px;
}

input[type="text"],
input[type="file"],
input[type="password"],
input[type="date"],
input[type="number"],
textarea,
select {
  width: 100%;
  padding: 10px 14px;
  /* Soft containment: a faint edge, with the blue focus ring doing the work
     of showing which box is live. */
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 14px;
  color: var(--ink);
  background: var(--field-bg);
  transition: border-color .15s, box-shadow .15s;
}

input[type="text"]:focus,
input[type="password"]:focus,
input[type="date"]:focus,
input[type="number"]:focus,
textarea:focus,
select:focus {
  /* A blue border and a soft blue ring. The border carries the contrast
     (4.8:1 against white); the ring makes it findable at a glance. */
  outline: none;
  border-color: var(--focus);
  box-shadow: 0 0 0 3px var(--focus-soft);
}

::placeholder { color: var(--muted); opacity: 1; font-weight: 400; }

/* Remarks. Starts exactly as tall as the single-line fields beside it so the
   row does not sit in a hole, and grows when there is more to say. Tighter
   padding than an input because a textarea adds its own line box on top. */
textarea.multiline {
  min-height: 38px;
  line-height: 1.4;
  padding: 6px 10px;
  resize: vertical;
  display: block;      /* kills the baseline gap under an inline-block */
}

/* Running totals beside the row counter. Tabular figures so the numbers do not
   jump sideways as they change while somebody is typing. */
.running-totals {
  margin-left: auto;
  white-space: nowrap;
  color: var(--label);
}

.running-totals strong {
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.totals-sep { display: inline-block; width: 14px; }

.field { margin-bottom: 16px; }

.grid-2 {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 0 20px;
}

fieldset {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  margin: 0 0 18px;
  padding: 14px 16px;
}

legend {
  padding: 0 7px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-2);
}

.radio, .checkbox {
  display: flex;
  align-items: center;
  gap: 9px;
  font: inherit;
  font-size: 14px;
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
  color: var(--ink);
  margin: 9px 0;
  cursor: pointer;
}

.radio input, .checkbox input { width: auto; margin: 0; accent-color: var(--accent); }

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

button, .button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  cursor: pointer;
  font: inherit;
  font-size: 14px;
  font-weight: 600;
  padding: 8px 16px;
  border-radius: var(--radius-sm);
  /* The secondary, outline button: every button that is not the one primary
     action on its screen. */
  border: 1px solid var(--field-line);
  background: var(--card);
  color: var(--ink-2);
  text-decoration: none;
  transition: background .2s ease, border-color .2s ease, transform .05s;
}

button:hover, .button:hover { background: var(--paper); border-color: var(--muted); }
button:active, .button:active { transform: translateY(1px); }

button.primary {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--card);
  font-weight: 600;
  padding: 10px 20px;
}
/* A real colour change, not a transparency fade: the old opacity trick let
   the page show through and made the button look disabled on hover. */
button.primary:hover { background: var(--brand-hover); border-color: var(--brand-hover); }

/* Third level: undo actions, which must be reachable without competing with
   the thing the screen is actually for. */
button.quiet, .button.quiet {
  border-color: transparent;
  background: none;
  color: var(--label);
  box-shadow: none;
}

button.quiet:hover, .button.quiet:hover {
  background: var(--head-bg);
  border-color: transparent;
  color: var(--ink);
}

button.danger { color: var(--danger); border-color: var(--line); }
button.danger:hover { background: var(--danger-bg); border-color: var(--danger); }

button.link-button {
  border: none;
  background: none;
  padding: 0;
  font-size: 13.5px;
  color: var(--ink-2);
  text-decoration: none;
}
button.link-button:hover { background: none; color: var(--danger); text-decoration: none; }

.form-actions {
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  align-items: center;
  margin-top: 22px;
  padding-top: 18px;
  border-top: 1px solid var(--line-soft);
}

.form-actions .spacer { flex: 1; }

/* ------------------------------------------------------------ upload zone */

.dropzone {
  display: block;
  /* Tokens, not the literal #cbd5e1 and #fafafa these carried. A hardcoded
     light panel does not flip with the theme, so in dark mode the title —
     which is var(--ink), and so goes near-white — sat on it at 1.05:1 and was
     invisible. The main control of the whole app, unreadable, and nothing in
     light mode showed it. */
  border: 2px dashed var(--line);
  border-radius: var(--radius);
  background: var(--head-bg);
  padding: 46px 24px;
  text-align: center;
  cursor: pointer;
  transition: border-color .15s, background .15s, color .15s;
  color: var(--text-2);
  /* The drop zone IS a <label>, so it inherits the uppercase and letter
     spacing meant for short field captions. On a whole sentence that is hard
     to read and was shouting DRAG & DROP INVOICE PDFS OR CLICK TO BROWSE. */
  text-transform: none;
  letter-spacing: normal;
}

/* Blue only here. It is the one place in the app where the target is a thing
   you aim at rather than read, and it has to say "let go here" while a file is
   already hovering over it. */
.dropzone:hover, .dropzone.dragover {
  border-color: var(--blue);
  background: var(--blue-soft);
  color: var(--blue);
}

.dropzone input[type="file"] {
  position: absolute;
  width: 1px; height: 1px;
  opacity: 0; pointer-events: none;
}

.dropzone-icon {
  width: 40px;
  height: 40px;
  margin: 0 auto;
  display: block;
  color: inherit;         /* picks up the blue on hover */
}

.dropzone-title {
  margin-top: 12px;
  font-size: 16px;
  font-weight: 600;
  color: var(--ink);
}

/* --label rather than --muted: muted sits at about 3.3:1 on the panel now
   that the panel follows the theme, which is too faint to read. */
.dropzone-sub { margin-top: 4px; font-size: 13px; color: var(--label); }

.dropzone-file {
  margin-top: 14px;
  font-size: 13.5px;
  font-weight: 600;
  color: var(--ink-2);
  word-break: break-all;
}

/* ------------------------------------------------------------ item editor */

/* No outer border: the table sits on a white surface held by a soft shadow,
   and only faint horizontal rules separate the rows. */
.table-wrap {
  overflow-x: auto;
  border: 0;
  border-radius: var(--radius);
  background: var(--card);
  box-shadow: var(--shadow);
  margin-top: 6px;
}

table.items-editor, table.list {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
}

/* Below this the fixed column percentages stop working: the status column gets
   too narrow for its badge and clips it to "ISSU". .table-wrap already scrolls,
   so a narrow window scrolls the table sideways rather than crushing it.
   Scoped to the REGISTER, which is the wide one — eight columns and three
   action buttons. Applied to every .list it also forced the Drafts table past
   its container for no reason, and the extra width came out of the column
   holding the duplicate explanation, which wrapped to seven lines. */
table.list.register-table { min-width: 1040px; }
/* Small enough that a narrow window shrinks the columns instead of scrolling.
   The drafts table has no three-button action cell to protect, so it has far
   less it must hold on to than the register. */
table.list.drafts-table { min-width: 720px; }

/* Drafts columns.

   `auto`, not the `fixed` the register uses. Fixed layout hands every column
   exactly its declared share whether or not the content needs it, which is
   what left the gap between Invoice No and Items: Invoice No was holding 24%
   for a duplicate note that most rows do not have. Auto treats these as the
   proportions to aim for and lets a column give width back when its content is
   short — and, usefully, a cell can no longer be too narrow for a badge, which
   is the failure the register had to be measured to find. */
table.list.drafts-table { table-layout: auto; }

table.list.drafts-table th:nth-child(1),
table.list.drafts-table td:nth-child(1) { width: 5%;  }
table.list.drafts-table th:nth-child(2),
table.list.drafts-table td:nth-child(2) { width: 15%; }
table.list.drafts-table th:nth-child(3),
table.list.drafts-table td:nth-child(3) { width: 18%; }
table.list.drafts-table th:nth-child(4),
table.list.drafts-table td:nth-child(4) { width: 22%; }
table.list.drafts-table th:nth-child(5),
table.list.drafts-table td:nth-child(5) { width: 18%; }
table.list.drafts-table th:nth-child(6),
table.list.drafts-table td:nth-child(6) { width: 8%;  }
table.list.drafts-table th:nth-child(7),
table.list.drafts-table td:nth-child(7) { width: 8%;  }
table.list.drafts-table th:nth-child(8),
table.list.drafts-table td:nth-child(8) { width: 6%;  }

/* Numbers and badges centred under their headings; the words that are read
   down the column stay left; the buttons sit at the end of the row. */
table.list.drafts-table th:nth-child(6),
table.list.drafts-table td:nth-child(6),
table.list.drafts-table th:nth-child(7),
table.list.drafts-table td:nth-child(7) { text-align: center; }
table.list.drafts-table td:nth-child(8) { text-align: right; }
table.list.drafts-table td.draft-actions .actions-inner { justify-content: flex-end; }

/* One padding for every cell in this table, header and body alike. The base
   table rules set these per-element with !important, so matching that is the
   only way the two rows line up. */
table.list.drafts-table th,
table.list.drafts-table td { padding: 0.75rem 1rem !important; }

table.items-editor th, table.list th {
  text-align: left;
  font-size: 12px;              /* 0.75rem */
  font-weight: 600;
  letter-spacing: .05em;
  text-transform: uppercase;
  /* Steel grey, secondary to the data: the header names the column, the
     rows are what is being read. Not the placeholder grey, though -- a
     heading fainter than that stops working as a heading. */
  color: var(--text-2);
  background: var(--paper);
  padding: 10px 16px;
  white-space: nowrap;
  border-bottom: 1px solid var(--line);
}

table.items-editor td, table.list td {
  padding: 0;
  border-bottom: 1px solid var(--line);
}

table.list td {
  padding: 12px 16px;
  vertical-align: middle;
  color: var(--ink);
}
table.items-editor tbody tr:last-child td,
table.list tbody tr:last-child td { border-bottom: none; }

table.items-editor input {
  border: none;
  border-radius: 0;
  background: transparent;
  padding: 10px 12px;
  font-size: 14px;
}
table.items-editor input:focus {
  outline: none;
  box-shadow: inset 0 0 0 2px var(--accent);
  background: var(--card);
}

table.items-editor .sl-no {
  text-align: center;
  color: var(--text-2);
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  width: 54px;
}

.col-qty-cell { width: 110px; }
.col-ctn-cell { width: 150px; }
.col-act-cell { width: 46px; }

table.items-editor input[name="quantity"],
table.items-editor input[name="cartons"] {
  text-align: center;
  font-variant-numeric: tabular-nums;
}

/* Cartons depend on how the goods are actually packed — one fan can be two
   cartons, several spares can share a box — so the operator always types them.
   Tinted to read as their column, never as something the app filled. */
/* The cartons column is tinted because it is the one the operator is expected
   to touch. Brass on the old warm palette; on slate that read as a stain, so
   it is a neutral tint now — still visibly the odd column out, without
   introducing a second accent colour to the screen. */
table.items-editor input[name="cartons"] { background: var(--head-bg); }
table.items-editor input[name="cartons"]:focus { background: var(--card); }

.col-note {
  display: block;
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: .06em;
  color: var(--accent);
  text-transform: uppercase;
}

.remove-row {
  width: 26px; height: 26px;
  padding: 0;
  margin: 0 auto;
  border: none;
  background: none;
  border-radius: 50%;
  color: var(--muted);
  font-size: 17px;
  line-height: 1;
}
.remove-row:hover { background: var(--danger-bg); color: var(--danger); }

.item-actions {
  display: flex;
  gap: 12px;
  align-items: center;
  margin-top: 12px;
  flex-wrap: wrap;
}

#row-count { font-size: 13px; color: var(--text-2); }
#row-count.over-limit { color: var(--danger); font-weight: 600; }

/* ------------------------------------------------------------ lists/table */

table.list tbody tr:hover { background: var(--hover); }

/* A duplicate document number. Amber, not red: the draft is fine and can be
   issued — it just should not be issued without somebody looking. Red is
   reserved for the rows that genuinely cannot go. */
table.list tbody tr.is-duplicate {
  background: var(--warn-bg);
  box-shadow: inset 3px 0 0 var(--warn);
}

table.list tbody tr.is-duplicate:hover { background: var(--warn-bg); }

.duplicate-note {
  margin-top: 3px;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto,
               Helvetica, Arial, sans-serif;
  font-size: 12px;
  font-weight: 500;
  line-height: 1.35;
  letter-spacing: 0;
  white-space: normal;
  color: var(--warn);
}

/* Identifiers are read digit by digit, compared against a paper invoice and
   typed into other systems, so they get a monospace face where 0/O and 1/l are
   not the same shape, and full-strength ink. */
table.list .serial-cell,
table.list .invoice-cell {
  font-family: var(--mono);
  font-weight: 600;
  color: var(--ink);
  font-size: 13px;
  letter-spacing: -.01em;
  /* Monospace is wider than the body face, so "FR 262702188" broke over two
     lines in the narrower Drafts column. A reference number split across lines
     is one that gets misread back to somebody over the phone.
     nowrap on its own was not safe: a cell that cannot be made narrower than
     its text forces a fixed-layout table past its declared width, so the same
     page scrolled sideways in Firefox and not in Chromium, purely on how wide
     each renders the monospace face. The column is given room below instead,
     and this yields rather than overflowing. */
  white-space: nowrap;
  min-width: 0;
  overflow-wrap: normal;
}

/* Actions read as things you press, not as prose links. */
.actions-inner a,
.actions-cell > a,
.actions-inner button.link-button {
  display: inline-block;
  padding: 5px 11px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--card);
  font-size: 12.5px;
  font-weight: 600;
  line-height: 1.4;
  color: var(--ink-2);
  text-decoration: none;
  transition: background .15s, border-color .15s, color .15s;
}

.actions-inner a:hover,
.actions-cell > a:hover,
.actions-inner button.link-button:hover {
  background: var(--head-bg);
  border-color: var(--muted);
  color: var(--ink);
  text-decoration: none;
}

.actions-inner button.link-button:hover { color: var(--danger); border-color: var(--danger); }

/* The ✕ that removes a draft. Deliberately not styled like the buttons beside
   it: it is the only destructive thing on the row and it sits next to Open, so
   it reads as an icon rather than as a third equal choice, and it takes its red
   only on hover. */
.actions-inner button.draft-delete {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--muted);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
  transition: background .15s, border-color .15s, color .15s;
}
.actions-inner button.draft-delete:hover {
  background: var(--danger-bg);
  border-color: var(--danger);
  color: var(--danger);
}
.actions-inner button.draft-delete:disabled { opacity: .5; cursor: default; }

/* On a row that is asking to have one of a pair removed, the ✕ is the answer to
   the question the row is posing — so it stops being quiet. */
tr.needs-attention .actions-inner button.draft-delete,
tr.is-duplicate .actions-inner button.draft-delete {
  border-color: var(--danger);
  color: var(--danger);
}

/* A row on its way out. It is removed from the DOM 150ms later; this is only
   so the removal reads as the row leaving rather than the table jumping. */
table.list tbody tr.removing {
  opacity: 0;
  transition: opacity .15s ease-out;
}
@media (prefers-reduced-motion: reduce) {
  table.list tbody tr.removing { transition: none; }
}

/* Where a row's carton number came from. Small and quiet — it is an
   explanation, not a field. */
.items-editor .carton-note {
  font-size: 11.5px;
  margin-top: 3px;
  min-height: 14px;
}

/* ------------------------------------------------- fan & carton master */
table.list.fans-table th:nth-child(1),
table.list.fans-table td:nth-child(1) { width: 46%; }
table.list.fans-table th:nth-child(2),
table.list.fans-table td:nth-child(2) { width: 16%; }
table.list.fans-table th:nth-child(3),
table.list.fans-table td:nth-child(3) { width: 22%; }
table.list.fans-table th:nth-child(4),
table.list.fans-table td:nth-child(4) { width: 16%; }
.col-centre { text-align: center; }
table.list.fans-table td.fan-cartons { font-weight: 700; }
table.list.fans-table td input {
  width: 100%;
  padding: 5px 8px;
  font-size: 13.5px;
}

.add-panel { margin-bottom: 16px; }
.add-row {
  display: flex;
  align-items: flex-end;
  gap: 12px;
  flex-wrap: wrap;
}
.add-row .field { flex: 1; min-width: 220px; margin: 0; }
.add-row .field-narrow { flex: 0 0 150px; min-width: 150px; }

.actions-inner button.row-edit,
.actions-inner button.row-save,
.actions-inner button.row-cancel {
  padding: 5px 11px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--card);
  font-size: 12.5px;
  font-weight: 600;
  color: var(--ink-2);
  cursor: pointer;
}
.actions-inner button.row-save {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--card);
}
.actions-inner button.row-delete {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 26px; height: 26px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--muted);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
}
.actions-inner button.row-delete:hover {
  background: var(--danger-bg);
  border-color: var(--danger);
  color: var(--danger);
}

/* The audit history on a pass. Collapsed when there is only the issue entry,
   open once something has actually been changed — the whole reason to look. */
details.history summary {
  cursor: pointer;
  font-weight: 600;
  font-size: 13.5px;
  color: var(--ink-2);
}
.history-list { margin: 12px 0 0; padding-left: 20px; }
.history-list li { margin-bottom: 8px; font-size: 13px; line-height: 1.5; }
.history-when { color: var(--text-2); font-variant-numeric: tabular-nums; margin-right: 6px; }
.history-detail { color: var(--ink-2); }

/* --------------------------------- the review screen's document panel */
/* Form and original side by side, so an extraction error can be seen against
   the document it came from. Below 1100px they stack instead: two half-width
   columns are worse than one of each, and the item table needs its width. */
.review-split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.85fr);
  gap: 20px;
  align-items: start;
}
.review-split.document-hidden { grid-template-columns: minmax(0, 1fr); }

@media (max-width: 1100px) {
  .review-split { grid-template-columns: minmax(0, 1fr); }
}

.review-document { padding: 0; overflow: hidden; position: sticky; top: 16px; }
.document-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--line);
  background: var(--head-bg);
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--label);
}
.document-head a, .document-head button {
  font-size: 12px;
  text-transform: none;
  letter-spacing: 0;
  margin-left: 10px;
}
/* The rendered page. White behind it whatever the theme: this is a picture of
   a piece of paper, and paper is white — a dark surround would make the scan
   look like it had been photographed on a desk at night. It is also what makes
   every browser agree, which is the whole reason the page is rendered here
   rather than embedded as a PDF. */
.document-viewport {
  background: #ffffff;
  /* Tall enough to read a line off without scrolling for every field, short
     enough that the page still scrolls as one thing. */
  height: 78vh;
  min-height: 420px;
  overflow: auto;
  /* The image is a fixed pixel width; centre it if the panel is wider. */
  text-align: center;
}
.document-page {
  display: block;
  width: 100%;
  height: auto;
  /* Fit the width of the panel, which is what somebody comparing a line
     against a form actually wants — not a page zoomed to fit vertically and
     unreadable. */
  max-width: 100%;
}
.page-indicator {
  font-size: 12px;
  text-transform: none;
  letter-spacing: 0;
  color: var(--text-2);
  margin: 0 2px;
}
.document-head button:disabled { opacity: .35; cursor: default; }

@media (max-width: 1100px) {
  .review-document { position: static; }
  .document-viewport { height: 60vh; }
}

/* ------------------------------------------------------------------ toasts */
/* Confirmation for something that happened without leaving the page. The flash
   strip at the top of the layout only appears on a navigation, and the ✕
   deliberately does not navigate. */
/* The totals row inside the item editor. Under the column it adds up, so it
   can be checked against the column rather than taken on trust. */
table.items-editor tfoot td {
  border-top: 2px solid var(--line);
  padding: 10px 12px;
  font-size: 14px;
  color: var(--ink);
}
table.items-editor tfoot .totals-label {
  text-align: right;
  font-weight: 600;
  color: var(--label);
  text-transform: uppercase;
  font-size: 12px;
  letter-spacing: .05em;
}
table.items-editor tfoot strong { font-weight: 700; }

.toast-tray {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.toast {
  padding: 10px 16px;
  border: 1px solid var(--line);
  border-left: 3px solid var(--muted);
  border-radius: var(--radius-sm);
  background: var(--card);
  box-shadow: 0 6px 20px rgba(15, 23, 42, .16);
  font-size: 13.5px;
  font-weight: 600;
  color: var(--ink-2);
  animation: toast-in .18s ease-out;
}
.toast.ok  { border-left-color: var(--ok); }
.toast.bad { border-left-color: var(--danger); }

@keyframes toast-in {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
/* Somebody who has asked for less movement gets the toast without the slide. */
@media (prefers-reduced-motion: reduce) {
  .toast { animation: none; }
}

/* Whether a pass has reached paper, shown on the button that does it rather
   than as a badge in the status column. The register is read by scanning down
   it, and a badge on every row that has not been printed yet — which is every
   new pass, for the first minute of its life — reads as a column of warnings
   about nothing.

   Solid means the paper does not exist yet. Quiet means it does, and the button
   still works: a printed pass can always be printed again. */
/* --brand with --card text, the same pair every other solid button in here
   uses. Not --ink with a literal #fff: --ink is "primary TEXT", so in dark mode
   it flips to near-white and the button became white-on-white — invisible, and
   only visible as such by looking at it in dark mode. */
/* Unprinted: solid black, so the passes still owed their paper are the ones
   the eye finds first. Printed stays plain grey text with no box. */
.actions-inner a.print-action.not-printed.btn-print-register {
  background-color: #0f172a;
  color: #ffffff;
  border: 1px solid #0f172a;
  border-radius: 6px;
  padding: 6px 14px;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s ease;
}
.actions-inner a.print-action.not-printed.btn-print-register:hover {
  background-color: #1e293b;
  border-color: #1e293b;
  color: #ffffff;
}
/* On dark, #0f172a is the page itself and all but the card, so the button
   would lose its edges; inverted there like every other solid button. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .actions-inner a.print-action.not-printed.btn-print-register {
    background-color: var(--brand); color: var(--card); border-color: var(--brand);
  }
  :root:not([data-theme="light"]) .actions-inner a.print-action.not-printed.btn-print-register:hover {
    background-color: var(--brand-hover); border-color: var(--brand-hover); color: var(--card);
  }
}
:root[data-theme="dark"] .actions-inner a.print-action.not-printed.btn-print-register {
  background-color: var(--brand); color: var(--card); border-color: var(--brand);
}
.actions-inner a.print-action.is-printed {
  background: transparent;
  border-color: transparent;
  /* --label, not --muted. Muted is slate 400, which on this fill is about
     2:1 against the background — fine for a placeholder, too faint for a
     control that still works. */
  color: var(--text-2);
}
.actions-inner a.print-action.is-printed:hover {
  background: var(--head-bg);
  border-color: var(--muted);
  color: var(--ink-2);
}
table.list tr.cancelled td:not(.actions-cell) { color: var(--muted); }
table.list tr.cancelled .serial-cell { text-decoration: line-through; }

.serial-cell {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
  font-weight: 600;
  white-space: nowrap;
}

.pill {
  display: inline-block;
  padding: 3px 10px;
  border-radius: 9999px;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .03em;
  text-transform: uppercase;
  /* A wrapped badge renders as a lozenge with "ISSUE" over "D" in it, which is
     what happened the moment the action links became buttons and took width
     out of the status column. */
  white-space: nowrap;
}
.pill.issued, .pill.approved { background: var(--ok-bg); color: var(--ok); }
.pill.cancelled, .pill.rejected { background: var(--danger-bg); color: var(--danger); }
.pill.pending { background: var(--pill-warn-bg); color: var(--pill-warn); }
.pill.disabled { background: var(--line-soft); color: var(--muted); }
.pill.admin { background: var(--accent-soft); color: var(--accent); }

.section-title,
.sticker-queue-head h2 {
  font-size: 16px;
  font-weight: 700;
  color: var(--ink);
  margin: 28px 0 10px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.section-title:first-of-type { margin-top: 18px; }

.count-badge {
  display: inline-grid;
  place-items: center;
  min-width: 18px;
  height: 18px;
  padding: 0 5px;
  margin-left: 6px;
  border-radius: 9999px;
  /* A soft red-tinted count, not a solid badge. */
  background: var(--count-bg);
  color: var(--count-fg);
  font-size: 11px;
  font-weight: 700;
}

.topbar nav a.active .count-badge { background: var(--card); color: var(--brand); }

.mono-cell {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 13px;
}

.you-tag {
  margin-left: 6px;
  font-size: 10.5px;
  font-weight: 600;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--text-2);
}

.empty.compact { padding: 22px; }

.actions-cell button { padding: 6px 12px; font-size: 13px; }
.actions-cell form + form { margin-left: 8px; }

/* No underline: these are buttons now, and this rule sits after the button
   styling so it was quietly putting the underline back on every Print and
   Open in the register. */
.actions-cell a { color: var(--ink-2); font-size: 13.5px; text-decoration: none; }
.actions-cell a:hover { text-decoration: none; }
.actions-cell > * + * { margin-left: 12px; }

.filters {
  display: flex;
  gap: 10px;
  margin-bottom: 18px;
  flex-wrap: wrap;
}
.filters input { flex: 1; min-width: 200px; }
.filters select { width: auto; min-width: 130px; }

.muted-cell { color: var(--text-2); font-size: 13px; white-space: nowrap; }

/* ------------------------------------------------------------ empty state */

.empty {
  text-align: center;
  padding: 52px 24px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.empty-icon { font-size: 28px; }
.empty-title { margin-top: 10px; font-weight: 600; }
.empty-sub { margin-top: 4px; font-size: 13.5px; color: var(--muted); }
.empty .button { margin-top: 18px; }

@media (max-width: 640px) {
  .topbar { padding: 0 14px; height: auto; flex-wrap: wrap; gap: 8px; padding-bottom: 10px; }
  .topbar nav { width: 100%; overflow-x: auto; }
  .container { padding: 20px 14px 60px; }
  .card { padding: 16px; }
}

/* Batch draft list */
.tick-cell { width: 38px; text-align: center; }
.tick-cell input { width: auto; accent-color: var(--accent); }
table.list tr.needs-attention { background: var(--warn-bg); }
table.list tr.needs-attention:hover { background: var(--warn-bg); }
button[disabled] { opacity: .5; cursor: not-allowed; }

/* ------------------------------------------------- batch file picker */

.file-panel-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 16px 0 8px;
}

.file-count { font-size: 14px; font-weight: 600; }

.file-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 260px;
  overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--card);
}

.file-row {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 12px;
  font-size: 13.5px;
  border-bottom: 1px solid var(--line-soft);
}
.file-row:last-child { border-bottom: none; }

.file-name {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.file-size { color: var(--text-2); font-size: 12.5px; font-variant-numeric: tabular-nums; }

.file-state {
  flex: none;
  width: 18px; height: 18px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  font-size: 11px;
  font-weight: 700;
  color: var(--muted);
  background: var(--line-soft);
}
.file-state[data-state="done"]      { background: var(--ok-bg);     color: var(--ok); }
.file-state[data-state="attention"] { background: var(--warn-bg);   color: var(--warn); }
.file-state[data-state="failed"]    { background: var(--danger-bg); color: var(--danger); }

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

/* Room above the track for the truck pin, which rides on top of it. */
.progress-wrap { margin-top: 16px; padding-top: 22px; }

.progress-track {
  position: relative;
  width: 100%;
  height: 8px;
  background-color: #f1f5f9;
  border-radius: 9999px;
  overflow: visible;          /* the pin rides above the track, not inside it */
}

.progress-fill {
  position: relative;         /* the pin is placed against the fill's front edge */
  height: 100%;
  width: 0;
  background: linear-gradient(90deg, #3b82f6, #2563eb);
  border-radius: 9999px;
  transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

/* A round pin centred on the fill's front edge. It lives INSIDE the fill,
   at right: 0, pulled half its width forward, so it is carried by the one
   width transition rather than animated separately -- the pin and the bar
   cannot drift apart. (The spec's left: calc(N% - 14px) on top of
   translateX(-50%) would have offset it twice, 14px behind the edge.) */
.truck-indicator-wrapper {
  position: absolute;
  top: -12px;
  right: 0;
  transform: translateX(50%);
  width: 28px;
  height: 28px;
  background: #ffffff;
  border: 1px solid #cbd5e1;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: 0 2px 6px rgba(37, 99, 235, 0.2);
  color: #2563eb;             /* the truck's stroke, through currentColor */
  z-index: 10;
  pointer-events: none;
}
.truck-svg { display: block; }
.truck-svg .truck-wheel { fill: #ffffff; }

/* A small engine bounce while invoices are being read; the drawn truck
   already faces the way the bar fills, so there is no flip. */
@keyframes driving-bounce {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-2px); }
}
.progress-track.is-driving .truck-svg {
  animation: driving-bounce 0.6s ease-in-out infinite;
}

/* On dark the white pin and wheels would glare against the dark page, so
   they take the card colour and the truck a lighter blue. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .progress-track { background-color: var(--line); }
  :root:not([data-theme="light"]) .truck-indicator-wrapper {
    background: var(--card); border-color: var(--line); color: #60a5fa;
  }
  :root:not([data-theme="light"]) .truck-svg .truck-wheel { fill: var(--card); }
}
:root[data-theme="dark"] .progress-track { background-color: var(--line); }
:root[data-theme="dark"] .truck-indicator-wrapper {
  background: var(--card); border-color: var(--line); color: #60a5fa;
}
:root[data-theme="dark"] .truck-svg .truck-wheel { fill: var(--card); }

/* Somebody who has asked for less motion gets a pin that moves along the
   bar as the work is done -- that is information -- but does not bounce. */
@media (prefers-reduced-motion: reduce) {
  .progress-fill { transition: none; }
  .progress-track.is-driving .truck-svg { animation: none; }
}

.progress-labels {
  display: flex;
  justify-content: space-between;
  margin-top: 6px;
  font-size: 13px;
  color: var(--text-2);
}

#progress-percent { font-variant-numeric: tabular-nums; font-weight: 600; color: var(--ink); }

/* ----------------------------------------------------- date filters */

.filter-bar {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 14px 16px;
  margin-bottom: 18px;
}

.quick-ranges {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}

.quick-range {
  padding: 5px 12px;
  border-radius: 999px;
  border: 1px solid var(--line);
  font-size: 13px;
  text-decoration: none;
  color: var(--ink-2);
  background: var(--card);
  white-space: nowrap;
}
.quick-range:hover { background: var(--line-soft); }
.quick-range.on {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--card);
  font-weight: 600;
}

.filter-row {
  display: flex;
  gap: 10px;
  align-items: flex-end;
  flex-wrap: wrap;
}
.filter-row .grow { flex: 1; min-width: 190px; }
.filter-row label { margin-bottom: 4px; }
.filter-row input[type="date"] {
  padding: 8px 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font: inherit;
  font-size: 14px;
  color: var(--ink);
  background: var(--card);
}

.export-bar {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
  margin-top: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--line-soft);
}

/* ------------------------------------------- register toolbar + filter panel */

.toolbar {
  display: flex;
  gap: 10px;
  align-items: center;
  margin-bottom: 12px;
  flex-wrap: wrap;
}

.toolbar-search {
  flex: 1;
  min-width: 220px;
}

.filter-toggle {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  white-space: nowrap;
}
.filter-toggle[aria-expanded="true"] {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent);
}
.filter-toggle .count-badge { background: var(--accent); }

.filter-panel {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  padding: 14px 16px;
  margin-bottom: 18px;
}
.filter-panel[hidden] { display: none; }

/* ------------------------------------------------------------ reports page */

.quick-ranges label.quick-range { cursor: pointer; user-select: none; }

button.big { padding: 11px 22px; font-size: 15px; }
button.secondary { background: var(--card); }

#match-count { margin-left: auto; }

/* A long invoice is information, not a failure — it still becomes one gate
   pass, just printed over more pages. */
/* Informational, not a warning: the plain .notice is amber and this must not
   look like one. Brass on the old warm palette; slate-blue on this one. */
.notice.info {
  background: var(--blue-soft);
  border-color: #bfdbfe;
  color: #1d4ed8;
}

/* Setting a password needs two matching entries, so it gets a small dialog
   rather than a prompt(), which would show the password in clear. */
.pw-dialog {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 22px;
  max-width: 380px;
  width: 90%;
  background: var(--card);
  color: var(--ink);
  box-shadow: var(--shadow-lift);
}
.pw-dialog::backdrop { background: rgba(20, 20, 26, .45); }

/* Feature permissions — a collapsible block on the create form, and the same
   grid inside the edit dialog. */
.perm-block {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  margin-bottom: 16px;
}
.perm-block summary { cursor: pointer; font-size: 14px; font-weight: 600; }
.perm-block summary .hint { font-weight: 400; }

.perm-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 4px 16px;
  margin: 10px 0;
}
.perm-item { margin: 4px 0; font-size: 13.5px; }

/* Register pages. Wraps on a narrow screen rather than pushing the table into
   a horizontal scroll: the count goes on its own line and the buttons stay
   side by side, because they are only useful next to each other. */
.pager {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 16px;
  margin: 16px 0 4px;
}

.pager-count { color: var(--ink-2); font-size: 13.5px; margin: 0; }
.pager-count strong { color: var(--ink); font-weight: 600; }

/* These wrap too, not just the bar around them. Five nowrap buttons come to
   385px, which is wider than a 375px phone — without this the whole page
   scrolls sideways to reach Last. */
.pager-links, .pager-size {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
}
/* Pushes the page buttons right on a wide screen and lets them fall under the
   count on a narrow one, without a media query. */
.pager-links { margin-left: auto; }

.pager-page {
  font-size: 13.5px;
  color: var(--ink-2);
  padding: 0 10px;
  white-space: nowrap;
}

/* --label, not --muted: --muted is placeholder grey, which measured 3.96:1 on
   the dark card and fails AA for text this size. This is a real label. */
.pager-size-label { font-size: 13px; color: var(--label); margin-right: 2px; }
/* nowrap so "« First" and "Next ›" keep their arrow on the same line as the
   word — on a phone they otherwise break in two and the row doubles in height
   for no gain. The ROW wraps instead; the labels do not. */
.pager .button { padding: 7px 12px; font-size: 13.5px; white-space: nowrap; }

/* Rendered as spans, so these never take hover, focus or a click — the style
   only has to say so. */
.pager .button.is-disabled {
  color: var(--muted);
  border-color: var(--line-soft);
  cursor: default;
  pointer-events: none;
}

/* --brand/--card rather than --ink: --ink is primary text, which in dark mode
   is near-white, and a near-white background under white text is invisible. */
.pager .button.is-current {
  background: var(--brand);
  border-color: var(--brand);
  color: var(--card);
  font-weight: 600;
  cursor: default;
}

/* Bulk selection bar — floats above the register once something is ticked. */
.bulk-bar {
  position: sticky;
  bottom: 16px;
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 16px;
  padding: 12px 16px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: var(--card);
  box-shadow: var(--shadow-lift);
}
.bulk-bar[hidden] { display: none; }
.bulk-count { font-size: 14px; font-weight: 600; }
.bulk-count.over-limit { color: var(--danger); }
.bulk-spacer { flex: 1; }

/* --------------------------------------------------- register table layout */

/* The register is nine columns wide; the reading width the other pages use
   squeezes the last one until "Cancel" has nowhere to go. */
.container.container-wide { max-width: 1300px; }

/* Fixed layout so the columns keep their proportions whatever the data says.
   With `auto`, one long customer name takes the space the actions column needs
   and the buttons get pushed off the edge. */
table.list {
  width: 100%;
  table-layout: fixed;
  border-collapse: collapse;
}

/* Text columns wrap rather than being cut off — a register is for reading, so a
   long customer name should take a second line, not disappear behind an
   ellipsis. */
table.list td {
  overflow-wrap: anywhere;
  word-break: normal;
}

/* Adds to 100. Status is back to one badge — the print state moved onto the
   Print button — so the width it was given for a second one goes back to the
   columns holding names, which are the ones that wrap. */
.reg-tick     { width: 4%;  text-align: center; }
.reg-serial   { width: 10%; }
.reg-issued   { width: 12%; }
.reg-by       { width: 10%; }
.reg-from     { width: 12%; }
.reg-customer { width: 14%; }
.reg-invoice  { width: 11%; }
/* 12, not the 9 it had for years: "CANCELLED" plus the cell's padding needs
   about 117px, and 9% of the table at a 1100px window is 92. It ran past the
   column the whole time and nothing was looking at the right thing. */
.reg-status   { width: 12%; }
/* 11%, not 8%: "Print" + "Cancel" plus the cell padding measures about 130px,
   which is 10.6% of the table. At 8% they would overflow the cell — the very
   clipping this layout is meant to prevent. */
.reg-actions  { width: 15%; }

/* Keep the two actions on one line, hard against the right edge.
   The flex row is the inner div: putting display:flex on the <td> itself takes
   it out of the table layout, and `table-layout: fixed` then cannot give the
   column its width — measured, it collapsed from 11% to 1.9%. */
td.actions-cell { white-space: nowrap; }
.actions-inner {
  display: flex;
  gap: 8px;
  justify-content: flex-end;
  align-items: center;
  white-space: nowrap;
}
.actions-inner > * + * { margin-left: 0; }
.actions-inner form { display: inline-flex; margin: 0; }

/* Nothing between the table and the page may clip the last column. The wrapper
   scrolls rather than hides, so on a narrow screen the column is still
   reachable instead of being cut off. */
.table-wrap { overflow-x: auto; overflow-y: visible; }

/* ------------------------------------------------------------ people page */

.add-person { padding: 0; }
.add-person > summary { list-style: none; }
.add-person > summary::-webkit-details-marker { display: none; }

.add-person-summary {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 16px 22px;
  cursor: pointer;
  font-size: 15px;
  font-weight: 600;
  border-radius: var(--radius);
}
.add-person-summary:hover { background: var(--line-soft); }
.add-person[open] > .add-person-summary {
  border-bottom: 1px solid var(--line);
  border-radius: var(--radius) var(--radius) 0 0;
}
.add-person > form { padding: 20px 22px 22px; }

/* What someone can actually do, spelled out rather than left as a count. */
.perm-chips {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 5px;
}
.chip {
  font-size: 10.5px;
  padding: 2px 7px;
  border-radius: 999px;
  background: var(--line-soft);
  color: var(--ink-2);
  white-space: nowrap;
}

/* ---------------------------------------------------------- settings page */

/* Restarting the numbering is not an everyday setting; it should not look like
   one sitting next to "show totals". */
.danger-zone { border-color: var(--danger); }
.danger-zone-head {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--danger);
  margin-bottom: 10px;
}

/* ------------------------------------------------- people accounts table */

table.people-table { table-layout: fixed; }

table.people-table td,
table.people-table th { padding: 10px 12px !important; }

.ppl-name     { width: 22%; }
.ppl-username { width: 16%; }
.ppl-perms    { width: 26%; }
.ppl-status   { width: 12%; }
.ppl-actions  { width: 24%; text-align: right; }

/* One tidy row of small buttons, hard against the right edge. Labels are kept
   short on purpose — "Edit permissions / New password / Switch off" measured
   312px against the 300px this column gives, so they wrapped onto a second
   line. Shortening them was cheaper than widening the column or hiding the
   actions behind a menu. */
.user-actions-cell {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 6px;
  white-space: nowrap;
}
.user-actions-cell form { display: inline-flex; margin: 0; }

.btn-action-sm {
  padding: 4px 8px;
  font-size: 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--ink);
  cursor: pointer;
  white-space: nowrap;
}
.btn-action-sm:hover { background: var(--line-soft); }
.btn-action-sm.danger { color: var(--danger); }
.btn-action-sm.danger:hover { background: var(--danger-bg); border-color: var(--danger); }
.btn-action-sm.primary {
  background: var(--brand); border-color: var(--brand); color: var(--card); font-weight: 600;
}

/* Badges sit tight against the top of their cell rather than floating in it. */
.ppl-status, .ppl-perms { vertical-align: middle; }
.ppl-perms .pill { margin-bottom: 2px; }

/* ----------------------------------------------------- reports page panel */

/* Every control reads down the left edge: label above input, rows wrapping
   left to right, nothing centred and nothing stretched to the full width. */
.report-panel { text-align: left; }

.control-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 12px 16px;
  margin-bottom: 4px;
}

.control { display: flex; flex-direction: column; }
.control input,
.control select { width: 190px; }
.control-wide input { width: 240px; }
.control-note { padding-bottom: 9px; }

.option-set {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 10px 14px 12px;
  margin: 0;
  min-width: 260px;
}
.option-set legend {
  padding: 0 6px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .05em;
  text-transform: uppercase;
  color: var(--text-2);
}
.option-set .checkbox { margin: 6px 0; font-size: 13.5px; }

.report-actions {
  display: flex;
  justify-content: flex-start;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 22px;
  padding-top: 18px;
  border-top: 1px solid var(--line-soft);
}

/* A count beside a heading, not a status badge — it should not read as a
   warning the way the pending-approvals badge does. */
.count-badge.neutral {
  background: var(--line-soft);
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}


/* A value read off the invoice, on the review screen. Shown as text rather
   than a disabled input: a greyed-out box invites people to click into it and
   wonder why it will not take. This reads as "this is what the document says",
   which is what it is. The dotted underline marks it as a value, not a label. */
.read-field {
  margin: 0;
  padding: 9px 0 8px;
  font-size: 15px;
  font-weight: 600;
  color: #17171a;
  border-bottom: 1px dotted #d9d5cd;
}

.read-cell {
  display: inline-block;
  padding: 6px 2px;
  font-size: 14px;
  color: #17171a;
}


/* Accounts heading with its action on the right. */
.section-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin: 26px 0 12px;
}

/* Creating an account: four fields, then the permission grid. Wider than the
   password dialog because a two-column tick list needs the room. */
.add-dialog { max-width: 640px; }

.dialog-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px 16px;
  margin-bottom: 20px;
}

.perm-heading {
  margin: 0 0 10px;
  font-size: 13px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .06em;
  color: #45454e;
}

/* Two columns of tick boxes, each with its line of explanation underneath. */
.perm-grid-2 {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px 20px;
}

.perm-grid-2 .perm-item { align-items: flex-start; }

.perm-hint {
  display: block;
  margin-top: 2px;
  font-size: 11.5px;
  font-weight: 400;
  line-height: 1.35;
  color: #7b7b88;
}

.dialog-actions {
  display: flex;
  gap: 10px;
  align-items: center;
  margin-top: 22px;
  padding-top: 16px;
  border-top: 1px solid #e9e5dd;
}

@media (max-width: 640px) {
  .dialog-grid, .perm-grid-2 { grid-template-columns: 1fr; }
}


/* ------------------------------------------------------------------ print */
/* Nothing on paper animates. The fan is frozen where it stands rather than
   hidden, so a printed page still carries the whole lockup. */
@media print {
  .fan-blade { animation: none !important; }
}

/* ===================================================================
   Box stickers  (/print-stickers)

   An OVERLAY for pre-printed SM Express stationery, not a design. The paper
   already carries "AWB No:", "ORIGIN:", "DESTINATION:", the SURFACE header
   band and the courier's footer. This contributes three short strings and
   nothing else.

   EVERY NUMBER BELOW IS MEASURED, NOT CHOSEN
   -------------------------------------------------------------------
   From a scan of the real sheet, using its known 164mm width to turn pixels
   into millimetres. Distances are from the top of the sheet:

       4.9mm   top border of the first label
      20.6mm   header band ends, AWB No row begins
      36.3mm   AWB row ends, ORIGIN begins
      52.0mm   ORIGIN ends, DESTINATION begins
      70.6mm   DESTINATION ends, footer band begins
      76.9mm   footer ends

   The three labels repeat every 82.4mm by measurement; 82mm is used here
   because that is the stated size, and the 0.4mm difference comes to 0.8mm
   by the third label — a fifth of the slack in a 15.7mm row.

   Row centres are therefore 28.5, 44.2 and 61.3mm. A single spacing cannot
   hit all three because the DESTINATION row is taller than the other two
   (18.6mm against 15.7mm), so 16.4mm is used: it lands line two 0.7mm above
   its row centre and line three exactly on it.

   The pre-printed labels end at 17mm (AWB No:), 24mm (ORIGIN:) and 38mm
   (DESTINATION:), so values start at 44mm and clear the longest by 6mm.

   None of this is the last word. No two printers agree on where the paper
   starts, so these are only the STARTING values for the three calibration
   boxes on the page, which move the whole block and are remembered per
   browser.
   =================================================================== */

.sticker-fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 14px 18px;
}

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

/* The calibration boxes. Quieter than the fields above, because they are set
   once when the printer is first lined up and then left alone. */
/* The receiver box and its Manage button share a line, so the button reads
   as belonging to that field rather than to the form. */
/* The text box and its caret are one control; the gear is a separate tool
   beside it. */
.receiver-field { display: flex; gap: 6px; align-items: stretch; }
.receiver-input { position: relative; flex: 1 1 auto; min-width: 0; }
.receiver-input input { width: 100%; padding-right: 30px; }

/* Qualified by the field: .icon-button sizes itself for a table cell (26px)
   and is declared further down the sheet, so an unqualified rule here lost
   and the gear sat as a 26px button against a 42px input. */
.receiver-field .receiver-manage {
  flex: 0 0 auto;
  width: 38px;
  height: auto;          /* stretched by the row, so it tracks the input */
  color: var(--label);
}
.receiver-field .receiver-manage:hover { color: var(--ink); }
.receiver-field .receiver-manage[aria-expanded="true"] {
  border-color: var(--line);
  background: var(--head-bg);
  color: var(--ink);
}

/* --- the manage panel -------------------------------------------------- */
/* Anchored under the field, not in the flow: see the note in the template.
   Same card, border, radius and shadow as the pick list, because they are
   two views of one list and should look like it. */
.receiver-list {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  z-index: 31;
  padding: 10px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: var(--card);
  box-shadow: var(--pop-shadow);
}

.receiver-list-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin: 0 0 8px;
  padding: 0 0 8px;
  border-bottom: 1px solid var(--line);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--label);
}
.receiver-list-head .icon-button {
  width: 24px;
  height: 24px;
  font-size: 17px;
  line-height: 1;
}

.receiver-list ul {
  list-style: none;
  margin: 0 0 10px;
  padding: 0;
  /* Long lists scroll inside the panel rather than growing it off-screen. */
  max-height: 168px;
  overflow-y: auto;
}

.receiver-list li {
  display: flex;
  align-items: center;
  gap: 8px;
  /* Each name is a row with its own hover, so it is obvious which one the
     delete button belongs to. Unpadded rows with a distant x made it a
     guess. */
  padding: 5px 6px;
  border-radius: var(--radius-sm);
  font-size: 13.5px;
}
.receiver-list li:hover { background: var(--head-bg); }
.receiver-list li span { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; }
.receiver-list .receiver-empty { color: var(--muted); }
.receiver-list .receiver-empty:hover { background: none; }

.receiver-remove {
  flex: 0 0 auto;
  width: 24px;
  height: 24px;
  padding: 0;
  line-height: 1;
  font-size: 16px;
  border-radius: 50%;
  border-color: transparent;
  background: none;
  color: var(--muted);
  /* Dimmed rather than hidden. Removing a name is what this panel is FOR, so
     a list with no visible affordance until the mouse happens to cross a row
     is hiding the feature; five crosses at full strength beside five names
     read as five errors. Dim, then solid on the row you are pointing at. */
  opacity: 0.5;
}
.receiver-list li:hover .receiver-remove,
.receiver-remove:focus-visible { opacity: 1; }
.receiver-remove:hover {
  background: var(--danger-bg);
  border-color: transparent;
  color: var(--danger);
}

.receiver-add { display: flex; gap: 6px; }
.receiver-add input { flex: 1 1 auto; min-width: 0; }
.receiver-add button { flex: 0 0 auto; white-space: nowrap; }
.receiver-list .hint { margin: 8px 0 0; font-size: 12px; }

.sticker-align {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 12px 16px;
  margin-top: 18px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
}

/* The one thing on this page that must be read. A scale error cannot be
   corrected by any of the offsets below it, so it is set apart from them
   rather than being another line of hint text. */
.sticker-warning {
  margin: 18px 0 0;
  padding: 11px 14px;
  border: 1px solid #f0c987;
  border-left: 3px solid #d98c1f;
  border-radius: var(--radius-sm);
  background: #fdf7ec;
  color: #7a4d06;
  font-size: 13.5px;
  line-height: 1.5;
}

@media (prefers-color-scheme: dark) {
  .sticker-warning {
    border-color: #6b4f1d;
    border-left-color: #d98c1f;
    background: #251d0d;
    color: #f0d9a8;
  }
}

.sticker-align label { display: block; }
.sticker-align select { min-width: 190px; }
.sticker-align input[type="number"] { width: 92px; }
.sticker-align .hint { flex: 1 1 220px; }

.sticker-sheet { margin-top: 18px; }

/* One physical sheet: 164 x 247mm, three labels of 82mm. */
.sticker-page {
  /* Every number here is the document's, measured out of the file that
     prints correctly, and NOT adjusted towards what looked better against a
     scan. Comparing the two line by line, the earlier values were short at
     every step — 3.06mm on the first line, 0.26mm on each line gap, 0.68mm
     on each label — and short errors in the same direction accumulate, so by
     the last line of the third label the page was 6.5mm high.

         first line   19.40mm from the top of the page (4.09mm page margin
                      plus the block's own 15.31mm)
         line gap     16.66mm, the mean of the document's six
         label pitch  82.68mm, the mean of its two
         left         36mm, its blocks at 35.95, 35.95 and 36.18mm

     The document places its three labels by hand, so its own spacings vary
     by a couple of millimetres either way. Means are used rather than
     copying that variation, which is not a measurement of anything.

     The left and first-line figures are no longer the document's 36 and
     19.4mm but 37 and 21.6mm, which is where the sheet actually lines up on
     the office printer. They were arrived at in front of it, against a real
     sticker, which beats any measurement taken from a file. */
  --sticker-left: 37mm;
  --sticker-top: 21.6mm;
  --sticker-gap: 16.66mm;
  /* Top of one label to the top of the next. Measured at 82mm on the sheet,
     and adjustable for the same reason everything else here is: if a printer
     enlarges the page, the labels come out further apart than they were sent
     and the drift compounds — the second label is out by one gap, the third
     by two. Shrinking the pitch cancels that without disturbing the first
     label, which by then is usually already lined up. */
  --sticker-pitch: 82.7mm;
  /* Cancels whatever the printer does to the page. 1 means it was left
     alone. See .sticker-ruler below for how the number is arrived at. */
  --sheet-scale: 1;

  transform: scale(var(--sheet-scale));
  /* From the corner the paper starts at, so the correction moves every value
     by the same proportion of its distance from that corner — which is
     exactly how the printer's own scaling went wrong. */
  transform-origin: top left;

  width: 163mm;
  height: 248mm;
  margin: 0 auto;
  display: grid;
  grid-template-columns: 1fr;
  /* Two rows at the pitch, and the third takes whatever is left.
     Three rows at the pitch came to 248.1mm in a 248mm page — over by a
     tenth of a millimetre, which is nothing on paper and everything to a
     print engine: the third label was pushed onto a sheet of its own and one
     consignment printed across two. The space below the last label is empty,
     so letting it be the remainder costs nothing and cannot overflow. */
  grid-template-rows: repeat(2, var(--sticker-pitch)) 1fr;
  /* And if a pitch is ever set wide enough that the third label genuinely
     does not fit, it is clipped rather than fragmenting the sheet. */
  overflow: hidden;
  align-content: start;
}

/* One label's worth of the sheet. It draws nothing on paper — the stationery
   already has the artwork — it only gives the three values somewhere to be. */
.sticker-card {
  position: relative;
  width: 163mm;
  /* Filling its grid row rather than naming a height, so the last card is
     the remainder of the page and the first two are exactly the pitch. */
  height: 100%;
  box-sizing: border-box;
  /* NOT overflow:hidden. A shortened pitch leaves the destination line close
     to the bottom of its own box, and clipping it there would hide the very
     thing the pitch is being adjusted to fix. */
  color: #000;
}

/* Absolute, so a long destination cannot push the line below it out of its
   blank and onto a printed rule. Each line stays where the paper expects it
   whatever it contains. */
/* Centred in a 110mm box, not left-aligned against the printed label — this
   is what the document that printed correctly does, and the reason is legible
   on the sheet: the blank is 120mm of empty row, and text pushed hard against
   "DESTINATION:" reads as a caption to it rather than as the destination.
   Centred, the value sits in the middle of its own space.

   Absolute, so a long destination cannot push the line below it out of its
   blank and onto a printed rule. Each line stays where the paper expects it
   whatever it contains. */
.sticker-card div {
  position: absolute;
  left: var(--sticker-left);
  width: 110mm;
  /* Regular Arial, at normal weight -- NOT bold, NOT Arial Black. Arial
     Black is heavier than bold by design (it is the "black" weight of
     Arial), so it cannot be un-bolded; the requirement is that the values
     are not bold, whatever the face.

     Plain "Arial" is also the one family name every Windows browser
     resolves reliably. "Arial Black" was not: Windows files it inside the
     Arial family and Brave/Chrome silently printed the next name instead.
     Liberation Sans is Arial's metric twin on Linux -- the same advance
     widths -- so a sheet laid out on a Linux box fits exactly as it will on
     a Windows one. */
  font-family: Arial, "Liberation Sans", Helvetica, sans-serif;
  font-weight: 400;
  /* Nothing here may be thickened by the browser, whatever face it picks. */
  font-synthesis: none;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  line-height: 1.1;
  text-align: center;
  white-space: nowrap;
}

/* 29pt and 25pt, from the document. This is a label read off the side of a
   carton across a warehouse, not a form field — the earlier 17px was legible
   on screen and far too small on the box. The destination is the smaller of
   the two for the reason a longer line always is: it has twice as far to go. */
.sticker-lr   { top: var(--sticker-top); font-size: 29pt; }
.sticker-from { top: calc(var(--sticker-top) + var(--sticker-gap)); font-size: 29pt; }
.sticker-to   { top: calc(var(--sticker-top) + (2 * var(--sticker-gap)));
                font-size: 25pt; }

/* A printed 100mm rule, for measuring what the printer actually did.
   A driver set to fit the page to the paper enlarges everything, and the
   error grows with distance down the sheet: the first label is a few
   millimetres out and the third is thirty. No offset can correct that, and
   nothing on screen can reveal it — it happens after the page is handed over.
   So the page prints a line of known length, and whoever holds a ruler
   against it types back what they measured. */
.sticker-ruler {
  display: none;
  position: absolute;
  left: 0;
  top: 4mm;
  width: 100mm;
  border-top: 0.4mm solid #000;
  color: #000;
  font-size: 10px;
  font-weight: 700;
  padding-top: 1mm;
}

/* End stops, so it is measured between two marks rather than by eye. */
.sticker-ruler::before,
.sticker-ruler::after {
  content: "";
  position: absolute;
  top: -2.5mm;
  width: 0.4mm;
  height: 5mm;
  background: #000;
}
.sticker-ruler::before { left: 0; }
.sticker-ruler::after  { right: 0; }

/* What the sheet was drawn with, printed beside the rule. A photograph of a
   test page then says which numbers produced it, which is the one thing a
   photograph of a bad print could not previously tell anybody. */
.sticker-stamp {
  display: none;
  position: absolute;
  left: 0;
  top: 12mm;
  font-size: 8px;
  color: #000;
}

/* Shown only while a test page is being printed, so neither lands on real
   stationery. */
body.is-test-print .sticker-ruler,
body.is-test-print .sticker-stamp { display: block; }

/* On screen only: the stationery drawn from the same measurements, so the
   preview shows which blank each value is aimed at and the calibration can
   be judged without printing a sheet. None of it reaches paper. */
@media screen {
  .sticker-page {
    background: #fff;
    border: 1px solid var(--line);
    border-radius: var(--radius);
    margin: 0 auto 14px;
    box-shadow: 0 1px 3px rgba(15, 23, 42, 0.08);
  }

  /* The label's own rules, at the measured offsets: the header band, the
     three row dividers and the footer band. */
  .sticker-card {
    background:
      linear-gradient(#e2e8f0, #e2e8f0) 0 4.9mm / 100% 0.3mm no-repeat,
      linear-gradient(#eef2f7, #eef2f7) 0 4.9mm / 100% 15.7mm no-repeat,
      linear-gradient(#e2e8f0, #e2e8f0) 0 20.6mm / 100% 0.3mm no-repeat,
      linear-gradient(#e2e8f0, #e2e8f0) 0 36.3mm / 100% 0.3mm no-repeat,
      linear-gradient(#e2e8f0, #e2e8f0) 0 52.0mm / 100% 0.3mm no-repeat,
      linear-gradient(#e2e8f0, #e2e8f0) 0 70.6mm / 100% 0.3mm no-repeat,
      linear-gradient(#eef2f7, #eef2f7) 0 70.6mm / 100% 6.3mm no-repeat;
  }

  /* The pre-printed labels, all three in ONE pseudo-element.
     They are fixed to the SHEET and must not move: they stand for ink that
     is already on the paper. Only the values move, which is the whole point
     of the preview — it shows where the calibration is putting them relative
     to blanks that are staying put.

     Previously two were pinned here and the third rode on its own value's
     line, so it drifted with the calibration and ended up printed on top of
     ORIGIN:. One element cannot disagree with itself.

     The rows are evenly pitched — 20.6, 36.3, 52.0mm, so 15.7mm apart — so
     a line-height of 15.7mm starting at the first row's top lands all three
     in their own rows without positioning each by hand. */
  .sticker-card::before {
    position: absolute;
    left: 4mm;
    top: 20.6mm;
    content: "AWB No:\A ORIGIN:\A DESTINATION:";
    white-space: pre;
    line-height: 15.7mm;
    font-size: 14px;
    font-weight: 700;
    color: #b6c0cd;
    pointer-events: none;
  }
}

@media print {
  /* Everything that is not a value. The form, the heading and the hints are
     inside .no-print already; the site's own chrome is not, and a nav bar
     across the top label is exactly the failure this rule exists for. */
  .no-print { display: none !important; }
  .topbar, .flashes { display: none !important; }

  /* main.container centres the page and caps its width for reading. On paper
     that would shrink the sheet and shift every value out of its blank, so it
     is undone rather than worked around. */
  body, main.container, .container-wide {
    margin: 0 !important;
    padding: 0 !important;
    max-width: none !important;
    width: auto !important;
    background: #fff !important;
  }

  .sticker-sheet { margin: 0; padding: 0; border: 0; background: #fff; }

  /* Top left, not centred. On A4 the sheet is smaller than the page, and
     where the stationery actually sits in the tray is what the Left and Top
     offsets are for — centring would add a margin they would then have to
     subtract. On the sheet size itself there is nothing to centre. */
  .sticker-page { margin: 0 !important; }

  /* No border, no shadow, no row rules, no ghost labels. The sheet in the
     printer already has every one of those on it; anything drawn here lands
     on top of the courier's own artwork. */
  .sticker-page {
    margin: 0;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    background: transparent;
    page-break-after: always;
    break-after: page;
  }

  /* ...except the last, which would otherwise eject a blank sheet. */
  .sticker-page:last-child {
    page-break-after: auto;
    break-after: auto;
  }

  .sticker-card {
    border: 0;
    background: none;
    /* A label split down the middle by a page break puts half a destination
       on one sheet and half on the next. Both spellings, because the older
       one is what some print engines still read. */
    page-break-inside: avoid;
    break-inside: avoid;
  }

  /* Clipped on paper only. A value too long for its blank must stop at the
     edge rather than run across the courier's pre-printed rule — but on
     screen the same rule also clipped the ghost labels drawn to the left of
     each line, which is where they belong. */
  .sticker-card div { overflow: hidden; }

  /* Restated for paper, by the name of the value rather than its position.
     A print stylesheet is a separate cascade in every engine, and this is
     the one place where being wrong is expensive: it is stationery that has
     already been bought and fed through the printer. !important because
     nothing later has any business overriding the courier's typeface.
     Sizes are deliberately absent -- they stay exactly as set for screen. */
  .sticker-card .awb-text,
  .sticker-card .origin-text,
  .sticker-card .dest-text {
    font-family: Arial, "Liberation Sans", Helvetica, sans-serif !important;
    font-weight: 400 !important;
    font-synthesis: none !important;
    font-synthesis-weight: none !important;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }

  .sticker-card::before { content: none; }
}

/* --- the sticker queue ------------------------------------------------- */
.sticker-queue { margin-top: 18px; }

.sticker-queue-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 4px;
}
.sticker-queue-head h2 { margin: 0; font-size: 16px; }

/* `table.list` sets table-layout: fixed and outranks a lone class, so the
   override needs the element name too — the drafts table does the same.
   Auto here because the destination is the only column that can be long,
   and it should give way rather than force a horizontal scrollbar. */
table.list.sticker-queue-table { table-layout: auto; }
.sticker-queue-table .q-num { width: 40px; }
.sticker-queue-table .q-qty { width: 70px; text-align: right; }
/* Two buttons live here now, and 80px clipped Delete to "Delet". */
.sticker-queue-table .q-act { text-align: right; }
.sticker-queue-table td, .sticker-queue-table th { white-space: nowrap; }

/* The destination is the one column that can be long, so it is the one that
   gives way rather than pushing the row into a horizontal scroll. */
.sticker-queue-table td:nth-child(4) {
  white-space: normal;
  overflow-wrap: anywhere;
}

.queue-remove { color: var(--danger); }
.queue-remove:hover { text-decoration: underline; }

.sticker-queue .hint { margin: 10px 0 0; }

/* --- the receiver dropdown -------------------------------------------- */
/* The field is the positioning context for the menu, which hangs under it. */
.receiver-field { position: relative; }

/* Inside the box, against its right edge, like a select's own arrow. The
   input reserves padding-right for it so a long name never runs underneath. */
.receiver-caret {
  position: absolute;
  top: 1px;
  right: 1px;
  bottom: 1px;
  width: 28px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 0;
  border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
  background: none;
  font-size: 12px;
  line-height: 1;
  color: var(--label);
}
.receiver-caret:hover { background: var(--head-bg); color: var(--ink); }

.receiver-menu {
  position: absolute;
  top: calc(100% + 4px);
  left: 0;
  right: 0;
  z-index: 30;
  margin: 0;
  padding: 4px;
  list-style: none;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  box-shadow: var(--pop-shadow);
  /* A long list scrolls inside the menu rather than running off the page. */
  max-height: 220px;
  overflow-y: auto;
}

.receiver-menu li {
  padding: 7px 9px;
  border-radius: var(--radius-sm);
  font-size: 13.5px;
  cursor: pointer;
  overflow-wrap: anywhere;
}
.receiver-menu li + li { margin-top: 1px; }
.receiver-menu li:hover,
.receiver-menu li.is-active { background: var(--head-bg); }

/* Keyboard and mouse agree: the highlighted row looks the same either way. */
.receiver-menu li.is-active { outline: 1px solid var(--line); }

/* The queue row the form is currently correcting. Without it, nothing says
   which of five rows the Save is about to overwrite. */
.sticker-queue-table tr.is-editing {
  background: var(--blue-soft);
  box-shadow: inset 3px 0 0 var(--blue);
}
.queue-edit { margin-right: 10px; }

/* --- sticker workspace polish ----------------------------------------- */
/* The form, the buttons and the queue are one task, so they are pulled
   together rather than sitting as three cards with a card's worth of air
   between them. This page is used dozens of times a day. */
.sticker-form { padding: 16px 18px; }
.sticker-fields { gap: 12px 16px; }
.sticker-actions { margin-top: 14px; gap: 12px; }

/* --- Add ---------------------------------------------------------------- */
/* Bigger than the site's default button. This row is used with one hand,
   often standing at the printer, dozens of times a morning -- it is not a
   settings form. */
.sticker-actions #generate {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  padding: 10px 24px;
  font-size: 15px;
  font-weight: 600;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: all 0.2s ease-in-out;
}

.sticker-actions #generate {
  background-color: #0f172a;
  color: #ffffff;
  border: 1px solid #0f172a;
}
.sticker-actions #generate:hover {
  background-color: #1e293b;
  border-color: #1e293b;
  transform: translateY(-1px);
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* #0f172a is very nearly the dark theme's own card colour -- 1.05:1 against
   it -- so on dark the button lost its edges entirely and only the white
   word "Add" floated there. The label stayed readable, which is why it is
   easy to miss: it does not look broken, it looks like text.

   Inverted here, which is what every other primary button on the site
   already does in dark mode. Light mode keeps the dark slate exactly. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .sticker-actions #generate {
    background-color: var(--ink);
    color: var(--card);
    border-color: var(--ink);
  }
  :root:not([data-theme="light"]) .sticker-actions #generate:hover {
    background-color: var(--ink-2);
    border-color: var(--ink-2);
  }
}
:root[data-theme="dark"] .sticker-actions #generate {
  background-color: var(--ink);
  color: var(--card);
  border-color: var(--ink);
}
:root[data-theme="dark"] .sticker-actions #generate:hover {
  background-color: var(--ink-2);
  border-color: var(--ink-2);
}

/* --- Clear and Print: the queue card's pair ----------------------------- */
/* The two actions that finish a batch, so they are one design in two
   colours: both ghosts -- a tint, a thin border, coloured text -- that fill
   solid only under the pointer. The colour arrives at the moment of intent
   rather than sitting on the card shouting, and neither button outranks the
   other until somebody reaches for it. Same padding, type, corners and
   minimum width, so they stay identical boxes. */
/* Equal columns, each as wide as the wider button. With icons and longer
   words ("Clear All", "Print All") a shared min-width stopped evening them
   out -- they came out 3px apart -- so the width is shared, not guessed. */
.queued-header-actions {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: 10px;
  align-items: center;
}
.queued-header-actions .btn-clear,
.queued-header-actions .btn-print {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 8px 16px;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.3px;
  border-radius: 8px;
  min-width: 80px;
  text-align: center;
  cursor: pointer;
  transition: background-color 0.2s ease-in-out, color 0.2s ease-in-out,
              border-color 0.2s ease-in-out, box-shadow 0.2s ease-in-out,
              transform 0.2s ease-in-out;
}

.queued-header-actions .btn-clear {
  background: #fef2f2;
  color: #ef4444;
  border: 1px solid #fecaca;
}
.queued-header-actions .btn-clear:hover {
  background: #dc2626;
  color: #ffffff;
  border-color: #dc2626;
  box-shadow: 0 4px 12px rgba(220, 38, 38, 0.2);
  transform: translateY(-1px);
}

/* Nothing queued. The card is hidden then, so this is rarely seen -- but a
   disabled button must still look disabled if it ever is. */
.queued-header-actions .btn-print {
  background: #e2e8f0;
  color: #94a3b8;
  border: 1px solid #e2e8f0;
  cursor: not-allowed;
}

/* Something queued: a green ghost, filling solid green under the pointer --
   Clear's pattern in Print's colour. The class is set with the disabled
   attribute in one place, setPrintReady(), so the two cannot disagree. */
.queued-header-actions .btn-print.btn-print-active {
  background: #f0fdf4;
  color: #16a34a;
  border-color: #bbf7d0;
  cursor: pointer;
}
.queued-header-actions .btn-print.btn-print-active:hover {
  background: #16a34a;
  color: #ffffff;
  border-color: #16a34a;
  box-shadow: 0 4px 10px rgba(22, 163, 74, 0.25);
  transform: translateY(-1px);
}
.queued-header-actions .btn-print.btn-print-active:active {
  background: #15803d;
  border-color: #15803d;
  transform: translateY(0);
}

/* On dark, the pale tints (#fef2f2, #f0fdf4) would be two glaring slabs on
   a dark card. Both ghosts keep their idea -- tinted, not solid -- as a
   translucent wash of their colour, with a lighter text shade that reads on
   it, and fill solid on hover as in light. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .queued-header-actions .btn-print.btn-print-active {
    background: rgba(22, 163, 74, 0.12); color: #4ade80;
    border-color: rgba(74, 222, 128, 0.35);
  }
  :root:not([data-theme="light"]) .queued-header-actions .btn-print.btn-print-active:hover {
    background: #16a34a; color: #ffffff; border-color: #16a34a;
  }
  :root:not([data-theme="light"]) .queued-header-actions .btn-clear {
    background: rgba(220, 38, 38, 0.12); color: #f87171;
    border-color: rgba(248, 113, 113, 0.35);
  }
  :root:not([data-theme="light"]) .queued-header-actions .btn-clear:hover {
    background: #dc2626; color: #ffffff; border-color: #dc2626;
  }
  :root:not([data-theme="light"]) .queued-header-actions .btn-print {
    background: var(--head-bg); color: var(--muted); border-color: var(--line);
  }
}
:root[data-theme="dark"] .queued-header-actions .btn-print.btn-print-active {
  background: rgba(22, 163, 74, 0.12); color: #4ade80;
  border-color: rgba(74, 222, 128, 0.35);
}
:root[data-theme="dark"] .queued-header-actions .btn-print.btn-print-active:hover {
  background: #16a34a; color: #ffffff; border-color: #16a34a;
}
:root[data-theme="dark"] .queued-header-actions .btn-clear {
  background: rgba(220, 38, 38, 0.12); color: #f87171;
  border-color: rgba(248, 113, 113, 0.35);
}
:root[data-theme="dark"] .queued-header-actions .btn-clear:hover {
  background: #dc2626; color: #ffffff; border-color: #dc2626;
}
:root[data-theme="dark"] .queued-header-actions .btn-print {
  background: var(--head-bg); color: var(--muted); border-color: var(--line);
}

/* --- the two cards and the queue table ---------------------------------- */
/* Scoped to this page. A hairline and a soft two-layer shadow instead of a
   plain border: the cards sit on the page rather than being drawn on it. */
.sticker-form.card,
.sticker-queue.card {
  border: 1px solid #e2e8f0;
  border-radius: var(--radius);
  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05), 0 1px 2px -1px rgba(0, 0, 0, 0.05);
}

.sticker-queue-table thead th {
  background-color: #f8fafc;
  color: #64748b;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  font-weight: 600;
}

/* On dark the light hex values would be a bright frame and a white header
   bar, so the same design is drawn in the theme's own dark tokens. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .sticker-form.card,
  :root:not([data-theme="light"]) .sticker-queue.card { border-color: var(--line); }
  :root:not([data-theme="light"]) .sticker-queue-table thead th {
    background-color: var(--head-bg); color: var(--label);
  }
}
:root[data-theme="dark"] .sticker-form.card,
:root[data-theme="dark"] .sticker-queue.card { border-color: var(--line); }
:root[data-theme="dark"] .sticker-queue-table thead th {
  background-color: var(--head-bg); color: var(--label);
}

/* Row icons get a soft square chip on hover. Red only on the cross: the
   pencil is not destructive, and turning it the same red would make
   "correct this row" and "throw this row away" look like one action. */
.sticker-queue-table .queue-print,
.sticker-queue-table .queue-edit,
.sticker-queue-table .queue-remove {
  border-radius: 4px;
  transition: background-color 0.15s ease-in-out, color 0.15s ease-in-out;
}
.sticker-queue-table .queue-remove:hover {
  background: #fee2e2;
  color: #991b1b;
  border-radius: 4px;
}
.sticker-queue-table .queue-edit:hover {
  background: #f1f5f9;
  color: #0f172a;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .sticker-queue-table .queue-remove:hover {
    background: rgba(220, 38, 38, 0.18); color: #fca5a5;
  }
  :root:not([data-theme="light"]) .sticker-queue-table .queue-edit:hover {
    background: var(--head-bg); color: var(--ink);
  }
}
:root[data-theme="dark"] .sticker-queue-table .queue-remove:hover {
  background: rgba(220, 38, 38, 0.18); color: #fca5a5;
}
:root[data-theme="dark"] .sticker-queue-table .queue-edit:hover {
  background: var(--head-bg); color: var(--ink);
}

/* Somebody who has asked not to be moved is not moved. */
@media (prefers-reduced-motion: reduce) {
  .sticker-actions #generate,
  .queued-header-actions .btn-clear,
  .queued-header-actions .btn-print {
    transition: none;
  }
  .sticker-actions #generate:hover,
  .queued-header-actions .btn-clear:hover,
  .queued-header-actions .btn-print.btn-print-active:hover {
    transform: none;
  }
}
.sticker-queue { margin-top: 12px; padding: 14px 18px; }
.sticker-queue-table td, .sticker-queue-table th { padding-top: 7px; padding-bottom: 7px; }

/* The live count sits against Add, because it answers "how much paper is
   this about to use" and that is asked with a hand on that button. */
.sticker-count {
  margin-left: 2px;
  font-variant-numeric: tabular-nums;
}

/* Compact, square, quiet until pointed at — a row of these should read as a
   column of marks, not as a second set of buttons competing with Add. */
.icon-button {
  width: 26px;
  height: 26px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-color: transparent;
  background: none;
  color: var(--label);
  border-radius: var(--radius-sm);
  line-height: 1;
}
.icon-button + .icon-button { margin-left: 2px; }
.icon-button:hover { background: var(--head-bg); border-color: transparent; }
.icon-button:focus-visible { border-color: var(--line); }

.queue-edit:hover { color: var(--ink); }

/* Deleting is the one that cannot be undone, so it is the one that colours.
   Red at rest, not only on hover: on a list of consignments about to be
   printed, the control that throws one away should be readable as such
   before the mouse is anywhere near it. */
.queue-remove {
  font-size: 16px;
  font-weight: 700;
  color: #dc3545;
  cursor: pointer;
}

/* Two icons, not two words, so the column narrows — but not below what two
   26px buttons and the cell's own padding need, or the table overflows its
   wrapper by a few pixels and a scrollbar appears under a table that fits. */
.sticker-queue-table .q-act { width: 120px; }   /* print, edit, delete */

/* --- sticker form validation ------------------------------------------ */
/* Above the fields and styled as a problem, not as another hint: the page is
   already full of grey explanatory text, and a complaint that looks like the
   rest of it does not get read. */
.sticker-problem {
  margin: 0 0 14px;
  padding: 10px 13px;
  border: 1px solid var(--danger);
  border-left: 3px solid var(--danger);
  border-radius: var(--radius-sm);
  background: var(--danger-bg);
  color: var(--danger);
  font-size: 13.5px;
  line-height: 1.5;
}

/* The offending boxes. Border AND a ring, because a 1px border change alone
   is easy to miss on a form of four identical fields. */
.sticker-form .is-missing,
.sticker-form .is-missing:focus {
  border-color: var(--danger);
  box-shadow: 0 0 0 3px var(--danger-bg);
}

/* The focus outline is drawn over the red border, so the one box we send the
   cursor to -- the first thing that is wrong -- was the only one that did not
   look wrong. Keep the ring, because it still has to be findable; recolour it,
   so being focused does not cancel being empty. */
.sticker-form .is-missing:focus {
  outline-color: var(--danger);
}

/* --- Queued Stickers header, required marks, Add, row dividers --------- */
/* A red asterisk on the four fields the queue refuses without. Hidden from
   screen readers: each input carries aria-required, which says it properly. */
.sticker-form label .req {
  color: #ef4444;
  margin-left: 2px;
  font-weight: bold;
}

/* The plus sits 8px from the word; the button's own gap was set for spacing
   buttons apart, and at 12px the icon read as a separate control. */
.sticker-actions #generate { gap: 8px; }

.queued-title {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
}
.queued-title .icon { color: #2563eb; flex: 0 0 auto; }

/* The size of the batch, as a pill beside its title. Empty -- nothing
   queued -- it takes no space rather than showing an empty lozenge. */
.queue-badge {
  background: #eff6ff;
  color: #2563eb;
  border-radius: 9999px;
  padding: 4px 12px;
  font-size: 12px;
  font-weight: 500;
  white-space: nowrap;
}
.queue-badge:empty { display: none; }

/* Thin, quiet dividers between queued rows. */
.sticker-queue-table tbody td { border-bottom: 1px solid #f1f5f9; }
.sticker-queue-table tbody tr:last-child td { border-bottom: 0; }

/* The row delete is a red trash can, drawn at the same size as the pencil. */
.sticker-queue-table .queue-remove .icon,
.sticker-queue-table .queue-edit .icon { width: 16px; height: 16px; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .queue-badge {
    background: rgba(37, 99, 235, 0.18); color: #93c5fd;
  }
  :root:not([data-theme="light"]) .queued-title .icon { color: #60a5fa; }
  :root:not([data-theme="light"]) .sticker-queue-table tbody td {
    border-bottom-color: var(--line-soft);
  }
}
:root[data-theme="dark"] .queue-badge { background: rgba(37, 99, 235, 0.18); color: #93c5fd; }
:root[data-theme="dark"] .queued-title .icon { color: #60a5fa; }
:root[data-theme="dark"] .sticker-queue-table tbody td { border-bottom-color: var(--line-soft); }

/* --- optional fields and dark-mode asterisks -------------------------- */
/* Vehicle No and Remarks: a shade back from the required boxes, so the eye
   goes to what must be filled first. Still a plain, full-contrast field --
   it is less urgent, not disabled. */
input.is-optional,
textarea.is-optional { background: var(--field-optional); }

/* The required-field star, softened to coral where #ef4444 glared on the
   dark card. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .sticker-form label .req { color: #f87171; }
}
:root[data-theme="dark"] .sticker-form label .req { color: #f87171; }

/* --- queue: ticks, row preview ----------------------------------------- */
.sticker-queue-table .q-num { width: 64px; }
.sticker-queue-table td.q-num,
.sticker-queue-table th.q-num { white-space: nowrap; }
/* The tick and the row number side by side, aligned inside an ordinary
   cell. Not display:flex on the td -- that stops it being a table cell,
   and the first column fell out of line with the rest of its row. */
.sticker-queue-table td.q-num > * { vertical-align: middle; }
.sticker-queue-table td.q-num .queue-select { margin-right: 8px; }
.sticker-queue-table input[type="checkbox"] {
  width: 16px;
  height: 16px;
  margin: 0;
  accent-color: #2563eb;
  cursor: pointer;
}

/* Rows open their preview on click, so they say so. */
.sticker-queue-table tbody tr { cursor: pointer; }
.sticker-queue-table tbody tr:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: -2px;
}
/* Warm amber, for cross-verifying a long list against the paperwork: it
   stands apart from the slate and blue of everything else on the page, so
   the row the eye is on cannot be confused with any other marking.

   Hover: soft amber right across the row and a golden-amber bar on its left
   edge, so the eye can trace LR number to box count without slipping a
   line. The bar sits on the first cell -- a box-shadow on the row itself
   does not paint in every browser. Scoped with table.list as well as the
   class: the site-wide table.list hover rule is otherwise the more
   specific one and paints its own faint tint over this. */
.sticker-queue-table tbody tr { transition: background-color .12s ease; }
table.list.sticker-queue-table tbody tr:hover { background: #fef3c7; }
.sticker-queue-table tbody tr:hover > td:first-child { box-shadow: inset 3px 0 0 #d97706; }

/* The row whose stickers are shown below the table: a step deeper in amber
   and a darker gold bar -- distinct from hover, and unchanged under the
   pointer, so which sticker is on display is never in doubt. */
table.list.sticker-queue-table tbody tr.is-previewing,
table.list.sticker-queue-table tbody tr.is-previewing:hover { background: #fde68a; }
.sticker-queue-table tbody tr.is-previewing > td:first-child,
.sticker-queue-table tbody tr.is-previewing:hover > td:first-child {
  box-shadow: inset 3px 0 0 #b45309;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .sticker-queue-table tbody tr:hover {
    background: rgba(217, 119, 6, 0.16);
  }
  :root:not([data-theme="light"]) .sticker-queue-table tbody tr:hover > td:first-child {
    box-shadow: inset 3px 0 0 #f59e0b;
  }
  :root:not([data-theme="light"]) .sticker-queue-table tbody tr.is-previewing,
  :root:not([data-theme="light"]) .sticker-queue-table tbody tr.is-previewing:hover {
    background: rgba(217, 119, 6, 0.30);
  }
  :root:not([data-theme="light"]) .sticker-queue-table tbody tr.is-previewing > td:first-child,
  :root:not([data-theme="light"]) .sticker-queue-table tbody tr.is-previewing:hover > td:first-child {
    box-shadow: inset 3px 0 0 #fbbf24;
  }
}
:root[data-theme="dark"] .sticker-queue-table tbody tr:hover { background: rgba(217, 119, 6, 0.16); }
:root[data-theme="dark"] .sticker-queue-table tbody tr:hover > td:first-child {
  box-shadow: inset 3px 0 0 #f59e0b;
}
:root[data-theme="dark"] .sticker-queue-table tbody tr.is-previewing,
:root[data-theme="dark"] .sticker-queue-table tbody tr.is-previewing:hover {
  background: rgba(217, 119, 6, 0.30);
}
:root[data-theme="dark"] .sticker-queue-table tbody tr.is-previewing > td:first-child,
:root[data-theme="dark"] .sticker-queue-table tbody tr.is-previewing:hover > td:first-child {
  box-shadow: inset 3px 0 0 #fbbf24;
}

.sticker-queue-table .queue-print .icon { width: 16px; height: 16px; }
.sticker-queue-table .queue-print:hover { background: #f1f5f9; color: #0f172a; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .sticker-queue-table .queue-print:hover {
    background: var(--head-bg); color: var(--ink);
  }
}
:root[data-theme="dark"] .sticker-queue-table .queue-print:hover {
  background: var(--head-bg); color: var(--ink);
}
.queue-preview-hint { margin: 10px 2px 0; }

/* ── App chooser (/select-app): Gate Pass, or MailStream Logistics ─────────── */
.app-chooser { max-width: 760px; margin: 48px auto 0; }
.chooser-title { font-size: 26px; margin: 0 0 6px; color: var(--ink); }
.chooser-sub { margin: 0 0 24px; color: var(--muted); }
.chooser-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 16px; }
.app-card-form { margin: 0; display: flex; }
.app-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: 8px; width: 100%;
  padding: 22px; border-radius: 14px; background: var(--card); border: 1px solid var(--line);
  color: var(--ink); text-decoration: none; text-align: left; font: inherit; cursor: pointer;
  box-shadow: 0 1px 2px rgba(15, 23, 42, .04); transition: border-color .15s, box-shadow .15s, transform .15s;
}
a.app-card:hover, button.app-card:hover {
  border-color: var(--accent); box-shadow: 0 6px 18px rgba(15, 23, 42, .08); transform: translateY(-1px);
}
a.app-card:focus-visible, button.app-card:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--accent-soft); border-color: var(--accent); }
.app-icon { display: grid; place-items: center; width: 48px; height: 48px; border-radius: 12px; }
.app-icon-gatepass { background: var(--accent-soft); color: var(--accent); }
.app-icon-mailstream { background: #ecfdf5; color: #047857; }
.app-name { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 17px; font-weight: 700; }
.app-desc { color: var(--muted); font-size: 14px; line-height: 1.45; }
.app-go { margin-top: auto; padding-top: 6px; font-weight: 600; font-size: 14px; color: var(--accent); }
.app-card.is-locked { cursor: not-allowed; opacity: .7; box-shadow: none; }
.app-card.is-locked .app-icon { filter: grayscale(1); }
.access-badge {
  display: inline-flex; align-items: center; gap: 4px; padding: 2px 9px; border-radius: 999px;
  font-size: 11px; font-weight: 700; background: #fef3c7; color: #92400e; border: 1px solid #fde68a;
}
@media (prefers-color-scheme: dark) {
  .app-icon-mailstream { background: #064e3b; color: #a7f3d0; }
  .access-badge { background: #3a2a10; color: #fcd34d; border-color: #78350f; }
}
