/* Tinkervillage VMS — one small stylesheet, no framework.
   Vendor screens lean on large touch targets; staff screens are denser. */

:root {
  --bg: #f6f7f9;
  --panel: #ffffff;
  --ink: #16181d;
  --muted: #6b7280;
  --line: #e3e6ea;
  --accent: #4338ca;
  --accent-soft: #eef2ff;
  --urgent: #b42318;
  --urgent-soft: #fef3f2;
  --warn: #b54708;
  --warn-soft: #fffaeb;
  --ok: #027a48;
  --ok-soft: #ecfdf3;
  --radius: 10px;
  --shadow: 0 1px 2px rgba(16, 24, 40, .06), 0 1px 3px rgba(16, 24, 40, .1);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 15px/1.5 "Segoe UI", system-ui, -apple-system, sans-serif;
  /* Design filenames run long and contain no spaces to break at
     (G4_Narayana_Comparing_Fractions_A4.pdf), so on a narrow screen they would otherwise push
     their whole panel sideways. `anywhere` rather than `break-word` because only `anywhere`
     also lets a flex or table cell shrink below that unbreakable word. */
  overflow-wrap: anywhere;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }

/* ------------------------------------------------------------------ chrome */

.topbar {
  display: flex;
  align-items: center;
  gap: 20px;
  padding: 0 24px;
  height: 56px;
  background: var(--panel);
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  z-index: 20;
}
.brand { font-weight: 700; letter-spacing: -.2px; color: var(--ink); }
.brand span { color: var(--accent); }
.topbar nav { display: flex; gap: 18px; margin-left: auto; align-items: center; }
.topbar nav a { color: var(--muted); font-weight: 500; }
.topbar nav a.active, .topbar nav a:hover { color: var(--ink); text-decoration: none; }

/* Desktop has no hamburger and no scrim; both only exist below the drawer breakpoint. */
.navtoggle, .navscrim { display: none; }

.bell { position: relative; }
.bell .count {
  position: absolute; top: -7px; right: -10px;
  background: var(--urgent); color: #fff;
  font-size: 11px; font-weight: 700;
  border-radius: 9px; padding: 1px 5px;
}

main { max-width: 1180px; margin: 0 auto; padding: 24px; }
main.narrow { max-width: 780px; }

h1 { font-size: 22px; margin: 0 0 4px; letter-spacing: -.3px; }
.pagehead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  flex-wrap: wrap;
}
.pagehead .btn { display: inline-flex; align-items: center; gap: 6px; }
h2 { font-size: 16px; margin: 28px 0 10px; }
.subtitle { color: var(--muted); margin: 0 0 20px; }

.panel {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}
.panel + .panel { margin-top: 16px; }
.panel-head {
  padding: 12px 16px;
  border-bottom: 1px solid var(--line);
  font-weight: 600;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 10px;
}
.panel-body { padding: 16px; }

/* ------------------------------------------------------------------ queue */

.queue { list-style: none; margin: 0; padding: 0; }
.queue li {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 14px 16px;
  border-bottom: 1px solid var(--line);
  background: var(--panel);
}
.queue li:last-child { border-bottom: 0; }
.queue li.urgent { background: var(--urgent-soft); }
.queue .body { flex: 1; min-width: 0; }
.queue .jobid {
  font-family: ui-monospace, "Cascadia Mono", Consolas, monospace;
  font-size: 12px;
  color: var(--muted);
  letter-spacing: .3px;
}
.queue .spec { font-size: 16px; font-weight: 600; margin: 2px 0; }
.queue .meta { font-size: 13px; color: var(--muted); }
.queue .actions { display: flex; gap: 8px; align-items: center; flex-shrink: 0; }

/* ------------------------------------------------- thumbnail + hover preview */

.thumb { position: relative; flex-shrink: 0; }
.thumb img.tile {
  width: 56px; height: 56px;
  object-fit: cover;
  border: 1px solid var(--line);
  border-radius: 6px;
  background: #fff;
  display: block;
}
.thumb.sm img.tile { width: 38px; height: 38px; }
.thumb.docket { width: 100%; }
.thumb.docket img.tile { width: 100%; height: auto; aspect-ratio: 4 / 5; object-fit: cover; }
.thumb.docket .placeholder { width: 100%; height: auto; aspect-ratio: 4 / 5; }
.thumb .pop {
  display: none;
  position: absolute;
  left: 0;
  top: calc(100% + 8px);
  z-index: 60;
  padding: 6px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
  box-shadow: 0 12px 28px rgba(16, 24, 40, .18);
}
.thumb .pop img { display: block; max-width: 420px; max-height: 520px; border-radius: 4px; }
/* A touch screen has no hover, so the popover would either never open or open and never close.
   Phones get the tap lightbox below instead — see static/js/preview.js. */
@media (hover: hover) {
  .thumb:hover .pop { display: block; }
}
.thumb .placeholder {
  width: 56px; height: 56px;
  border: 1px dashed var(--line);
  border-radius: 6px;
  display: grid; place-items: center;
  color: var(--muted); font-size: 11px;
  background: #fff;
}

/* ------------------------------------------------------------------ bits */

.badge {
  display: inline-block;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .4px;
  text-transform: uppercase;
  padding: 2px 7px;
  border-radius: 20px;
  background: var(--line);
  color: var(--muted);
}
.badge.urgent { background: var(--urgent); color: #fff; }
.badge.overdue { background: var(--warn-soft); color: var(--warn); }
.badge.ok { background: var(--ok-soft); color: var(--ok); }
.badge.live { background: var(--accent-soft); color: var(--accent); }

/* Which pack a file goes in. Deliberately not a .badge: the badges on these cards report a
   status the vendor reads and moves on from, and this is an instruction they act on with a
   marker. So it is a bordered chip with the code in a solid block, and it does not sit in the
   same visual family as "Urgent" or "In progress".

   The code is monospace at full weight because it is copied by hand onto a physical pack, and
   the whole chip is tinted so the two kinds are told apart across a workshop bench, before
   anybody has read a single letter. */
.pack {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 8px 2px 2px;
  border: 1px solid var(--line);
  border-radius: 20px;
  background: var(--panel);
  font-size: 11.5px;
  font-weight: 600;
  color: var(--muted);
  white-space: nowrap;
  vertical-align: middle;
}
.pack b {
  padding: 2px 7px;
  border-radius: 20px;
  font: 700 11px/1.35 ui-monospace, "Cascadia Mono", Consolas, monospace;
  letter-spacing: .1em;
  color: #fff;
}
.pack i { font-style: normal; }
.pack.sample { border-color: #c7d7fe; background: var(--accent-soft); color: #3730a3; }
.pack.sample b { background: var(--accent); }
.pack.production { border-color: #a6f4c5; background: var(--ok-soft); color: #05603a; }
.pack.production b { background: var(--ok); }

/* The vendor's own cards get the stamp: top-right corner, and big enough to be the second thing
   read after the quantity. On the staff screens this stays the quiet chip above — there it is a
   fact you look up, but here it is the instruction the vendor acts on with a marker, and a file
   packed into the wrong pack is not caught until it reaches us.

   The code is not reversed out of a solid block at this size. The card already has a 40px
   quantity as its hero, and a second solid slab would fight it; a tinted panel with the code in
   full colour reads as loudly without turning the top of every card into a colour test. */
.pack.stamp {
  flex-direction: column;
  align-items: center;
  gap: 2px;
  margin-left: auto;      /* this is what puts it in the corner */
  padding: 5px 12px 7px;
  border-width: 1.5px;
  border-radius: 10px;
  flex-shrink: 0;
}
/* One character, so it can be set far larger than a three-letter code ever could without making
   the stamp wider than the card can spare. No letter-spacing here either — that only ever put a
   gap after the last character, which on a single letter is the whole of it, throwing the code
   off-centre above its own caption. */
.pack.stamp b {
  padding: 0;
  background: none;
  font-size: 38px;
  line-height: 1;
  letter-spacing: 0;
}
.pack.stamp i {
  font: 700 9.5px/1.5 ui-monospace, "Cascadia Mono", Consolas, monospace;
  letter-spacing: .08em;
  text-transform: uppercase;
}
.pack.stamp.sample b { color: var(--accent); }
.pack.stamp.production b { color: var(--ok); }

.btn {
  display: inline-block;
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--ink);
  padding: 9px 16px;
  border-radius: 8px;
  font: inherit;
  font-weight: 600;
  cursor: pointer;
  white-space: nowrap;
}
.btn:hover { background: #f2f4f7; text-decoration: none; }
.btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn.primary:hover { background: #372fb5; }
.btn.danger { background: var(--urgent); border-color: var(--urgent); color: #fff; }
.btn.danger:hover { background: #96201a; }
.btn.small { padding: 5px 11px; font-size: 13px; }
.btn.big { padding: 12px 22px; font-size: 16px; }

/* Download: one fixed icon, always beside the file, never beside the workflow buttons. */
.btn-icon {
  display: grid;
  place-items: center;
  width: 38px; height: 38px;
  flex-shrink: 0;
  border: 1px solid var(--line);
  border-radius: 8px;
  background: var(--panel);
  color: var(--accent);
  cursor: pointer;
}
.btn-icon:hover {
  background: var(--accent-soft);
  border-color: var(--accent);
  text-decoration: none;
}

/* Dense tables scroll inside their own panel on a narrow screen rather than dragging the whole
   page sideways. Deliberately NOT a scroll container on desktop: overflow clips descendants, and
   the hover preview hangs below its row. On a phone there is no hover preview to clip — tapping
   a thumbnail opens the full-screen lightbox instead. */
.table-wrap { /* scrolls only below the drawer breakpoint — see the media query */ }

table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px 16px; text-align: left; border-bottom: 1px solid var(--line); }
th { font-size: 12px; text-transform: uppercase; letter-spacing: .4px; color: var(--muted); }
tbody tr:last-child td { border-bottom: 0; }
/* Figures and headings never wrap. A quantity broken over two lines, or a "Qty" column squeezed
   to one character wide, is unreadable in exactly the place accuracy matters. width:1% is
   shrink-to-fit — the text columns absorb the slack instead of the numbers doing it. */
td.num, th.num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  width: 1%;
}
th { white-space: nowrap; }
/* For short label columns — a vendor name, mainly — sitting beside the job column. Auto table
   layout hands out width by max-content, and a design filename's max-content dwarfs everything
   else in the row, so without this a two-word vendor gets folded onto two lines while the job
   column keeps room it does not need. */
td.nowrap { white-space: nowrap; }

.tiles { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px; }
.tile-stat {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 14px 16px;
  box-shadow: var(--shadow);
}
.tile-stat .n { font-size: 26px; font-weight: 700; letter-spacing: -.5px; }
.tile-stat .k { font-size: 13px; color: var(--muted); }
.tile-stat.alert .n { color: var(--urgent); }
.tile-stat.warn .n { color: var(--warn); }

form .field { margin-bottom: 16px; }
form label { display: block; font-weight: 600; font-size: 14px; margin-bottom: 5px; }
input[type=text], input[type=number], input[type=date], input[type=password],
input[type=file], select, textarea {
  width: 100%;
  padding: 9px 11px;
  border: 1px solid var(--line);
  border-radius: 8px;
  font: inherit;
  background: #fff;
}
input:focus, select:focus, textarea:focus {
  outline: 2px solid var(--accent-soft);
  border-color: var(--accent);
}
.checks label { font-weight: 500; display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
.checks input { width: auto; }
ul.errorlist { margin: 4px 0 0; padding-left: 18px; color: var(--urgent); font-size: 13px; }
.helptext { display: block; color: var(--muted); font-size: 13px; margin-top: 4px; }

.messages { list-style: none; margin: 0 0 18px; padding: 0; }
.messages li {
  padding: 11px 14px; border-radius: 8px; margin-bottom: 8px;
  background: var(--accent-soft); color: var(--accent); font-weight: 500;
}
.messages li.success { background: var(--ok-soft); color: var(--ok); }
.messages li.warning { background: var(--warn-soft); color: var(--warn); }
.messages li.error { background: var(--urgent-soft); color: var(--urgent); }

.timeline { list-style: none; margin: 0; padding: 0; }
.timeline li { display: flex; gap: 12px; padding: 9px 0; border-bottom: 1px dashed var(--line); }
.timeline li:last-child { border-bottom: 0; }
.timeline .when { color: var(--muted); font-size: 13px; width: 140px; flex-shrink: 0; }
.timeline .what { font-weight: 600; }
.timeline .who { color: var(--muted); font-weight: 400; }

/* --------------------------------------------------------- a job in a list */

/* File and quantity carry the row; the spec sits under it as fine print. */
/* Deliberately not a flex row. A flex item's box stays as wide as its longest line, so a filename
   that wrapped onto a short second line would leave the quantity marooned out to the right of
   nothing. Inline flow sets "x20" immediately after the last character of the name — the order you
   say it out loud, and it stays put however the name breaks. */
.jobline { display: block; }
/* Written out in full, never elided. Two design files routinely differ only in their last few
   characters — a colourway, usually — and an ellipsis eats exactly the part that tells them
   apart, which is how the wrong row gets opened. It wraps instead; `overflow-wrap: anywhere` off
   `body` breaks the long unbroken filenames and lets the cell shrink below one. */
.jobline .file { font-weight: 600; color: var(--ink); }
a.file:hover { color: var(--accent); }
.jobline .qty {
  margin-left: 6px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  letter-spacing: -.01em;
  white-space: nowrap; /* "x20" is one token; never break it over a line end */
}
.jobline-spec { font-size: 12.5px; color: var(--muted); margin-top: 1px; }

/* ------------------------------------------------- dashboard activity */

/* One block per submission. The only time shown is when a file last moved, and it is always on
   the right — the eye scans statuses down the middle, not timestamps. */
.task { border-bottom: 1px solid var(--line); }
.task:last-child { border-bottom: 0; }
.task-head {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  padding: 11px 16px 8px;
  font-size: 13.5px;
  color: var(--muted);
}
.task-head .sep { opacity: .5; }
.task-head .when, .task-files .when {
  margin-left: auto;
  color: var(--muted);
  font-size: 12.5px;
  white-space: nowrap;
}

.task-files { list-style: none; margin: 0; padding: 0 16px 12px; }
.task-files li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 0;
  border-top: 1px solid var(--line);
}
.task-files li:first-child { border-top: 0; }
/* File and quantity on top, spec underneath — same shape as every other listing. */
.task-file-main { flex: 1; min-width: 0; }
.task-files .badge { flex-shrink: 0; }
.task-files form, .task-head form { flex-shrink: 0; }
/* The "when" pushes to the right; anything after it sits alongside, not below. */
.task-files .nudged {
  flex-shrink: 0;
  font-size: 12px;
  color: var(--muted);
  font-style: italic;
}

/* Ops Lead only: everyone / just mine. */
.scope { display: flex; gap: 4px; border: 1px solid var(--line); border-radius: 8px; padding: 3px; background: var(--panel); }
.scope a { padding: 5px 12px; border-radius: 6px; font-size: 13px; font-weight: 600; color: var(--muted); }
.scope a:hover { text-decoration: none; background: #f2f4f7; }
.scope a.on { background: var(--accent); color: #fff; }

.empty { padding: 44px 16px; text-align: center; color: var(--muted); }
.empty strong { display: block; font-size: 17px; color: var(--ink); margin-bottom: 4px; }

.row { display: flex; gap: 16px; flex-wrap: wrap; }
.row > * { flex: 1; min-width: 240px; }
.filters { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
.filters .field { margin-bottom: 0; }
.muted { color: var(--muted); }
.strike { text-decoration: line-through; color: var(--muted); }

/* ------------------------------------------------------- drag & drop upload */

.dropzone {
  border: 2px dashed var(--line);
  border-radius: var(--radius);
  background: #fbfcfd;
  padding: 34px 20px;
  text-align: center;
  cursor: pointer;
  transition: border-color .12s, background .12s;
}
.dropzone:hover { border-color: var(--accent); }
.dropzone.dragging {
  border-color: var(--accent);
  background: var(--accent-soft);
}
.dropzone.has-files { padding: 18px 20px; }
.dropzone .dz-title { font-weight: 600; font-size: 16px; }
.dropzone .dz-hint { color: var(--muted); font-size: 13px; margin-top: 4px; }
.dropzone input[type=file] { display: none; }

#filelist { margin-top: 12px; }
.filerow {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 8px;
  margin-bottom: 6px;
  text-align: left;
}
.filerow-thumb {
  width: 44px; height: 44px;
  flex-shrink: 0;
  border: 1px solid var(--line);
  border-radius: 6px;
  display: grid; place-items: center;
  background: #fff;
  font-size: 11px; font-weight: 700; color: var(--muted);
  overflow: hidden;
}
.filerow-thumb img { width: 100%; height: 100%; object-fit: cover; }
.filerow-meta { flex: 1; min-width: 0; }
.filerow-name {
  font-weight: 600; font-size: 14px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.filerow-size { font-size: 12px; color: var(--muted); }

/* ------------------------------------------------------- wizard chrome */

.steps {
  display: flex;
  gap: 8px;
  list-style: none;
  margin: 0 0 20px;
  padding: 0;
  flex-wrap: wrap;
}
.steps li {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 14px 7px 8px;
  border: 1px solid var(--line);
  border-radius: 20px;
  background: var(--panel);
  color: var(--muted);
  font-size: 13px;
  font-weight: 600;
}
.steps li span {
  display: grid;
  place-items: center;
  width: 20px; height: 20px;
  border-radius: 50%;
  background: var(--line);
  color: var(--muted);
  font-size: 12px;
}
.steps li.on { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.steps li.on span { background: var(--accent); color: #fff; }
.steps li.done { color: var(--ok); border-color: #a6f4c5; background: var(--ok-soft); }
.steps li.done span { background: var(--ok); color: #fff; }

.progress { height: 4px; background: var(--line); overflow: hidden; }
.progress.flat { border-radius: 0; }
.progress-bar { height: 100%; width: 0; background: var(--accent); transition: width .2s ease; }

.wizard { display: flex; gap: 0; align-items: stretch; }
.wizard-preview {
  width: 46%;
  flex-shrink: 0;
  padding: 20px;
  background: #f2f4f7;
  border-right: 1px solid var(--line);
  display: flex;
  flex-direction: column;
  gap: 12px;
  align-items: center;
  justify-content: flex-start;
}
.wizard-preview img {
  max-width: 100%;
  max-height: 460px;
  border-radius: 6px;
  border: 1px solid var(--line);
  background: #fff;
  box-shadow: var(--shadow);
}
.wizard-filemeta { text-align: center; }
.wizard-filemeta .filerow-name { white-space: normal; word-break: break-all; }
.nopreview {
  padding: 50px 20px;
  text-align: center;
  color: var(--muted);
  border: 1px dashed var(--line);
  border-radius: 8px;
  background: #fff;
  width: 100%;
}
.nopreview strong { display: block; color: var(--ink); margin-bottom: 4px; }

.wizard-form { flex: 1; padding: 20px; min-width: 0; }
.wizard-form .row > * { min-width: 140px; }
.applyall {
  background: var(--accent-soft);
  border: 1px solid #c7d2fe;
  border-radius: 8px;
  padding: 10px 12px;
}
.wizard-nav {
  display: flex;
  gap: 8px;
  align-items: center;
  flex-wrap: wrap;
  padding-top: 8px;
  border-top: 1px solid var(--line);
  margin-top: 16px;
}

.filmstrip {
  display: flex;
  gap: 8px;
  padding: 12px 16px;
  border-top: 1px solid var(--line);
  background: #fbfcfd;
  overflow-x: auto;
}
.filmstrip .strip {
  position: relative;
  width: 48px; height: 48px;
  flex-shrink: 0;
  border: 2px solid var(--line);
  border-radius: 6px;
  overflow: hidden;
  display: grid; place-items: center;
  background: #fff;
  color: var(--muted);
  font-weight: 700;
}
.filmstrip .strip img { width: 100%; height: 100%; object-fit: cover; }
.filmstrip .strip.current { border-color: var(--accent); }
.filmstrip .strip .tick {
  position: absolute;
  right: 0; bottom: 0;
  background: var(--ok);
  color: #fff;
  font-size: 10px;
  font-style: normal;
  padding: 0 3px;
  border-radius: 4px 0 0 0;
}

/* ------------------------------------------------------- upload overlay */

.overlay {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(16, 24, 40, .55);
  display: grid;
  place-items: center;
}
.overlay[hidden] { display: none; }
.overlay-card {
  background: var(--panel);
  border-radius: var(--radius);
  padding: 28px 32px;
  width: min(380px, 90vw);
  text-align: center;
  box-shadow: 0 20px 40px rgba(16, 24, 40, .3);
}
.overlay-title { font-weight: 700; font-size: 17px; margin: 14px 0 12px; }
.overlay-hint { color: var(--muted); font-size: 13px; margin-top: 10px; }
.overlay .progress { border-radius: 999px; }
.spinner {
  width: 34px; height: 34px;
  margin: 0 auto;
  border: 3px solid var(--line);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin .8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ------------------------------------------------- vendor queue: batches */

/* The heading is only a divider now. Everything a vendor needs lives on the card. */
.batch { margin-bottom: 30px; }
.batch-head {
  display: flex;
  align-items: center;
  gap: 7px;
  flex-wrap: wrap;
  padding: 0 2px 8px;
  margin-bottom: 10px;
  border-bottom: 1px solid var(--line);
  font-size: 13.5px;
  color: var(--muted);
}
.batch-head .who { font-weight: 700; color: var(--ink); font-size: 15px; }
.batch-head .sep { opacity: .5; }
.batch-note {
  font-size: 14px;
  padding: 0 2px 12px;
  color: var(--muted);
}

/* ------------------------------------------------- vendor queue: the docket */

.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
  margin-bottom: 12px;
}
.docket { display: flex; gap: 18px; padding: 16px; }
.docket.is-urgent { border-left: 4px solid var(--urgent); }
.docket.is-redo { border-left: 4px solid var(--warn); }

.docket-file {
  width: 116px;
  flex-shrink: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.filemeta { font-size: 11.5px; color: var(--muted); text-align: center; }

.dl-wide {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  width: 100%;
  padding: 7px 0;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--panel);
  color: var(--accent);
  font-size: 12.5px;
  font-weight: 600;
  cursor: pointer;
}
.dl-wide:hover {
  background: var(--accent-soft);
  border-color: var(--accent);
  text-decoration: none;
}

.docket-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 12px; }
/* Tops aligned, not centres: the stamp on the right is twice the height of the id and badges on
   the left, and centring them against it leaves the row's contents floating in its middle. */
.docket-top { display: flex; align-items: flex-start; gap: 8px; flex-wrap: wrap; }
.docket-top .jobid, .docket-top .badge { margin-top: 3px; }

/* Quantity is the number the vendor acts on, so it is the loudest thing on the card. */
.docket-hero { display: flex; align-items: baseline; gap: 12px; }
.docket-qty {
  font-size: 40px;
  font-weight: 700;
  line-height: .9;
  letter-spacing: -.03em;
  font-variant-numeric: tabular-nums;
}
.docket-qty small { font-size: 14px; font-weight: 600; color: var(--muted); margin-left: 3px; }
/* The whole instruction as one sentence: 3 x Print + Plotter Cut on 170gsm Art Card.
   "on" is the only quiet word in it — both the process and the stock stay at full weight. */
.specline { font-size: 19px; font-weight: 600; line-height: 1.25; min-width: 0; }
.specline .on { color: var(--muted); font-weight: 400; }
.specline .mat { color: var(--ink); font-weight: 600; }
.specline em {
  display: block;
  font-style: normal;
  font-size: 14px;
  font-weight: 400;
  color: var(--muted);
  margin-top: 2px;
}

.facts {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
  margin: 0;
  padding: 12px 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
}
.facts dt {
  font: 700 10px/1.6 ui-monospace, "Cascadia Mono", Consolas, monospace;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
}
.facts dd { margin: 2px 0 0; font-size: 14.5px; font-weight: 600; }
.facts dd.late { color: var(--warn); }

.docket-note {
  font-size: 14px;
  background: #fbfcfd;
  border: 1px solid var(--line);
  border-left: 3px solid var(--line);
  border-radius: 0 6px 6px 0;
  padding: 9px 12px;
}
.docket-note.warn {
  background: var(--warn-soft);
  border-color: #fedf89;
  border-left-color: var(--warn);
}
.docket-note .lbl {
  display: block;
  font: 700 10px/1.6 ui-monospace, "Cascadia Mono", Consolas, monospace;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
}
.docket-note.warn .lbl { color: var(--warn); }

.docket-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  font-size: 13px;
}

/* ------------------------------------------------- vendor finish: one card per file */

/* Finishing a file reuses the queue's own docket card rather than a second card language for
   the same job — the vendor already knows this shape. */
.finish-cards { display: flex; flex-direction: column; }

/* The whole point of this screen is confirming the right artwork was produced, so the filename
   wraps in full instead of eliding. `.jobline .file`'s ellipsis rule is exactly the fault this
   fixes: real filenames are long and often differ only near the end (a colourway, usually), so
   truncating them can make two different files look identical on the one screen where the
   vendor confirms which one they made. Deliberately its own class rather than reusing
   `.jobline` so no other listing is affected. */
.fname {
  display: block;
  font-weight: 600;
  font-size: 17px;
  color: var(--ink);
  white-space: normal;
  overflow-wrap: anywhere;
  line-height: 1.32;
}
.jobid-line { font-size: 12px; color: var(--muted); margin: 2px 0 10px; }

/* The filename can run to three lines, so the stamp is pinned to the top of the row rather than
   centred against it — it belongs to the corner of the card, not to the middle of the name. */
.finish-head { display: flex; align-items: flex-start; gap: 14px; }
.finish-head-text { flex: 1; min-width: 0; }

.finish-card .card-figures {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 14px;
  align-items: start;
}
.finish-card .card-figures label {
  display: block;
  font: 700 10px/1.6 ui-monospace, "Cascadia Mono", Consolas, monospace;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 4px;
}
.finish-card input[type=number] { width: 100%; }
.finish-card .figure-static {
  font-size: 19px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  padding: 9px 0;
}
/* A strip reserved under BOTH Produced and Unit price, whatever it says — or nothing. Only the
   unit box used to carry a hint under it, which made it read as taller than Produced beside it;
   reserving the same space under both keeps the inputs themselves starting level. */
.figure-note { font-size: 12px; color: var(--muted); min-height: 15px; line-height: 15px; margin-top: 3px; }
.figure-note.no-rate { font-style: italic; }

.figure-total-row {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 21px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  padding-top: 2px;
}
.figure-total-row .lbl {
  font: 700 10px/1.6 ui-monospace, "Cascadia Mono", Consolas, monospace;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--muted);
}
/* No rate card is the common case here, not an error — it must read as calm and deliberate, so
   an em dash in a quiet, italic hand rather than anything that looks like a warning. */
.noprice { color: var(--muted); font-style: italic; font-weight: 400; }

.card-done { display: flex; align-items: center; gap: 8px; font-weight: 600; }
.card-done input[type=checkbox] { transform: scale(1.2); }

/* ------------------------------------------------- tap-to-enlarge (touch only) */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 300;
  background: rgba(16, 24, 40, .82);
  display: grid;
  place-items: center;
  padding: 16px;
}
.lightbox[hidden] { display: none; }
.lightbox img {
  max-width: 100%;
  max-height: 100%;
  border-radius: 6px;
  background: #fff;
  box-shadow: 0 20px 40px rgba(16, 24, 40, .4);
}
.lightbox .close {
  position: absolute;
  top: 12px; right: 12px;
  width: 40px; height: 40px;
  display: grid; place-items: center;
  border: 0;
  border-radius: 50%;
  background: rgba(255, 255, 255, .92);
  color: var(--ink);
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
}

/* ------------------------------------------------------------------ responsive */

@media (max-width: 860px) {
  .wizard { flex-direction: column; }
  .wizard-preview { width: 100%; border-right: 0; border-bottom: 1px solid var(--line); }
  .wizard-preview img { max-height: 300px; }
}

/* The nav is 7 items wide for staff and will not fit on a phone, so below 760px it becomes a
   drawer that slides in from the right. The bar itself stays one row and stays sticky. */
@media (max-width: 760px) {
  .topbar { padding: 0 14px; gap: 12px; }

  .navtoggle {
    position: relative;
    display: grid;
    place-items: center;
    margin-left: auto;
    width: 42px; height: 42px;
    border: 1px solid var(--line);
    border-radius: 8px;
    background: var(--panel);
    color: var(--ink);
    cursor: pointer;
  }
  .navtoggle .dot {
    position: absolute;
    top: 6px; right: 7px;
    width: 9px; height: 9px;
    border-radius: 50%;
    background: var(--urgent);
    border: 2px solid var(--panel);
  }

  .navscrim {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 30;
    background: rgba(16, 24, 40, .45);
  }
  .navscrim[hidden] { display: none; }

  .topbar nav {
    position: fixed;
    top: 0; right: 0; bottom: 0;
    z-index: 40;
    width: min(290px, 84vw);
    margin-left: 0;
    padding: 16px;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    background: var(--panel);
    border-left: 1px solid var(--line);
    box-shadow: -14px 0 30px rgba(16, 24, 40, .18);
    overflow-y: auto;
    transform: translateX(100%);
    transition: transform .22s ease;
  }
  .topbar nav.open { transform: none; }

  /* Full-width rows: a drawer item should be tappable anywhere along the line. */
  .topbar nav > a {
    padding: 13px 4px;
    font-size: 16px;
    border-bottom: 1px solid var(--line);
  }
  .topbar nav .btn { width: 100%; text-align: center; margin-top: 12px; }
  .topbar nav form { width: 100%; }

  /* In the drawer the alert count sits inline — there is no icon for it to hang off. */
  .bell { display: flex; align-items: center; justify-content: space-between; }
  .bell .count { position: static; top: auto; right: auto; padding: 2px 8px; font-size: 12px; }

  .table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }
}

@media (prefers-reduced-motion: reduce) {
  .topbar nav { transition: none; }
}

@media (max-width: 640px) {
  main { padding: 14px; }
  th, td { padding: 8px 10px; }
  .queue li { flex-wrap: wrap; }
  .queue .actions { width: 100%; }
  .queue .actions .btn { flex: 1; text-align: center; }

  .docket { flex-direction: column; gap: 14px; }
  .docket-file { flex-direction: row; align-items: flex-start; width: 100%; }
  .thumb.docket { width: 96px; flex-shrink: 0; }
  .dl-wide { width: auto; padding: 9px 16px; }
  .filemeta { text-align: left; align-self: center; }
  .facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .docket-foot { flex-direction: column; align-items: stretch; gap: 8px; }
  .docket-foot .btn { width: 100%; text-align: center; }

  /* iOS Safari zooms the page in on any focused control under 16px and never zooms back out. */
  input[type=text], input[type=number], input[type=date], input[type=password],
  input[type=file], select, textarea { font-size: 16px; }

  .table-wrap > table { min-width: 620px; }
  .table-wrap.wide > table { min-width: 840px; }

  .filters { flex-direction: column; align-items: stretch; }
  .filters .btn { width: 100%; text-align: center; }

  .timeline li { flex-direction: column; gap: 2px; }
  .timeline .when { width: auto; }

  .pagehead h1 { font-size: 20px; }

  /* Alerts are prose, so they stack rather than scroll sideways. */
  .notes, .notes tbody, .notes tr, .notes td { display: block; }
  .notes tr { padding: 10px 4px; border-bottom: 1px solid var(--line); }
  .notes tr:last-child { border-bottom: 0; }
  .notes td, .notes td.num { border: 0; padding: 2px 0; text-align: left; }
}

/* The finish screen is already a stack of docket cards on desktop, so below 720px it only needs
   to narrow, same as the queue card it is built from (see .docket rules above) — there is no
   table pretending to be a phone layout here to restack. */

/* Process pills. A real checkbox stays underneath each one — the pill is the label, so keyboard
   focus, space-to-toggle and submitting with JavaScript off all behave as they always did. The
   list narrows to the chosen material (static/js/spec.js). */
.pills { display: flex; flex-wrap: wrap; gap: 8px; }
.pills .pill {
  display: inline-flex; align-items: center; gap: 0;
  padding: 8px 14px; border: 1px solid var(--line); border-radius: 999px;
  background: #fff; cursor: pointer; font-weight: 500; font-size: 14px;
  user-select: none; transition: background .12s, border-color .12s, color .12s;
}
/* The checkbox is visually gone but still focusable and still posts. */
.pills .pill input {
  position: absolute; width: 1px; height: 1px; margin: 0;
  opacity: 0; pointer-events: none;
}
/* An explicit display beats the browser's default [hidden] { display: none }, so a pill
   spec.js has hidden would otherwise keep painting. This is what actually narrows the list. */
.pills .pill[hidden] { display: none; }
.pills .pill:hover { border-color: var(--accent); }
.pills .pill:has(input:checked) {
  background: var(--accent); border-color: var(--accent); color: #fff;
}
/* Keyboard users land on the hidden checkbox, so the ring has to be drawn by the pill. */
.pills .pill:focus-within { outline: 2px solid var(--accent); outline-offset: 2px; }
.pill-note { display: block; margin-top: 8px; color: var(--warn); }
@media (max-width: 760px) { .pills .pill { font-size: 15px; padding: 10px 16px; } }

/* Quantity and due-date shortcut tiles: the two or three answers that cover almost every job,
   sitting beside the free field rather than replacing it. The free field gets a tile of its own
   so the row reads as one set of choices instead of controls-plus-an-afterthought.

   The shortcuts are inert without spec.js, so they ship `hidden` and it reveals them. */
.tiles { display: flex; flex-wrap: wrap; gap: 10px; align-items: stretch; }
.tile {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  min-width: 88px; min-height: 76px; padding: 10px 14px;
  border: 1px solid var(--line); border-radius: 16px; background: #fff;
  font: inherit; color: inherit; cursor: pointer; text-align: center;
  transition: background .12s, border-color .12s, color .12s;
}
/* As with the pills, an explicit display beats the browser's default [hidden] rule. */
.tile[hidden] { display: none; }
.tile b { font-size: 26px; line-height: 1.1; font-weight: 700; }
.tile span { font-size: 12px; color: var(--muted); }
.tile:hover { border-color: var(--accent); }
/* Chosen is a filled tile with its colours flipped — accent ground, white type — over the same
   thin border every tile carries. An outline alone was too quiet at a glance across five boxes;
   the fill is what you see without looking for it. This also puts the tiles back in step with
   the process pills, which fill the same way. */
.tile.on {
  background: var(--accent); border-color: var(--accent); color: #fff;
}
.tile.on span { color: rgba(255, 255, 255, .85); }
/* The typed quantity is an <input>, so its own colour has to flip with the tile. */
.tile.on input { color: #fff; caret-color: #fff; }
/* Date shortcuts carry a word and a date rather than a numeral, so they need the width. */
.tile.wide { min-width: 124px; }
.tile.wide b { font-size: 18px; }
.tile-field { cursor: text; }
.tile-field input {
  width: 100%; border: 0; padding: 0; background: transparent; color: inherit;
  font-size: 22px; font-weight: 700; text-align: center;
}
.tile-field input:focus { outline: none; }
.tile-field:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(67, 56, 202, .12); }
.tile.wide.tile-field input { font-size: 15px; font-weight: 600; }
/* Kept at 16px+ on phones: iOS Safari zooms into any smaller focused field and never back out. */
@media (max-width: 760px) { .tile { min-width: 80px; } .tile.wide { min-width: 118px; } }

/* Form sections. The spec form is a run of unrelated decisions, and a flat list of fields makes
   "which material" and "how many" look like the same kind of question. A hairline groups them —
   material with its processes, quantity, date — with no heading over any block: at two fields a
   heading is more furniture than the grouping is worth, and the rule already does the work. */
.formsection { padding: 20px 0; border-top: 1px solid var(--line); }
.formsection:first-of-type { padding-top: 0; border-top: 0; }
.formsection > .field:last-child { margin-bottom: 0; }

/* The file being specced, at the top of the form. Full filename, never elided: two files in a
   batch often differ only in their last few characters (a colourway, usually). */
.specfile { margin: 0 0 18px; font-size: 16px; font-weight: 600; color: var(--ink); }

/* The "type your own" quantity box needs room for its placeholder, and the calendar tile for a
   date read out in full. */
.tile.free { min-width: 168px; }
.tile.free input { font-size: 20px; }
.tile-field input::placeholder { font-size: 13px; font-weight: 500; color: var(--muted); }

/* The calendar tile. The date field lies over it, transparent: showPicker() anchors the popup to
   the field and refuses to run on one that is not being rendered, so it cannot be display:none.
   Only spec.js adds `enhanced`, so with JavaScript off the plain date field stays visible and
   usable and this rule never applies. */
.tile-date { position: relative; min-width: 124px; }
.tile-date .cal { display: flex; align-items: center; justify-content: center; color: var(--muted); }
.tile-date .cal { color: var(--muted); }
.tile-date.on .cal { color: #fff; }
.tile-date .datelabel { font-size: 12px; color: var(--muted); }
.tile-date.picked .datelabel { font-size: 15px; font-weight: 700; }
.tile-date.on .datelabel { color: #fff; }
.tiles.enhanced .tile-date input {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; pointer-events: none; border: 0; padding: 0;
}

/* The quantity row stays on one line: five boxes reading as one set of choices, which a wrap
   breaks up.

   All five are the same tile — same font, same padding, same caption underneath — and the only
   difference is width, set by what has to fit inside: one numeral for the shortcuts, five for
   the box you type in. Earlier versions gave the typing box a smaller font and `flex: 1`, so it
   swallowed the rest of the row and sat there in a different size from everything beside it. */
.tiles.qty { flex-wrap: nowrap; }
.tiles.qty .tile { flex: 0 0 auto; min-width: 76px; height: 80px; }
.tiles.qty .tile.free { width: 124px; }
.tiles.qty .tile.free input { font-size: 26px; font-weight: 700; line-height: 1.1; }
/* A number input reserves room for its spinner whether or not it is showing, which clipped the
   fifth digit off the widest quantity the box is meant to take. Nobody steps a print run up one
   copy at a time with an arrow, so the spinner goes and the digits get the space. */
.tiles.qty .tile.free input::-webkit-outer-spin-button,
.tiles.qty .tile.free input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.tiles.qty .tile.free input { -moz-appearance: textfield; appearance: textfield; }
@media (max-width: 760px) {
  /* Below this the five will not fit honestly, so let them wrap rather than squeeze. */
  .tiles.qty { flex-wrap: wrap; }
  .tiles.qty .tile.free { min-width: 100%; }
}
