/* ============================================================================
   printopus — redesign stylesheet
   One file, no framework, no build step (matches the existing src/web/ui.rs
   approach). Everything is driven by custom properties so the two halves of the
   app share one set of rules: the backoffice is the default (dark, slate
   neutrals, orange accent) and the customer side is <body class="guest">
   (light, teal).

   Order: tokens · reset · type · shell · cards · tables · lists · board ·
          forms · chips & pills · previews · customer side · print
   ========================================================================= */

/* ---------------------------------------------------------------- tokens -- */
:root {
  /* backoffice: a workshop screen that stays open all day. Dark whatever the
     machine prefers, cool slate so the orange accent carries the weight. */
  --bg:            oklch(0.175 0.012 265);
  --panel:         oklch(0.225 0.013 265);
  --panel-raised:  oklch(0.238 0.013 265);
  --panel-bar:     oklch(0.205 0.013 265);
  --field:         oklch(0.190 0.012 265);

  --line:          oklch(0.300 0.012 265);
  --line-mid:      oklch(0.280 0.012 265);
  --line-soft:     oklch(0.245 0.012 265);

  --ink:           oklch(0.950 0.008 265);
  --muted:         oklch(0.700 0.014 265);
  --dim:           oklch(0.580 0.014 265);
  --faint:         oklch(0.520 0.012 265);

  --accent:        oklch(0.720 0.160 50);   /* button fill */
  --accent-hover:  oklch(0.780 0.150 52);
  --accent-on:     oklch(0.170 0.030 50);   /* text on accent fill */
  --accent-text:   oklch(0.840 0.130 58);   /* accent as text */
  --accent-soft:   oklch(0.270 0.040 55);   /* active nav, selected seg */
  --accent-line:   oklch(0.420 0.070 55);
  --accent-panel:  oklch(0.245 0.030 55);   /* recommendation banners */

  --ok:            oklch(0.840 0.120 155);
  --ok-soft:       oklch(0.300 0.050 155);
  --ok-dot:        oklch(0.720 0.130 155);

  --warn:          oklch(0.860 0.120 85);
  --warn-soft:     oklch(0.320 0.050 85);
  --warn-dot:      oklch(0.780 0.140 82);

  --bad:           oklch(0.720 0.160 25);
  --bad-line:      oklch(0.550 0.140 25);
  --bad-soft:      oklch(0.300 0.050 25);

  --info:          oklch(0.820 0.060 200);
  --info-soft:     oklch(0.270 0.020 200);
  --petg:          #16b6c9;

  /* the model-preview placeholder. In the real app this is replaced by the
     picture src/preview.rs draws; the hatch is only for the design files. */
  --hatch-a:       oklch(0.310 0.016 265);
  --hatch-b:       oklch(0.275 0.016 265);
  --hatch-ink:     oklch(0.700 0.016 265);

  --quote-bg:      oklch(0.245 0.014 265);

  /* A box you type in, a box that is empty, and the mark on a box you choose
     from. Telling these apart was left to one colour and 17 px of invisible
     padding, which is not enough to tell them apart by. */
  --field-empty:   oklch(0.165 0.012 265);
  --line-empty:    oklch(0.250 0.012 265);
  --placeholder:   oklch(0.600 0.012 265);
  --chevron: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'><path d='M1.6 3.6 5 7 8.4 3.6' fill='none' stroke='%239A9EA7' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/></svg>");

  --font:          'Space Grotesk', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --mono:          'Space Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  --r-xs: 6px;  --r-sm: 7px;  --r-md: 9px;  --r-lg: 11px;
  --r-xl: 14px; --r-2xl: 16px; --r-pill: 999px;

  --gutter: 24px;          /* page padding, left and right */
  --shell-max: 1680px;     /* backoffice content width */
  --topbar-h: 52px;
  --contextbar-h: 46px;
}

/* ------------------------------------------------------------------ reset -- */
* { box-sizing: border-box; }

html, body { margin: 0; padding: 0; }

/* Any class that sets `display` beats the browser's own rule for [hidden], and
   the pages hide buttons and pictures with it. One rule, once, rather than a
   pairing for every class that could be hidden. */
[hidden] { display: none !important; }
/* the context bar reaches past the page to the window edge; clip rather than
   scroll for the half-scrollbar that costs. Not `hidden`: that would make the
   body a scroll container and the sticky bars would stop sticking. */
body { overflow-x: clip; }

body {
  background: var(--bg);
  color: var(--ink);
  font: 400 14px/1.5 var(--font);
  -webkit-font-smoothing: antialiased;
}

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

::selection { background: oklch(0.45 0.10 55); color: #fff; }
/* Legible enough to read as a prompt, which the old one was not — it sat at
   3.3:1. What keeps it from being mistaken for an answer is no longer its
   colour but the shape of the box it is in: see :placeholder-shown below. */
::placeholder { color: var(--placeholder); }

img { max-width: 100%; display: block; }

/* ------------------------------------------------------------------- type -- */
h1, h2, h3 { margin: 0; letter-spacing: -0.02em; font-weight: 600; }

.mono { font-family: var(--mono); }
.num  { font-family: var(--mono); font-variant-numeric: tabular-nums; text-align: right; }
.muted { color: var(--muted); }
.dim   { color: var(--dim); }
.wrap  { overflow-wrap: anywhere; }

/* one line, ellipsised. Uploaded file names are arbitrary; they must not be
   allowed to push a row out of shape. */
.trunc {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* the small uppercase mono label used for every section heading */
.eyebrow {
  font: 700 12px/1 var(--mono);
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--muted);
}
.eyebrow--accent { color: var(--accent-text); }
.eyebrow--sm { font-size: 11px; letter-spacing: 0.11em; }

/* ------------------------------------------------------------------ shell -- */
/* One deviation from the handoff: the context bar is rendered by each page
   rather than by the shell, because only the page knows its own title and
   counts. It is sticky just under the top bar, which puts it where the design
   has it without the layout having to know what is going on inside it. */
.topbar {
  position: sticky;
  top: 0;
  z-index: 20;
  background: var(--panel-bar);
  border-bottom: 1px solid var(--line);
}

.topbar__row {
  display: flex;
  align-items: center;
  gap: 28px;
  height: var(--topbar-h);
  max-width: var(--shell-max);
  margin: 0 auto;
  padding: 0 var(--gutter);
}

.brand {
  display: flex;
  align-items: baseline;
  gap: 7px;
  flex: none;
  color: var(--ink);
}
.brand__name { font: 700 16px/1 var(--font); letter-spacing: -0.03em; }
/* the same octopus the tab wears, so the page and its tab read as one thing */
.brand__mark { width: 17px; height: 17px; align-self: center; flex: none; }
.brand__sub {
  font: 400 10px/1 var(--mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--dim);
}

/* primary navigation: only the four things done every day. Printers,
   filaments and build plates live inside Setup. */
.nav { display: flex; gap: 2px; flex: none; }
.nav__item {
  display: flex;
  align-items: center;
  gap: 7px;
  border: 0;
  border-radius: var(--r-md);
  padding: 6px 12px;
  background: transparent;
  color: var(--muted);
  font: 500 14px/1 var(--font);
  cursor: pointer;
}
.nav__item:hover { color: var(--ink); }
.nav__item[aria-current="page"],
.nav__item.is-active {
  background: var(--accent-soft);
  color: var(--accent-text);
}
.nav__badge {
  min-width: 16px;
  height: 16px;
  border-radius: 8px;
  background: oklch(0.68 0.17 40);
  color: oklch(0.16 0.02 40);
  font: 700 10px/16px var(--mono);
  text-align: center;
}

.topbar__tools {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
  min-width: 0;          /* must be able to compress before the page scrolls */
  flex: 0 1 auto;
}
.topbar__sep { width: 1px; height: 20px; background: var(--line); flex: none; }

/* who is signed in, and the form that ends it — a form so signing out stays
   a POST, drawn as if it were only a button. A long name gives way with an
   ellipsis rather than pushing the bar past the window. */
.topbar__who {
  font-size: 13px;
  color: var(--muted);
  white-space: nowrap;
  min-width: 0;
  max-width: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
}
.topbar__signout { display: contents; }
a.topbar__who:hover { color: var(--ink); }
/* below a wide desktop the keystroke hint goes; the name stays at every
   width, because it is no longer a caption — it is the door to the account
   menu, and hiding it locked phones out of setup, language and sign-out */
@media (max-width: 1180px) {
  .topbar__sep--who, .topbar__tools .kbd { display: none; }
}

/* ------------------------------------------------------------------ login -- */
/* One card in the middle of the dark shell: the way into the backoffice. */
.login { display: grid; place-items: center; padding: 12vh 16px 0; }
/* full width on a phone, a hand's width on anything larger — measured
   against the room actually given, not the viewport, so the page gutters
   are respected too */
.login__card { width: min(380px, 100%); display: grid; gap: 14px; min-width: 0; }
/* the card is a grid, not the contextbar's flex line: the title and its
   sub-line wrap here instead of leaning on a truncating flex parent that
   this card does not have */
.login__card .page-title,
.login__card .page-meta {
  white-space: normal;
  overflow-wrap: anywhere;
  line-height: 1.5;
}
.login__no { margin: 0; font-size: 13px; color: var(--bad); }

.linkout {
  display: inline-flex;
  align-items: center;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  padding: 5px 12px;
  font-size: 13px;
  color: var(--muted);
  white-space: nowrap;
}
.linkout:hover { border-color: var(--accent-line); color: var(--accent-text); text-decoration: none; }

/* second row: what page you are on, its numbers, and that page's controls */
.contextbar {
  position: sticky;
  top: var(--topbar-h);
  z-index: 19;
  display: flex;
  align-items: center;
  gap: 20px;
  height: var(--contextbar-h);
  margin: 0 auto;
  padding: 0 var(--gutter);
  background: var(--panel-bar);
  border-top: 1px solid var(--line-soft);
  border-bottom: 1px solid var(--line);
}
.contextbar__inner {
  display: flex;
  align-items: center;
  gap: 20px;
  width: 100%;
  max-width: var(--shell-max);
  margin: 0 auto;
}
.contextbar__title {
  display: flex;
  align-items: baseline;
  gap: 10px;
  min-width: 0;
  flex: 0 1 auto;
}
.page-title {
  font: 600 15px/1 var(--font);
  /* a job's title is "Jobs / <model> → <printer>", which is as long as the two
     names are. It gives way like the line under it rather than pushing the
     page past the window. */
  white-space: nowrap;
  min-width: 0;
  flex: 0 1 auto;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* the line under the title says as much as there is room for and no more: the
   tools on the right are worked with, this is only read */
.page-meta {
  font: 400 13px/1 var(--mono);
  color: var(--dim);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.contextbar__tools { display: flex; align-items: center; gap: 8px; margin-left: auto; flex: none; }

/* A named dialog: the omni already proved the element, this dresses the
   others — the upload for now — in the panel's own clothes. */
.modal {
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  background: var(--panel-raised);
  color: var(--ink);
  padding: 20px 22px;
  width: min(820px, calc(100vw - 32px));
}
.modal::backdrop { background: rgb(0 0 0 / 0.55); }
.uploader.uploader--modal { display: grid; gap: 14px; justify-items: start; }
.uploader.uploader--modal .filepick__name { max-width: none; font-size: 13px; }
.modal__row { display: flex; justify-content: flex-end; gap: 8px; width: 100%; }

/* What the browser could see about the chosen files before any of them was
   sent: one row each, the picture a project carries of its own plate, and the
   name the file will land under. Empty and out of the way until a file is
   chosen, and never there at all without the script that builds it. */
.preflight {
  display: grid;
  gap: 9px;
  width: 100%;
  /* a picker's whole armful is a dozen rows, and a dialog taller than the
     window hides its own buttons: the rows scroll, the total below them does
     not, because that sentence is the one that decides whether to press it */
  max-height: min(56vh, 520px);
  overflow-y: auto;
}
.preflight__row {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  border: 1px solid var(--line-soft);
  border-radius: var(--r-md);
  padding: 9px 10px;
  background: var(--field);
}
/* a file that cannot be a model at all, marked before it costs an upload */
.preflight__row.is-refused { border-color: var(--bad-line); background: var(--bad-soft); }
.preflight__shot {
  flex: none;
  width: 46px;
  height: 46px;
  border-radius: var(--r-sm);
  overflow: hidden;
  background: var(--panel);
}
.preflight__shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.preflight__body {
  flex: 1 1 auto;
  min-width: 0;
  display: grid;
  gap: 4px 14px;
  align-items: center;
  /* wide enough for both: what the file is on the left, what it will be
     called on the right. Narrow windows stack them again. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 20rem);
}
.preflight__file, .preflight__note { grid-column: 1; }
.preflight__body .preflight__title { grid-column: 2; grid-row: 1 / span 2; }
@media (max-width: 720px) {
  .preflight__body { grid-template-columns: minmax(0, 1fr); }
  .preflight__body .preflight__title { grid-column: 1; grid-row: auto; }
}
.preflight__file { display: flex; align-items: baseline; gap: 8px; min-width: 0; margin: 0; }
.preflight__name {
  font: 500 13px/1.3 var(--font);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.preflight__size { flex: none; font: 400 12px/1.3 var(--mono); color: var(--muted); }
.preflight__note { margin: 0; font-size: 12px; color: var(--dim); }
.preflight__title { padding: 7px 10px; font-size: 13px; }
.preflight__total { margin: 0; font-size: 13px; color: var(--muted); }
/* on the customer's side the same rows sit inside the drop box rather than a
   dialog, and there is one of them at most */
.preflight--guest { margin-top: 12px; text-align: left; }
/* the one sentence that stands between a picker and a refused upload — and
   the button it turns off, which has to look off as well as be off */
.preflight__total.is-over { color: var(--bad); font-weight: 500; }
/* a file that cannot be a model has nothing to be called: the box stays in
   the form, because the names are paired with the files by position, but
   there is no reason to invite anyone to type in it */
.preflight__row.is-refused .input { display: none; }
.btn:disabled { opacity: 0.45; cursor: not-allowed; }

/* A contextbar carrying filters stacks: the title row keeps its line, the
   narrowing controls get their own beneath it, inside the same bar. */
.contextbar--filters {
  height: auto;
  flex-direction: column;
  align-items: stretch;
  gap: 9px;
  padding-top: 9px;
  padding-bottom: 10px;
}
.contextbar--filters .contextbar__inner { min-height: 0; }
.contextbar__filterrow {
  width: 100%;
  max-width: var(--shell-max);
  margin: 0 auto;
}
.contextbar__filterrow .filters { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }

.main {
  max-width: var(--shell-max);
  margin: 0 auto;
  padding: 22px var(--gutter) 72px;
}
/* The page renders its own context bar as the first thing in <main>, so it has
   to break back out of the padding to span the window; its contents still line
   up with the page. */
.main > .contextbar:first-child {
  max-width: none;
  /* half the content box out to half the window: spans the window at any
     width, and lines its own contents back up with the page */
  margin: -22px calc(50% - 50vw) 22px;
}

/* two-column arrangements. --rail: work on the left, machines on the right.
   --detail: the object on the left, what you do to it on the right. */
.layout { display: grid; gap: 18px; align-items: start; }
.layout--rail   { grid-template-columns: minmax(0, 1fr) 380px; }
.layout--detail { grid-template-columns: 340px minmax(0, 1fr); }
.layout > * { min-width: 0; }

/* --catalog: the section's own lists on the left, the one being read on the
   right. Narrower than --detail's 340: this rail holds three words, not a
   model. */
.layout--catalog { grid-template-columns: 196px minmax(0, 1fr); }

.stack { display: grid; gap: 18px; min-width: 0; }
.stack > * { min-width: 0; }
.stack--tight { gap: 12px; }

@media (min-width: 1101px) { .sticky { position: sticky; top: calc(var(--topbar-h) + var(--contextbar-h) + 14px); } }
@media (max-width: 1100px) {
  .layout--rail, .layout--detail, .layout--catalog {
    grid-template-columns: minmax(0, 1fr);
  }
  /* with no column to stand in, the rail lies down and scrolls sideways
     rather than eating a third of a phone */
  .subnav {
    grid-auto-flow: column;
    justify-content: start;
    gap: 6px;
    overflow-x: auto;
    padding-bottom: 2px;
  }
  .subnav__item { white-space: nowrap; }
}

/* the lists inside one section of the app, and how much is in each */
.subnav { display: grid; gap: 2px; }
.subnav__item {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 11px;
  border-radius: var(--r-md);
  color: var(--muted);
  font: 500 14px/1 var(--font);
}
.subnav__item:hover { color: var(--ink); background: var(--field); }
.subnav__item[aria-current="page"] {
  background: var(--accent-soft);
  color: var(--accent-text);
}
.subnav__count {
  margin-left: auto;
  font: 400 12px/1 var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--faint);
}
.subnav__item[aria-current="page"] .subnav__count { color: inherit; }

/* ------------------------------------------------------------------ cards -- */
.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  min-width: 0;
}
.card--flush { overflow: hidden; }          /* rows run to the card edge */
.card--pad { padding: 16px; }
.card--accent { border-color: var(--accent-line); }

.card__head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 13px 16px;
  border-bottom: 1px solid var(--line-mid);
}
.card__head--accent { background: var(--accent-panel); }
.card__note { font: 400 13px/1.3 var(--mono); color: var(--dim); }
.card__action { margin-left: auto; flex: none; }
.card__body { padding: 14px 16px; }
.card__lede {
  margin: 0;
  padding: 14px 16px;
  border-bottom: 1px solid var(--line-mid);
  font-size: 13px;
  color: var(--muted);
  max-width: 80ch;
}

/* the recommendation banner above a request's lines */
.banner {
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  background: var(--accent-panel);
  border: 1px solid var(--accent-line);
  border-radius: var(--r-xl);
  padding: 15px 16px;
}
.banner__text { min-width: 0; flex: 1 1 auto; }
/* the files an upload could not take, under the tally of the ones it did */
.banner__list { margin: 6px 0 0; padding-left: 18px; }
.banner__list li { margin-top: 2px; }
.banner__sub { font-size: 13px; color: oklch(0.76 0.02 60); margin-top: 3px; }

/* a quoted sentence from a customer */
.quote {
  margin: 13px 0 0;
  padding: 11px 13px;
  border-left: 2px solid var(--accent-line);
  border-radius: 0 var(--r-md) var(--r-md) 0;
  background: var(--quote-bg);
  color: var(--ink);
  font-size: 13px;
  line-height: 1.55;
}

/* key/value list in a detail panel */
.facts { display: grid; gap: 7px; font-size: 13px; }
.facts > div { display: flex; gap: 10px; }
.facts dt, .facts__key { flex: 0 0 72px; color: var(--dim); margin: 0; }
.facts dd, .facts__val { margin: 0; white-space: pre-line; }

/* measurements: label left, tabular figure right, hairline between */
.specs { display: grid; font-size: 13px; }
.specs > div {
  display: flex;
  justify-content: space-between;
  padding: 7px 0;
  border-bottom: 1px solid var(--line-mid);
}
.specs > div:last-child { border-bottom: 0; }
.specs .v { font: 400 13px/1 var(--mono); }

/* The job facts grid: hairline cells, no table needed. Each cell draws its own
   1px ring; rings of neighbours land on the same line and merge, a partial
   row's last cell caps itself, and the container's border swallows the rest —
   the old version painted the whole backdrop in line grey and let cells cover
   it, which turned every unfilled track into a large empty field that meant
   nothing once Process and the md5 took rows of their own. */
.factgrid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  background: var(--panel);
  border: 1px solid var(--line-mid);
  border-radius: var(--r-lg);
  overflow: hidden;
}
.factgrid > div {
  background: var(--panel);
  padding: 12px 14px;
  box-shadow: 0 0 0 1px var(--line-mid);
}
.factgrid dt {
  font: 400 11px/1 var(--mono);
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--dim);
}
.factgrid dd { margin: 6px 0 0; font: 400 15px/1.3 var(--mono); overflow-wrap: anywhere; }
/* a preset name and an md5 are long strings, not figures: they take a row of
   their own. With a full row there is no reason left to shrink them; the md5
   alone steps back in colour, being the least-read fact on the page */
.factgrid__wide { grid-column: 1 / -1; }
.factgrid__quiet { color: var(--muted); }

/* ----------------------------------------------------------------- tables -- */
/* Real tables for tabular screens. Columns are minmax(0, …fr) so a long file
   name ellipsises instead of spilling out of the card. */
.data { width: 100%; border-collapse: collapse; table-layout: fixed; }
.data th, .data td {
  text-align: left;
  padding: 11px 16px;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: middle;
  /* the columns are a fixed width, so anything rigid inside one — a pill, a
     button — is clipped by its cell rather than laid over its neighbour */
  overflow: hidden;
}
.data thead th {
  font: 700 11px/1 var(--mono);
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: var(--dim);
  padding-top: 10px;
  padding-bottom: 10px;
  border-bottom: 1px solid var(--line);
}
.data tbody tr:last-child td { border-bottom: 0; }
.data tbody tr.is-clickable { cursor: pointer; }
.data tbody tr.is-clickable:hover { background: oklch(0.245 0.014 265); }
.data td.num, .data th.num { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; }
.data td.mono { font: 400 12px/1.4 var(--mono); color: var(--muted); }
.data .col-thumb { width: 72px; }
/* wide enough for the widest thing these columns hold: a "Make default"
   button, and the pill that says "on the printer" */
.data .col-tight { width: 116px; }
/* a column whose whole job is one small button — the button's width, plus the
   icon it wears, plus the cell's own padding */
.data .col-act { width: 140px; }
.data .col-mid   { width: 160px; }
/* "1146 × 1155 × 140 mm" is as wide as a measured size ever gets, and it is
   read as one figure rather than two lines */
.data .col-size  { width: 182px; white-space: nowrap; }

.cell-file { display: flex; align-items: center; gap: 9px; min-width: 0; }
.cell-sub { font: 400 12px/1.7 var(--mono); color: var(--dim); }

/* ------------------------------------------------------------------ lists -- */
/* "Waiting on you": one request per row, fixed height, action on the right. */
.reqlist { list-style: none; margin: 0; padding: 0; }
.req {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--line-soft);
}
.req:last-child { border-bottom: 0; }
.req__text { min-width: 0; flex: 1 1 auto; }
.req__who { display: flex; align-items: baseline; gap: 8px; }
.req__name { font-weight: 600; }
.req__age { font: 400 12px/1 var(--mono); color: var(--dim); }
.req__what { font-size: 13px; color: var(--muted); margin-top: 3px; }

/* a slice that went wrong sits in the same list as the requests, because both
   are waiting on someone — but it is not a person asking for something, so it
   is marked rather than dressed as one */
.req--failed { background: oklch(0.225 0.020 25); }
body.guest .req--failed { background: var(--bad-soft); }
.req--failed .req__who { gap: 9px; }

/* machines rail */
.machines { list-style: none; margin: 0; padding: 0; }
.machine { padding: 11px 16px; border-bottom: 1px solid var(--line-soft); }
.machine:last-child { border-bottom: 0; }
.machine__head { display: flex; align-items: center; gap: 8px; }
.machine__name { font-weight: 600; font-size: 13px; }
.machine__nozzle { margin-left: auto; font: 400 12px/1 var(--mono); color: var(--dim); }
.machine__state { font-size: 12px; color: var(--muted); margin-top: 4px; padding-left: 15px; }
/* the estimate has run out and the print is still on the machine: the words
   turn the warning colour, and a one-press answer sits under them */
.machine__due--over { color: var(--bad); }
.machine__nudge {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 6px;
  font-size: 12px;
  color: var(--muted);
}
/* the rail indents its state line under the dot; the nudge lines up with it */
.machine .machine__nudge { padding-left: 15px; }

/* status dot: state by modifier, never by colour alone — pair it with text */
.dot { width: 7px; height: 7px; border-radius: 50%; flex: none; background: var(--dim); }
.dot--free { background: var(--ok-dot); }
.dot--busy { background: var(--warn-dot); }
.dot--off  { background: var(--faint); }

/* the card's due line: no bar and no percent — nothing measures a print, so
   the card only says when it went on and when the estimate says it comes off */
.progress__left { font: 400 10px/1.6 var(--mono); color: var(--warn); margin-top: 3px; }
.progress__left--over { color: var(--bad); }

/* the run-sheet variant of the dashboard */
.runsheet { list-style: none; margin: 0; padding: 0; }
.runsheet li {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 13px 16px;
  border-bottom: 1px solid var(--line-soft);
}
.runsheet li:last-child { border-bottom: 0; }
.runsheet__n {
  width: 22px; height: 22px;
  border-radius: var(--r-xs);
  flex: none;
  text-align: center;
  background: oklch(0.29 0.012 265);
  color: var(--muted);
  font: 700 12px/22px var(--mono);
}
.runsheet__kind {
  flex: 0 0 80px;
  font: 700 11px/1 var(--mono);
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.runsheet__kind--request { color: oklch(0.82 0.12 45); }
.runsheet__kind--slice   { color: oklch(0.80 0.12 200); }
.runsheet__kind--print   { color: var(--ok); }
.runsheet__kind--failed  { color: var(--bad); }

/* ---------------------------------------------------------- models: cards -- */
/* Fixed track width and justify-content:start, so one model does not inflate
   into a full-width blob and twenty models stay the same size. */
.modelgrid {
  display: grid;
  grid-template-columns: repeat(auto-fill, 236px);
  justify-content: start;
  gap: 16px;
}
.modelcard {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  overflow: hidden;
  cursor: pointer;
}
.modelcard:hover { border-color: var(--accent-line); }
.modelcard__body { padding: 11px 13px 13px; }
.modelcard__name { font-weight: 600; font-size: 14px; }
.modelcard__dims { font: 400 12px/1.6 var(--mono); color: var(--muted); margin-top: 3px; }
.modelcard__foot { display: flex; align-items: center; gap: 8px; margin-top: 8px; font-size: 12px; color: var(--dim); }

/* ------------------------------------------------------------------ board -- */
/* Three lanes: to print, on the printer, printed. Slicing is a banner, because
   an empty fourth column was two thirds of the page doing nothing. */
.board {
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr) minmax(0, 1fr);
  gap: 14px;
  align-items: start;
}

/* On a big enough window the jobs view is a real board: it fills the window
   exactly, the page stops scrolling, and each lane scrolls in itself. Below
   that (narrow columns stacked, or a very short window) the page scrolls as
   pages do. The chain of min-height: 0 is what lets flex hand the height down
   instead of growing to fit. */
@media (min-width: 721px) and (min-height: 560px) {
  body:has(.board) {
    height: 100dvh;
    overflow: hidden;
    display: flex;
    flex-direction: column;
  }
  body:has(.board) .main {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
    padding-bottom: 16px;
  }
  body:has(.board) .main > .stack {
    flex: 1 1 auto;
    min-height: 0;
    display: flex;
    flex-direction: column;
  }
  body:has(.board) .board {
    flex: 1 1 auto;
    min-height: 0;
    align-items: stretch;
  }
  body:has(.board) .lane {
    display: flex;
    flex-direction: column;
    min-height: 0;
  }
  body:has(.board) .lane__cards {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    /* the list now has a fixed height, and a grid with spare room stretches
       its rows into it — order boxes ballooned and their chips with them;
       the cards keep their own height and the space stays at the bottom */
    align-content: start;
  }
  /* an empty lane keeps its drop-here line in the middle of the space
     instead of pinned to the floor by the stretched card list above it */
  body:has(.board) .lane__cards:empty { flex: 0 0 0; }
  body:has(.board) .lane__empty { margin: auto 0; }
}
@media (max-width: 1100px) { .board { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 720px)  { .board { grid-template-columns: minmax(0, 1fr); } }

.lane {
  background: oklch(0.215 0.013 265);
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  padding: 12px;
  min-height: 220px;
}
.lane__head { display: flex; align-items: center; gap: 9px; padding: 2px 4px 12px; }
.lane__title { font: 700 13px/1 var(--mono); letter-spacing: 0.1em; text-transform: uppercase; }
.lane--todo    .lane__title { color: var(--accent-text); }
.lane--printer .lane__title { color: var(--warn); }
.lane--printed .lane__title { color: var(--ok); }
/* the slicing banner's heading, cut from the same cloth as the lanes below
   it, in a quieter colour: nothing here can be dragged yet */
.lane__title--slicing { color: var(--muted); }
.lane__count {
  min-width: 20px; height: 20px;
  border-radius: var(--r-xs);
  text-align: center;
  background: oklch(0.29 0.012 265);
  color: var(--muted);
  font: 700 12px/20px var(--mono);
}
.lane__cards { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px; }

.job {
  display: flex;
  gap: 11px;
  min-width: 0;
  background: var(--panel-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 10px;
  cursor: grab;
}
.job:hover { border-color: var(--accent-line); }
.job.is-dragging { opacity: 0.45; cursor: grabbing; }
.job__text { min-width: 0; flex: 1 1 auto; overflow: hidden; }
.job__head { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.job__model { font-weight: 600; font-size: 13px; }
.job__time { margin-left: auto; flex: none; font: 400 12px/1 var(--mono); color: var(--muted); }
/* An order's cards sit together inside one quiet box, captioned once with
   whose it is; pressing the caption narrows the whole board to that order.
   The workshop's own jobs stay loose in the lane. */
.lane__order {
  border: 1px dashed var(--line);
  border-radius: var(--r-lg);
  padding: 8px;
  display: grid;
  gap: 8px;
}
.lane__order-cards { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px; }
.lane__order .job__for { justify-self: start; }

/* whose order a card belongs to, worn by the group caption and the slicing
   banner's lines; pressing it shows only that order */
.job__for {
  display: inline-flex;
  align-items: center;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  padding: 1px 8px 2px;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--field);
  font: 400 11px/1.6 var(--mono);
  color: var(--muted);
}
.job__for:hover { border-color: var(--accent-line); color: var(--accent-text); text-decoration: none; }

/* three cards of one project carry the same name; the plate is what tells them
   apart, so it sits in the headline rather than third in a grey line */
.job__plate {
  flex: none;
  padding: 1px 7px 2px;
  border-radius: var(--r-pill);
  background: var(--field);
  font: 400 10px/1.6 var(--mono);
  color: var(--muted);
}
/* One fact a line on a card, a small mark in front of each. The icons are
   inline SVG data masks painted with currentColor, so they dim with the text
   and cost no route and no file — the favicon's reasoning, one floor down. */
.job__facts { display: grid; gap: 3px; margin: 5px 0 0; padding: 0; list-style: none; }
.job__fact { display: flex; align-items: center; gap: 7px; font-size: 12px; color: var(--muted); min-width: 0; }
.job__fact:last-child { font: 400 12px/1.5 var(--mono); color: var(--dim); }
.job__fact .icon { opacity: 0.85; }
.lane.is-over { border-color: var(--accent-line); background: var(--accent-panel); }
/* the invitation goes as soon as the lane has something in it */
.lane__cards:not(:empty) + .lane__empty { display: none; }
.lane__empty {
  border: 1px dashed var(--line);
  border-radius: var(--r-lg);
  padding: 16px;
  text-align: center;
  font-size: 13px;
  color: var(--faint);
}
.lane__banner-item { display: inline-flex; align-items: center; gap: 7px; min-width: 0; }
.lane__banner--bad { border-color: var(--bad-line); color: var(--bad); }
.lane__banner--bad .eyebrow { color: var(--bad); }

.lane__banner {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 12px;
  min-width: 0;
  background: oklch(0.215 0.013 265);
  border: 1px dashed var(--line);
  border-radius: var(--r-lg);
  padding: 11px 16px;
  font-size: 13px;
  color: var(--muted);
}

/* ------------------------------------------------------------------ forms -- */
.field { display: grid; gap: 5px; min-width: 0; }
.field__label {
  font: 400 12px/1.2 var(--mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--dim);
}
.field__label--required { color: var(--accent-text); }
/* what a field is for, when the label is the question and this is the caveat */
.field__hint { font-size: 12px; color: var(--dim); margin-top: -2px; }

.input, .select, .textarea {
  width: 100%;
  background: var(--field);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 9px 11px;
  color: var(--ink);
  font: 400 13px/1.4 var(--font);
}
.input--num { font-family: var(--mono); text-align: right; }
/* the browser's own up/down arrows sit on top of the number in a box this
   narrow, and every one of these has something better beside it: a stepper on
   the customer's side, the keyboard everywhere else */
.input--num { appearance: textfield; -moz-appearance: textfield; }
.input--num::-webkit-outer-spin-button,
.input--num::-webkit-inner-spin-button { appearance: none; margin: 0; }
/* `appearance: none` takes the browser's arrow away, and nothing was put back:
   a dropdown and a text box were the same rectangle, differing only by padding
   nobody can see. This draws the arrow, in the same thin stroke as every other
   icon in the app. */
.select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 32px;
  background-image: var(--chevron);
  background-repeat: no-repeat;
  background-position: right 11px center;
  cursor: pointer;
}
.select--sm { background-position: right 8px center; }
.textarea { min-height: 72px; resize: vertical; line-height: 1.55; }
/* Nothing in it yet, said by the box rather than by the shade of the words
   inside it. Colour alone had to carry "this is a prompt, not an answer", and
   it could not: a placeholder legible enough to read is close enough to an
   answer to be mistaken for one. */
.input:placeholder-shown,
.textarea:placeholder-shown {
  background: var(--field-empty);
  border-color: var(--line-empty);
}
/* and an answer carries a little more weight than a prompt does, which is a
   difference in shape rather than in shade — the one thing a shade cannot do
   is be both legible and obviously not an answer */
.input:not(:placeholder-shown),
.textarea:not(:placeholder-shown),
.select { font-weight: 500; }
/* a box that cannot be typed into should not be holding out an example */
.input:disabled::placeholder, .textarea:disabled::placeholder { color: transparent; }

.input:focus, .select:focus, .textarea:focus { outline: none; border-color: var(--accent-line); }
.input:focus::placeholder, .textarea:focus::placeholder { opacity: 0.55; }
.input:disabled, .select:disabled, .textarea:disabled { opacity: 0.5; cursor: not-allowed; }

/* a filter row inside the context bar: everything on one line, small */
.filters { display: flex; align-items: center; gap: 8px; }
.select--sm, .filters .search { padding: 5px 9px; font-size: 12px; width: auto; }
.select--sm { padding-right: 26px; }

.fieldrow { display: flex; flex-wrap: wrap; gap: 12px; align-items: end; }
/* Some rows hold a control that is taller than the rest — a size choice with a
   note under it — and bottom-aligning the row then drops every label to the
   floor. These line their boxes up instead, which means the button has to be
   pushed past the labels by hand. */
.fieldrow--tops { align-items: start; }
.fieldrow--tops > .btn { margin-top: 17px; }
.fieldrow > .field { flex: 1 1 200px; }
.fieldgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 12px; }

/* The small marks in front of words: inline SVG data masks painted with
   currentColor, so they take whatever ink their word has — hover, danger,
   either theme — and cost no route and no file. Each modifier is only the
   shape; the base class is the paint. */
.icon {
  width: 13px; height: 13px; flex: none;
  display: inline-block; vertical-align: -2px;
  background: currentColor;
  -webkit-mask: var(--icon) center / contain no-repeat;
  mask: var(--icon) center / contain no-repeat;
}
.icon--printer { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.5 6V2.5h7V6'/%3E%3Crect x='2' y='6' width='12' height='5.5' rx='1'/%3E%3Cpath d='M5 9.5h6v4H5z'/%3E%3C/svg%3E"); }
.icon--size { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 9.5v4h4M2.7 13.3L7 9M13.5 6.5v-4h-4M13.3 2.7L9 7'/%3E%3C/svg%3E"); }
.icon--weight { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 4.5a2 2 0 1 1 4 0'/%3E%3Cpath d='M4.3 4.5h7.4l1.6 8a1 1 0 0 1-1 1.2H3.7a1 1 0 0 1-1-1.2z'/%3E%3C/svg%3E"); }
.icon--search { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='7' cy='7' r='4.5'/%3E%3Cpath d='M10.3 10.3L14 14'/%3E%3C/svg%3E"); }
.icon--person { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='8' cy='4.8' r='2.7'/%3E%3Cpath d='M2.8 14.2a5.2 5.2 0 0 1 10.4 0'/%3E%3C/svg%3E"); }
.icon--gear { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='8' cy='8' r='2.4'/%3E%3Cpath d='M8 1.8v2.4M8 11.8v2.4M1.8 8h2.4M11.8 8h2.4M3.6 3.6l1.7 1.7M10.7 10.7l1.7 1.7M12.4 3.6l-1.7 1.7M5.3 10.7l-1.7 1.7'/%3E%3C/svg%3E"); }
.icon--globe { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='8' cy='8' r='5.7'/%3E%3Cellipse cx='8' cy='8' rx='2.6' ry='5.7'/%3E%3Cpath d='M2.3 8h11.4'/%3E%3C/svg%3E"); }
.icon--enter { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M9.5 2.5h3v11h-3'/%3E%3Cpath d='M2.5 8h7M7 5.5L9.5 8L7 10.5'/%3E%3C/svg%3E"); }
.icon--exit { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6.5 2.5h-3v11h3'/%3E%3Cpath d='M6.5 8h7M11 5.5L13.5 8L11 10.5'/%3E%3C/svg%3E"); }
.icon--check { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 8.6l3.9 3.9 7.1-9'/%3E%3C/svg%3E"); }
.icon--x { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3.5 3.5l9 9M12.5 3.5l-9 9'/%3E%3C/svg%3E"); }
.icon--trash { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.5 4.3h11M6.2 4.3V2.7h3.6v1.6M4.2 4.3l.7 9.2h6.2l.7-9.2'/%3E%3C/svg%3E"); }
.icon--retry { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M13.5 8a5.5 5.5 0 1 1-1.6-3.9'/%3E%3Cpath d='M13.7 1.6v3.2h-3.2'/%3E%3C/svg%3E"); }
.icon--plus { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 2.5v11M2.5 8h11'/%3E%3C/svg%3E"); }
.icon--upload { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 10.2V2.6M4.9 5.6L8 2.5l3.1 3.1'/%3E%3Cpath d='M2.5 10.5v3.2h11v-3.2'/%3E%3C/svg%3E"); }
.icon--download { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M8 2.3v7.6M4.9 6.8L8 9.9l3.1-3.1'/%3E%3Cpath d='M2.5 10.5v3.2h11v-3.2'/%3E%3C/svg%3E"); }
.icon--funnel { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.3 2.8h11.4L9.6 8v4.4l-3.2 1.4V8z'/%3E%3C/svg%3E"); }
.icon--play { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4.8 2.8l7.6 5.2-7.6 5.2z'/%3E%3C/svg%3E"); }
.icon--copy { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='5.3' y='5.3' width='8.2' height='8.2' rx='1'/%3E%3Cpath d='M10.7 2.5H3.5a1 1 0 0 0-1 1v7.2'/%3E%3C/svg%3E"); }
.icon--key { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='4.9' cy='11.1' r='2.6'/%3E%3Cpath d='M6.8 9.2l6.7-6.7M10.6 5.4l2.4 2.4'/%3E%3C/svg%3E"); }
.icon--arrow-right { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M2.3 8h11.4M9.4 3.7L13.7 8l-4.3 4.3'/%3E%3C/svg%3E"); }
.icon--send { --icon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M13.8 2.2L2.2 7l4.4 2.4 2.4 4.4zM13.8 2.2L6.6 9.4'/%3E%3C/svg%3E"); }

.btn {
  /* an <a class="btn"> is inline unless it is told otherwise, and an inline box
     with padding is drawn over whatever is above it rather than pushing it away */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  border: 1px solid transparent;
  border-radius: var(--r-md);
  padding: 9px 16px;
  font: 600 13px/1 var(--font);
  cursor: pointer;
  background: var(--accent);
  color: var(--accent-on);
  white-space: nowrap;
}
.btn:hover {
  background: var(--accent-hover);
  /* a filled button that is an <a>: bare a:hover outranks .btn (0-1-1 beats
     0-1-0) and paints the text in the same accent-hover as this background —
     orange on orange, a button with nothing on it. Restate the ink here, where
     the pseudo-class lifts the specificity past the anchor rule. */
  color: var(--accent-on);
}
/* and no underline sneaking onto any button that happens to be a link */
a.btn:hover { text-decoration: none; }
.btn--lg { padding: 11px 24px; font-size: 14px; }
.btn--ghost {
  background: transparent;
  border-color: var(--line);
  color: var(--muted);
  font-weight: 500;
}
.btn--ghost:hover { border-color: var(--accent-line); color: var(--accent-text); background: transparent; }
.btn--sm { padding: 5px 11px; font: 500 12px/1.3 var(--font); }
.btn--danger { background: transparent; border-color: var(--line); color: var(--muted); font-weight: 500; }
.btn--danger:hover { background: transparent; border-color: var(--bad-line); color: var(--bad); }
.btn--dashed {
  background: transparent;
  border: 1px dashed oklch(0.40 0.012 265);
  color: var(--accent-text);
  font-weight: 500;
}
.btn--dashed:hover {
  border-color: var(--accent-line);
  background: var(--accent-panel);
  color: var(--accent-text);
}

/* Whether a model is on the public form is a state someone has to be able to
   read across the room, so the button wears it: an invitation in its own
   colour, and a plain green once it is live. Hovering the green one says what
   pressing it would do. */
.btn--offer {
  background: transparent;
  border-color: var(--info);
  color: var(--info);
  font-weight: 500;
}
.btn--offer:hover { background: var(--info-soft); border-color: var(--info); }
.btn--offered {
  background: var(--ok-soft);
  border-color: var(--ok);
  color: var(--ok);
  font-weight: 600;
}
.btn--offered:hover { background: var(--bad-soft); border-color: var(--bad-line); color: var(--bad); }
.btn__tick { font-size: 12px; line-height: 1; }
.btn--block { display: flex; width: 100%; }

/* segmented control: page variants, job state, setup tabs */
.seg {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  border-radius: var(--r-md);
  background: var(--field);
  border: 1px solid var(--line);
}
.seg--lg { padding: 4px; border-radius: var(--r-lg); }
.seg__btn {
  display: inline-flex;
  align-items: center;
  border: 0;
  border-radius: var(--r-sm);
  padding: 5px 12px;
  background: transparent;
  color: var(--muted);
  font: 500 12px/1 var(--font);
  cursor: pointer;
  white-space: nowrap;
}
.seg--lg .seg__btn {
  display: inline-flex;
  align-items: center; padding: 8px 16px; font-size: 13px; }
.seg__btn[aria-pressed="true"], .seg__btn.is-on { background: var(--accent-soft); color: var(--accent-text); }
.seg__btn--fill[aria-pressed="true"] { background: var(--accent); color: var(--accent-on); }

.search {
  flex: 0 1 200px;
  min-width: 90px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 6px 10px;
  color: var(--ink);
  font-size: 13px;
}
.search:focus { outline: none; border-color: var(--accent-line); }

/* ------------------------------------------------------------ search, ⌘K -- */
/* The /search page and the ⌘K overlay show the same rows: the page renders
   them into #search-results, search.js lifts that markup into the dialog. So
   the rows are classed once and both surfaces read from here. */
.kbd {
  flex: none;
  margin-left: 8px;
  padding: 2px 5px;
  border: 1px solid var(--line);
  border-radius: var(--r-xs);
  background: var(--bg);
  font: 700 10px/1 var(--mono);
  color: var(--dim);
}

#omni {
  width: min(640px, 92vw);
  margin: 12vh auto auto;   /* top-aligned: results grow downward, not around */
  padding: 0;
  border: 1px solid var(--line-mid);
  border-radius: var(--r-lg);
  background: var(--panel-raised);
  color: var(--ink);
}
#omni::backdrop { background: oklch(0.12 0.01 265 / 0.55); }

.omni__input {
  width: 100%;
  padding: 13px 16px;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  background: var(--field);
  color: var(--ink);
  font: 400 15px/1.3 var(--font);
}
.omni__input:focus { outline: none; }

.omni__results { max-height: 55vh; overflow-y: auto; padding: 8px; }

.omni-group + .omni-group { margin-top: 10px; }
.omni-group > .eyebrow { display: block; padding: 6px 8px 5px; }
.omni-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }

.omni__row {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 6px 8px;
  border-radius: var(--r-md);
  color: var(--ink);
}
.omni__row:hover { background: var(--line-soft); text-decoration: none; }
.omni__row.is-active { background: var(--accent-soft); }
.omni__thumb { flex: none; width: 42px; display: grid; place-items: center; }
/* what this row answered to, when its own words do not say — searching a tag
   lists models whose names never mention it, and a hit that cannot explain
   itself reads like a wrong answer */
.omni__why {
  flex: none;
  margin-left: auto;
  max-width: 18ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font: 400 11px/1.6 var(--mono);
  color: var(--dim);
  padding-left: 10px;
}
.omni__text { min-width: 0; display: grid; }
.omni__name { font-size: 13px; font-weight: 500; }
.omni__sub { font-size: 12px; color: var(--muted); }

.omni-more { display: inline-block; padding: 5px 8px; font-size: 12px; color: var(--accent-text); }

/* the foot of a list that goes on: one link, centred under what it follows */
.pager { margin: 12px 0 0; text-align: center; }

.omni__hint {
  margin: 0;
  padding: 9px 16px;
  border-top: 1px solid var(--line-soft);
  font: 400 11px/1 var(--mono);
  color: var(--dim);
}

/* ---------------------------------------------------------- chips & pills -- */
/* A filament is chosen by its colour, so the colour has to be visible; a
   <select> cannot show it, which is why these are real inputs. */
.chips { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  padding: 7px 13px;
  background: transparent;
  color: var(--ink);
  font-size: 13px;
  cursor: pointer;
}
.chip:hover { border-color: var(--accent-line); }
.chip input { position: absolute; opacity: 0; width: 0; height: 0; }
.chip:has(input:checked), .chip.is-on {
  border-color: var(--accent-line);
  background: var(--accent-soft);
  font-weight: 500;
}
.chip:has(input:focus-visible) { outline: 2px solid var(--accent-line); outline-offset: 2px; }

/* ranked filament list on a request: name left, why it is ranked there right */
.picklist { display: grid; gap: 6px; }
.pick {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  cursor: pointer;
}
.pick.is-on { border-color: var(--accent-line); background: var(--accent-soft); }
.pick__name { font-weight: 500; font-size: 13px; }
.pick__why {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
  text-align: right;
  font-size: 12px;
  color: var(--dim);
}
/* the spool nearest the colour that was asked for. The list has always been
   sorted by this; now it says so. */
.pick__match {
  flex: none;
  padding: 1px 8px 2px;
  border-radius: var(--r-pill);
  background: var(--info-soft);
  color: var(--info);
  font: 400 11px/1.6 var(--mono);
}
.askedcolour {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 9px;
  font-size: 12px;
  color: var(--muted);
}
.pick.is-on .pick__why { color: var(--ok); }

.swatch {
  width: 14px; height: 14px;
  border-radius: 4px;
  flex: none;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14);
}
.swatch--round { border-radius: 50%; }
.swatch--sm { width: 9px; height: 9px; border-radius: 3px; }
.swatch--lg { width: 22px; height: 22px; border-radius: var(--r-xs); }

.pill {
  display: inline-block;
  padding: 4px 9px;
  border-radius: var(--r-pill);
  font: 500 11px/1 var(--mono);
  white-space: nowrap;
  background: oklch(0.29 0.012 265);
  color: var(--muted);
}
.pill--todo    { background: oklch(0.28 0.04 55);  color: var(--accent-text); }
.pill--printer { background: var(--warn-soft);     color: var(--warn); }
.pill--printed,
.pill--ok      { background: var(--ok-soft);       color: var(--ok); }
.pill--bad     { background: var(--bad-soft);       color: var(--bad); }
.pill--info    { background: var(--info-soft);     color: var(--info); }
/* straight off the upload and not worked on yet */
.pill--new     { background: var(--accent-panel);  color: var(--accent-text); }

/* the material a customer asked for, with its colour */
.tag {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 3px 9px;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  font: 400 12px/1.6 var(--mono);
  color: var(--muted);
}

/* --------------------------------------------------------------- previews -- */
/* In production the src is the picture the app draws (640×480 for a model,
   360×360 for a plate, in the job's filament colour). The hatch is the
   placeholder used in the design files. */
.preview {
  aspect-ratio: 4 / 3;
  border-radius: var(--r-lg);
  background: repeating-linear-gradient(135deg, var(--hatch-a) 0 6px, var(--hatch-b) 6px 12px);
  display: grid;
  place-items: center;
  font: 400 10px/1 var(--mono);
  letter-spacing: 0.08em;
  color: var(--hatch-ink);
  overflow: hidden;
}
.preview--filament { --hatch-a: oklch(0.44 0.10 45); --hatch-b: oklch(0.40 0.09 45); --hatch-ink: oklch(0.22 0.03 45); }

.thumb {
  width: 56px;
  height: 44px;
  border-radius: var(--r-sm);
  flex: none;
  display: grid;
  place-items: center;
  background: repeating-linear-gradient(135deg, var(--hatch-a) 0 4px, var(--hatch-b) 4px 8px);
  font: 700 9px/1 var(--mono);
  color: var(--hatch-ink);
  overflow: hidden;
}
.thumb--sq { width: 52px; height: 52px; border-radius: var(--r-md); }
.thumb--sm { width: 42px; height: 42px; }
.thumb--xs { width: 38px; height: 38px; border-radius: var(--r-sm); font-size: 8px; }
.thumb--plate { width: 64px; height: 64px; border-radius: var(--r-sm); }
.thumb--wide { width: 96px; height: 74px; border-radius: var(--r-lg); font-size: 10px; }

.platelist { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; }
.plate {
  display: flex;
  gap: 12px;
  align-items: center;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  padding: 11px;
}
/* a flex child will not shrink below its content unless it is told it may,
   and an object called outdated_still_1mm_servo_pocket is wider than the card */
.plate > div { min-width: 0; }
.plate__name {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 600;
  font-size: 14px;
  min-width: 0;
  overflow-wrap: anywhere;
}
.plate__dims { font: 400 12px/1.6 var(--mono); color: var(--muted); margin-top: 3px; }
.plate__objects { font-size: 12px; color: var(--dim); min-width: 0; }
/* the plate is the thing; the button that slices it belongs on it */
.plate__do { margin-top: 7px; }
.plate { align-items: start; }
.plate > div { display: grid; gap: 0; }

/* the slice card, when a link has just jumped to it */
#slice:target { outline: 2px solid var(--accent-line); outline-offset: 3px; }

/* ---------------------------------------------------------- customer side -- */
/* Different colours, so nobody has to guess which half of the app is on
   screen: light, teal, and a narrower measure because it is a form to read. */
body.guest {
  --bg:            oklch(0.975 0.008 195);
  --panel:         #ffffff;
  --panel-raised:  #ffffff;
  --panel-bar:     #ffffff;
  --field:         oklch(0.985 0.008 200);

  --line:          oklch(0.905 0.015 200);
  --line-mid:      oklch(0.925 0.015 200);
  --line-soft:     oklch(0.945 0.012 200);

  --ink:           oklch(0.270 0.030 215);
  --muted:         oklch(0.520 0.025 210);
  --dim:           oklch(0.550 0.030 210);
  --faint:         oklch(0.680 0.020 210);

  --accent:        oklch(0.500 0.095 205);
  --accent-hover:  oklch(0.440 0.100 205);
  --accent-on:     #ffffff;
  --accent-text:   oklch(0.450 0.100 205);
  --accent-soft:   oklch(0.965 0.030 200);
  --accent-line:   oklch(0.600 0.090 205);
  --accent-panel:  oklch(0.975 0.015 200);

  --ok:            oklch(0.480 0.100 160);
  --ok-soft:       oklch(0.940 0.040 160);

  --hatch-a:       oklch(0.945 0.020 200);
  --hatch-b:       oklch(0.925 0.025 200);
  --hatch-ink:     oklch(0.580 0.050 205);

  --quote-bg:      oklch(0.975 0.012 200);

  --field-empty:   oklch(0.955 0.010 200);
  --line-empty:    oklch(0.885 0.018 200);
  --placeholder:   oklch(0.560 0.020 210);
  --chevron: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'><path d='M1.6 3.6 5 7 8.4 3.6' fill='none' stroke='%23596D71' stroke-width='1.4' stroke-linecap='round' stroke-linejoin='round'/></svg>");

  --shell-max: 1080px;
  --gutter: 28px;
  --topbar-h: 58px;
  --contextbar-h: 44px;

  font-size: 15px;
}
body.guest ::selection { background: oklch(0.88 0.05 200); color: inherit; }
body.guest .topbar { border-bottom-color: var(--line); }
body.guest .main { padding-bottom: 120px; }

/* step indicator in the customer context bar */
.steps { display: flex; align-items: center; gap: 16px; }
.step { display: flex; align-items: center; gap: 8px; }
.step__n {
  width: 18px; height: 18px;
  border-radius: 50%;
  text-align: center;
  background: oklch(0.94 0.012 200);
  color: var(--muted);
  font: 700 11px/18px var(--mono);
}
.step__label { font-size: 13px; font-weight: 500; color: var(--faint); }
.step.is-done .step__n, .step.is-current .step__n { background: var(--accent); color: #fff; }
.step.is-done .step__label, .step.is-current .step__label { color: var(--ink); }

.guest-h1 { font: 600 28px/1.15 var(--font); letter-spacing: -0.03em; margin: 4px 0 6px; }
.guest-lede { margin: 0 0 22px; font-size: 15px; color: var(--muted); max-width: 64ch; text-wrap: pretty; }

/* the file we read out of the upload */
.filestrip {
  display: flex;
  gap: 16px;
  flex-wrap: wrap;
  align-items: center;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-2xl);
  padding: 16px;
}
.filestrip__name { font: 600 17px/1.2 var(--font); }
.filestrip__dims { font: 400 12px/1.7 var(--mono); color: var(--muted); margin-top: 3px; }
.summary {
  font: 400 12px/1.6 var(--mono);
  padding: 6px 12px;
  border-radius: var(--r-pill);
  background: var(--accent-soft);
  color: var(--accent-text);
}

.formcard {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-2xl);
  padding: 18px 20px 20px;
}
.formcard__head { display: flex; align-items: baseline; gap: 10px; margin-bottom: 4px; }
.formcard__step {
  font: 700 11px/1 var(--mono);
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent-text);
}
.formcard__title { font: 600 18px/1.2 var(--font); }
.formcard__lede { margin: 0 0 14px; font-size: 14px; color: var(--muted); max-width: 62ch; }
.formcard__hint { font-size: 13px; color: var(--dim); margin-top: 8px; max-width: 62ch; }

/* one print block: one set of choices */
.printblock { border: 1px solid var(--line-mid); border-radius: 13px; overflow: hidden; }
.printblock__head {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  background: oklch(0.975 0.012 200);
  border-bottom: 1px solid var(--line-mid);
}
.printblock__label { font: 700 11px/1 var(--mono); letter-spacing: 0.1em; text-transform: uppercase; color: var(--dim); }
.printblock__count { margin-left: auto; font-size: 13px; color: var(--muted); }
.printblock__body { padding: 16px 14px; display: grid; gap: 18px; }
.subfield { display: grid; gap: 8px; }
.subfield__label {
  font: 400 12px/1.2 var(--mono);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}
.subfield__head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.subfield__tools { display: flex; align-items: center; gap: 6px; margin-left: auto; }

/* Copies per plate. A file can hold twenty plates, so the chips are small and
   the grid scrolls rather than growing the page. */
.platechips {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(246px, 1fr));
  gap: 8px;
  list-style: none;
  margin: 0;
}
.platechip {
  display: flex;
  align-items: center;
  gap: 8px;
  border: 1px solid var(--line-mid);
  border-radius: var(--r-lg);
  background: var(--panel);
  padding: 4px 5px 4px 10px;
}
.platechip.is-wanted { border-color: var(--accent-line); background: var(--accent-soft); }
.platechip__name { font-size: 13px; font-weight: 600; flex: 1 1 auto; min-width: 0; }
/* the stepper is the point of the chip: it keeps its width, the name gives way */
.platechip .stepper { flex: none; }

.stepper {
  display: flex;
  align-items: center;
  border: 1px solid var(--line-mid);
  border-radius: var(--r-sm);
  background: var(--panel);
  flex: none;
}
.stepper button {
  border: 0;
  background: transparent;
  width: 26px; height: 26px;
  border-radius: var(--r-xs);
  font-size: 14px;
  color: var(--muted);
  cursor: pointer;
}
.stepper button:hover { background: var(--field); color: var(--ink); }
.stepper button:disabled { opacity: 0.4; cursor: default; }
/* the box in the middle wears the stepper's own face rather than its own */
.stepper .input {
  border: 0;
  background: transparent;
  width: 46px;
  padding: 5px 2px;
  text-align: center;
  font: 700 13px/1 var(--mono);
}
.stepper .input:focus { outline: 2px solid var(--accent-line); border-radius: var(--r-xs); }
.stepper output { min-width: 18px; text-align: center; font: 700 13px/1 var(--mono); }
.platechip:not(.is-wanted) .stepper output { color: var(--faint); }

/* one value box whose unit follows the size that was chosen */
.valuebox { display: flex; align-items: center; gap: 8px; }
.valuebox input { min-width: 0; font-family: var(--mono); }
.valuebox__unit { flex: none; font: 400 13px/1 var(--mono); color: var(--muted); }

/* Two controls that are one question: they keep to the room the question needs
   rather than stretching across whatever card they are dropped into. */
.sizechoice { display: flex; flex-wrap: wrap; align-items: start; gap: 12px; }
/* the note under the value box makes this taller than whatever it sits beside,
   which must not drag its neighbour's label down with it */
.fieldrow > .sizechoice { align-self: start; flex: 1 1 400px; }
.sizechoice__mode { flex: 0 1 230px; }
.sizechoice__value { flex: 0 0 160px; }

/* A number with no unit beside it is a number somebody has to guess at. CSS
   cannot compare one attribute with another, but it can compare one with a
   value, which is all this needs. */
.sizechoice[data-mode="factor"] .valuebox__unit::after { content: "×"; }
.sizechoice[data-mode="x"] .valuebox__unit::after,
.sizechoice[data-mode="y"] .valuebox__unit::after,
.sizechoice[data-mode="z"] .valuebox__unit::after,
.sizechoice[data-mode="fit"] .valuebox__unit::after { content: "mm"; }

/* the note explains the box that is off; once it is on, the unit does */

/* a label above a box that cannot be typed into should not read as loudly as
   one above a box that can */
.field:has(.input:disabled) .field__label,
.field:has(.select:disabled) .field__label { opacity: 0.55; }

.divider { display: flex; align-items: center; gap: 14px; padding: 4px 0; }
.divider::before, .divider::after { content: ""; height: 1px; flex: 1 1 auto; background: oklch(0.885 0.02 200); }
.divider__label {
  font: 400 11px/1 var(--mono);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--dim);
}

/* what you asked for, and the send button, always in reach */
.actionbar {
  position: sticky;
  bottom: 0;
  margin: 22px calc(var(--gutter) * -1) -120px;
  padding: 14px var(--gutter) 20px;
  background: linear-gradient(oklch(0.975 0.008 195 / 0), oklch(0.975 0.008 195) 40%);
}
.actionbar__inner {
  max-width: calc(var(--shell-max) - 56px);
  margin: 0 auto;
  display: flex;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--r-xl);
  padding: 13px 16px;
  box-shadow: 0 6px 24px oklch(0.60 0.03 210 / 0.10);
}
.actionbar__text { min-width: 0; flex: 1 1 auto; }
.actionbar__sub { font-size: 13px; color: var(--muted); margin-top: 2px; }

.dropzone {
  border: 1.5px dashed oklch(0.85 0.03 200);
  border-radius: var(--r-lg);
  padding: 34px 24px;
  text-align: center;
  background: var(--field);
}
/* a file being held over the box, so the largest words on the page are telling
   the truth about what happens if you let go */
.dropzone.is-holding { border-color: var(--accent-line); background: var(--accent-soft); }

/* the browser's own file control says "No file chosen" in its own typeface;
   this is the same control wearing the page's clothes */
.filepick {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  cursor: pointer;
  min-width: 0;
}
/* The file box is out of the way, but it is a replaced element and does not
   take `overflow: hidden` — shrinking it to a pixel leaves its own content
   spilling out of it. The wrapper is what gets shrunk. */
.filepick__box {
  position: absolute;
  left: 0;
  top: 0;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}
/* and the box itself, which is what a layout check measures */
.filepick__box input { clip-path: inset(50%); }
.filepick__btn {
  flex: none;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 9px 14px;
  background: var(--panel);
  font: 500 13px/1 var(--font);
  color: var(--ink);
}
.filepick:hover .filepick__btn { border-color: var(--accent-line); color: var(--accent-text); }
.filepick input:focus-visible + .filepick__btn { outline: 2px solid var(--accent-line); outline-offset: 2px; }
.filepick__name {
  font: 400 13px/1.4 var(--font);
  color: var(--faint);
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 26ch;
}
.dropzone.has-file .filepick__name { color: var(--ink); font-weight: 500; }
.dropzone__head { font: 600 17px/1.3 var(--font); }
.dropzone__sub { font-size: 14px; color: var(--muted); margin-top: 6px; }
/* the file box and the button that sends it, side by side and centred */
.dropzone__pick {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  align-items: center;
  justify-content: center;
  margin: 18px 0 0;
}

/* where the request stands */
.timeline { list-style: none; margin: 0; padding: 0; }
.timeline li { display: flex; gap: 14px; align-items: flex-start; }
.timeline__mark { display: grid; justify-items: center; flex: none; width: 20px; }
.timeline__dot {
  width: 13px; height: 13px;
  border-radius: 50%;
  background: oklch(0.90 0.012 200);
}
.timeline__line { width: 2px; height: 34px; background: var(--line-soft); }
.timeline li:last-child .timeline__line { height: 0; background: transparent; }
.timeline__body { padding-bottom: 14px; min-width: 0; }
.timeline__head { font-weight: 600; font-size: 15px; color: var(--faint); }
.timeline__sub { font-size: 13px; color: var(--muted); margin-top: 2px; }
.timeline li.is-done .timeline__dot { background: var(--ok); }
.timeline li.is-done .timeline__head { color: var(--ink); }
.timeline li.is-current .timeline__dot { background: var(--accent); box-shadow: 0 0 0 3px oklch(0.90 0.05 205); }
.timeline li.is-current .timeline__head { color: var(--ink); }


/* --------------------------------------------------------------- utilities -- */
/* Blocks are spaced by gap and padding, never by inherited paragraph margins. */
p, dl, dd, dt, blockquote, figure, ul, ol, fieldset, legend { margin: 0; }
fieldset { border: 0; padding: 0; min-width: 0; }
ul, ol { padding: 0; }
.steps, .reqlist, .machines, .runsheet, .lane__cards, .platelist, .platechips, .modelgrid, .timeline { list-style: none; }

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* the real radio sits under the chip and keeps keyboard behaviour */
.chip, .pick { position: relative; }
.pick input { position: absolute; opacity: 0; width: 0; height: 0; }
.pick:has(input:checked) { border-color: var(--accent-line); background: var(--accent-soft); }
.pick:has(input:checked) .pick__why { color: var(--ok); }
.pick:has(input:focus-visible) { outline: 2px solid var(--accent-line); outline-offset: 2px; }

.cell-sub { display: block; }
.ok-text { color: var(--ok); }

/* card link surfaces */
.modelcard a { display: block; color: inherit; text-decoration: none; }
.modelcard a:hover { text-decoration: none; }
.modelcard__body, .modelcard__name, .modelcard__dims { display: block; }
.modelcard .preview { border-radius: 0; }

.runsheet .req__text { display: grid; min-width: 0; flex: 1 1 auto; }
.req__name, .req__what { display: block; }

.timeline__body, .timeline__head, .timeline__sub { display: block; }

/* five machine tiles across the run-sheet dashboard */
.machinegrid { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; }
.machinetile { display: grid; gap: 6px; align-content: start; min-height: 112px; }
.machinetile__state { font-size: 12px; color: var(--muted); }
@media (max-width: 1100px) { .machinegrid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.stat { font: 600 30px/1 var(--font); letter-spacing: -0.03em; }
.statline { display: flex; align-items: baseline; gap: 8px; }

/* Below a laptop the header cannot hold everything on one line. It stops
   sticking (the sticky offsets are one fixed height each, and a wrapped header
   is not one height), wraps, and the nav takes a line of its own. */
/* Up to here the header cannot hold one line: the nav, the search, the
   signed-in name and the shortcuts together outgrow a laptop's width, so the
   bar wraps well before a phone. */
/* The search mid-bar: as wide as the row can spare, capped so it stays a
   control rather than a lake. It is an <a>, dressed as the field it opens. */
.topbar__search {
  flex: 1 1 auto;
  min-width: 0;
  max-width: 460px;
  margin-inline: auto;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 5px 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--field);
  color: var(--muted);
  font-size: 13px;
  text-decoration: none;
}
.topbar__search:hover { color: var(--ink); border-color: var(--line-mid); text-decoration: none; }
.topbar__search .kbd { margin-left: auto; }

/* The menu behind the signed-in name: a <details>, open until the next click
   anywhere — the invisible fixed backdrop on the open summary catches that
   click and folds it shut. */
.menu { position: relative; }
.menu > summary { list-style: none; cursor: pointer; }
.menu > summary::-webkit-details-marker { display: none; }
.menu > summary::after { content: " ▾"; font-size: 9px; opacity: 0.7; }
.menu[open] > summary::before { content: ""; position: fixed; inset: 0; z-index: 39; }
.menu__list {
  position: absolute;
  right: 0;
  top: calc(100% + 8px);
  z-index: 40;
  min-width: 190px;
  display: grid;
  gap: 2px;
  padding: 6px;
  background: var(--panel-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: 0 12px 32px rgb(0 0 0 / 0.4);
}
.menu__item {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  padding: 7px 10px;
  border-radius: var(--r-sm);
  border: 0;
  background: none;
  color: var(--ink);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  text-decoration: none;
}
.menu__item:hover { background: var(--field); }
.menu form { display: contents; }

@media (max-width: 1080px) {
  .topbar, .contextbar { position: static; }
  .topbar__row {
    height: auto;
    flex-wrap: wrap;
    gap: 10px 18px;
    padding-top: 10px;
    padding-bottom: 10px;
  }
  .nav { order: 3; width: 100%; overflow-x: auto; padding-bottom: 2px; }
  .topbar__search { order: 4; flex-basis: 100%; max-width: none; }
  .topbar__tools { flex-wrap: wrap; gap: 8px; }
  .topbar__sep { display: none; }
  .contextbar { height: auto; padding-top: 9px; padding-bottom: 9px; }
  .contextbar__inner { flex-wrap: wrap; gap: 8px 16px; }
  .contextbar__tools { margin-left: 0; flex-wrap: wrap; }
  /* the filters take a second line here rather than a scrollbar, and so does
     a segmented control with more buttons than the window is wide */
  .filters { flex-wrap: wrap; }
  .seg { flex-wrap: wrap; max-width: 100%; }
  .main > .contextbar:first-child { margin-bottom: 18px; }
}

/* On a phone the two links that are shortcuts rather than work are dropped,
   and the tools in the context bar take a line each rather than a scrollbar. */
@media (max-width: 560px) {
  .linkout--aside { display: none; }
  .contextbar__tools { width: 100%; }
  .contextbar__tools > form { flex: 1 1 100%; display: flex; gap: 8px; min-width: 0; }
  .search, .file { width: 100%; min-width: 0; flex: 1 1 auto; max-width: none; }
  .contextbar__tools .btn { flex: none; }
}

/* ------------------------------------------------------------------ print -- */
@media print {
  .topbar, .actionbar, .contextbar__tools { display: none; }
  body { background: #fff; color: #000; }
}

/* two things the app has that the design files did not draw: a card whose slice
   has not finished, and the button that puts a printed one away */
.job--pending { opacity: 0.6; cursor: default; border-style: dashed; }
.job .archive { display: none; margin-left: auto; align-self: center; }
.lane--printed .job .archive { display: block; }

/* one lane forward by pressing, only where a drag is not on offer: a screen
   without a hovering pointer, or a board folded to one column. Hiding it from
   the a11y tree at desktop widths is deliberate — there the job page's state
   buttons are the keyboard path, and the board stays a drag surface */
.job__advance { display: none; margin-left: auto; align-self: center; }
@media (hover: none), (max-width: 720px) {
  .job__advance { display: block; }
  /* a card dragged into the printed lane before any reload: its button still
     says the move it has just made, so the lane puts it away */
  .lane--printed .job .job__advance { display: none; }
}

/* things the app has that the design files did not draw ---------------------- */

/* a form that could not go through, said above the form */
.banner--warn {
  background: var(--warn-soft);
  border-color: var(--warn);
  color: var(--warn);
  font-size: 13px;
}
.warn-text { color: var(--warn); }

/* a checkbox that reads as one line rather than a labelled field */
.check {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
  color: var(--muted);
  padding-bottom: 9px;
  cursor: pointer;
}
.check input { accent-color: var(--accent); }

/* the angle and the plate rule mean nothing without supports to apply them to */
.supports[data-style="profile"] .only-supported,
.supports[data-style="off"] .only-supported { display: none; }

/* a named size already says how big, so the millimetre control steps aside */
.named-sizes:not([data-named=""]) .sizechoice { display: none; }

/* What a model is filed under. The mark is what says "these are tags"; the
   outlines are what say where one ends and the next begins. */
.tags { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 5px; min-width: 0; }
.tags__mark { flex: none; color: var(--faint); }
.tag-chip {
  display: inline-flex;
  align-items: center;
  padding: 1px 8px 2px;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  background: var(--field);
  font: 400 11px/1.6 var(--mono);
  color: var(--muted);
  white-space: nowrap;
  max-width: 18ch;
  overflow: hidden;
  text-overflow: ellipsis;
}
a.tag-chip:hover {
  border-color: var(--accent-line);
  color: var(--accent-text);
  text-decoration: none;
}

/* On the public form, said as a green ring around the picture rather than as
   another word in a row that has plenty. `outline` rather than a border: it is
   drawn outside the box, so nothing moves when a model is offered. */
.offered { display: inline-flex; line-height: 0; }
.thumb--offered, .preview--offered { outline: 2px solid var(--ok); outline-offset: 1px; }
.modelcard .preview--offered { outline-offset: -2px; }

/* What a plate is painted in, when that is more than one thing. The colours
   are the project author's, and the workshop answers them with its own spools
   when it slices. */
.wants { display: inline-flex; flex-wrap: wrap; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
.wants__slots { display: inline-flex; flex-wrap: wrap; gap: 6px; }
.wants__slot {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 1px 8px 2px;
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
  font: 400 11px/1.6 var(--mono);
  color: var(--muted);
  background: var(--field);
}
.plate__wants { margin-top: 5px; }

/* a row's tags sit under its name, on a line of their own */
.cell-tags, .modelcard__tags { display: flex; margin-top: 4px; min-width: 0; }
.modelcard__tags { padding: 0 13px 13px; }
.offer__tags { margin-top: 4px; }

/* the × that takes a named size away, inside its chip */
.chip-drop {
  border: 0;
  background: none;
  color: var(--dim);
  cursor: pointer;
  font-size: 15px;
  line-height: 1;
  padding: 0 0 0 2px;
}
.chip-drop:hover { color: var(--bad); }
.chip form { display: inline-flex; }

/* a plate someone wants copies of, told apart from the zeroes by its count */
.platechip[data-count]:not([data-count="0"]):not([data-count=""]) {
  border-color: var(--accent-line);
  background: var(--accent-soft);
}
.platechip[data-count="0"] .stepper .input { color: var(--faint); }
.platechip__objects {
  display: block;
  font: 400 11px/1.4 var(--mono);
  color: var(--dim);
  /* "15 objects · sliced ×2" is worth a second line; it is not worth an
     ellipsis that eats the half saying the plate is already done */
  white-space: normal;
}
.platechip__name { min-width: 0; overflow-wrap: anywhere; }
.platechip .plate-picture { flex: none; }

/* a print block that has not been asked for yet is not in the form at all */
.printblock[data-shown="0"], .add-row[data-shown="1"] { display: none; }

/* no colour asked for: the swatch says so rather than showing a colour */
.swatch.none {
  background: linear-gradient(135deg, var(--panel) 46%, var(--line) 46%,
                              var(--line) 54%, var(--panel) 54%);
  border: 1px solid var(--line);
}

/* A table with eight columns in a card 900px wide has to go somewhere: it
   scrolls inside its own box rather than pushing the page sideways. */
.scroller {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  /* the scrolling alone is not enough: a table wider than this box still counts
     towards the page's own scrollable width, and the whole page would slide */
  contain: paint;
}
/* Below this the fixed columns cannot hold what is in them, so the table keeps
   its width and the box around it scrolls. The widest is a job table: eight
   columns, three of them a fixed width. */
.scroller > .data { min-width: 820px; }
.scroller > .data--slim { min-width: 560px; }

/* "nothing here yet", said quietly inside the card that would have held it */
.empty {
  margin: 0;
  padding: 20px 2px;
  text-align: center;
  font-size: 13px;
  color: var(--faint);
}

/* what the slicer said when it gave up */
.log {
  margin: 12px 0 0;
  padding: 12px 14px;
  max-height: 340px;
  overflow: auto;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--field);
  font: 400 12px/1.6 var(--mono);
  color: var(--muted);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* What the workshop offers, as a list rather than a wall of pictures: a
   customer is looking for one thing and reads down the names. */
.offered { list-style: none; margin: 14px 0 0; padding: 0; display: grid; gap: 8px; }
.offer {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 12px;
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
  background: var(--panel);
}
.offer:hover { border-color: var(--accent-line); }
/* on a phone the button takes the line under the name rather than the room
   the name needed */
@media (max-width: 560px) {
  .offer { flex-wrap: wrap; }
  .offer__text { flex: 1 1 100%; }
  .offer .btn { margin-left: auto; }
}
.offer__text { min-width: 0; flex: 1 1 auto; }
.offer__name { font: 600 15px/1.25 var(--font); }
.offer__dims { font: 400 11px/1.7 var(--mono); color: var(--muted); }
.offer__tags { font-size: 12px; color: var(--dim); margin-top: 2px; }
.offer .btn { flex: none; }

/* a quote in a customer's own words, and one that carries bad news */
.quote--accent { background: var(--accent-panel); border-left-color: var(--accent-line); }
.quote--bad { border-left-color: var(--bad-line); }

/* the lines of a request, on the page the customer keeps */
.asklist { list-style: none; margin: 0; padding: 0; }
.asklist .req {
  padding: 9px 0;
  border-top: 1px solid var(--line-soft);
  border-bottom: 0;
}

/* a colour well, not a text box: the swatch is the whole control. Its height
   is the other fields', so a row of labels sits on one line. */
.input[type="color"] { padding: 3px; height: 38px; cursor: pointer; }

/* A machine has more than one thing worth changing, and a table cannot hold a
   form across its cells. One row, one form, one save. */
.setuplist { display: grid; }
.setuprow {
  display: grid;
  gap: 6px 14px;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: end;
  padding: 13px 16px;
  border-bottom: 1px solid var(--line-soft);
}
.setuprow:last-child { border-bottom: 0; }
.setuprow__edit {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: 10px;
  min-width: 0;
}
.setuprow__name { flex: 1 1 190px; }
.setuprow__name .input { font-weight: 600; }
.setuprow__plate { flex: 0 1 190px; }
.setuprow__process { flex: 1 1 260px; }
.setuprow__process .select { font-size: 12px; }
.setuprow__edit .btn { flex: none; }
.setuprow__drop { grid-column: 2; grid-row: 1; align-self: end; }
.setuprow__facts { grid-column: 1 / -1; font-size: 11px; }

/* the users tab: a row's controls sit under the name — the role toggle, the
   password reset with its one box, and Remove off to the right like a peer */
.userrow__acts { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; min-width: 0; }
.userrow__reset { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; min-width: 0; }
.userrow__reset .input { width: 170px; max-width: 100%; }

/* the row that adds one is not another row of the list: it is a different
   thing, and it says so before it starts asking */
.setupadd { border-top: 1px solid var(--line-mid); display: grid; gap: 12px; }
@media (max-width: 700px) {
  .setuprow { grid-template-columns: minmax(0, 1fr); }
  .setuprow__drop { grid-column: 1; grid-row: auto; }
}

/* an address worth handing to somebody else: shown in full, and copied by the
   button beside it rather than by selecting it and hoping */
.share { display: flex; align-items: center; gap: 8px; min-width: 0; }
.share__url {
  flex: 1 1 auto;
  min-width: 0;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  white-space: nowrap;
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  background: var(--field);
  padding: 7px 9px;
  font: 400 11px/1.4 var(--mono);
  color: var(--muted);
}
.share .btn { flex: none; }

/* uploading is one line in the context bar, not a card of its own */
.uploader { display: flex; align-items: center; gap: 8px; min-width: 0; }
.uploader .filepick__name { max-width: 16ch; font-size: 12px; }
.uploader .filepick__btn { padding: 6px 11px; font-size: 12px; }
.file {
  font: 400 12px/1 var(--font);
  color: var(--muted);
  max-width: 210px;
}
.file::file-selector-button {
  font: 500 12px/1 var(--font);
  color: var(--ink);
  background: var(--panel-raised);
  border: 1px solid var(--line);
  border-radius: var(--r-md);
  padding: 7px 11px;
  margin-right: 9px;
  cursor: pointer;
}
.file::file-selector-button:hover { border-color: var(--accent-line); }

/* a picture standing in for a thumb fills it rather than stretching it */
img.thumb { object-fit: cover; }

/* ----------------------------------------------------------------- viewer -- */
/* A picture of a model is also the way into it: pressing the ⟳ replaces the
   still with the thing itself, drawn in WebGL (assets/viewer.js). Nothing here
   is needed for the page to work — without WebGL the picture simply stays. */
.viewer { position: relative; }
.viewer canvas {
  width: 100%;
  display: block;
  aspect-ratio: 4 / 3;
  border-radius: var(--r-lg);
  background: var(--field);
  touch-action: none;   /* a drag turns the model rather than scrolling the page */
  outline-offset: 3px;
}
.viewer .spin,
.viewer .expand,
.viewer .recentre {
  position: absolute;
  right: 8px;
  border: 1px solid var(--line);
  background: var(--panel);
  color: var(--muted);
  cursor: pointer;
}
/* neither of these is worth offering until there is something to point them at */
.viewer .expand { display: none; top: 8px; right: 46px; }
.viewer .recentre { display: none; top: 8px; right: 84px; }
.viewer[data-spinning] .expand,
.viewer[data-spinning] .recentre {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: var(--r-pill);
}
.viewer .expand:hover,
.viewer .recentre:hover { border-color: var(--accent-line); color: var(--accent-text); }

/* what the squares under the model are worth, and how big it is */
.viewer-scale {
  margin: 8px 0 0;
  font: 400 11px/1.5 var(--mono);
  color: var(--dim);
}

/* The whole window, so a print can be looked at rather than glanced at. The
   browser's own fullscreen is the first choice; the panel below is what is
   left when it is refused. */
.viewer:fullscreen {
  padding: 18px;
  background: var(--panel);
  display: flex;
  flex-direction: column;
}
.viewer:fullscreen canvas {
  flex: 1 1 auto;
  height: auto;
  min-height: 0;
  border-radius: var(--r-lg);
}
.viewer:fullscreen .spin { right: 24px; }
.viewer:fullscreen .expand { right: 62px; }
.viewer:fullscreen .recentre { right: 100px; }
.viewer:fullscreen .spin,
.viewer:fullscreen .expand,
.viewer:fullscreen .recentre { top: 24px; }

/* the panel is fixed, and the sticky column it lives in is a stacking context
   of its own: while the panel is up, that context has to go, or everything
   positioned later on the page paints over it */
html:has(.viewer[data-big]) .sticky { position: static; }

.viewer[data-big] {
  position: fixed;
  inset: 14px;
  z-index: 40;
  margin: 0;
  padding: 14px;
  border-radius: var(--r-2xl);
  border: 1px solid var(--line);
  background: var(--panel);
  box-shadow: 0 30px 90px rgba(0, 0, 0, 0.45);
  display: flex;
  flex-direction: column;
}
.viewer[data-big] canvas { flex: 1 1 auto; height: auto; min-height: 0; border-radius: var(--r-lg); }
.viewer[data-big] .spin { right: 22px; }
.viewer[data-big] .expand { right: 60px; }
.viewer[data-big] .recentre { right: 98px; }
.viewer[data-big] .spin,
.viewer[data-big] .expand,
.viewer[data-big] .recentre { top: 22px; }

.viewer .spin {
  top: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: var(--r-pill);
}
.viewer .spin:hover { border-color: var(--accent-line); color: var(--accent-text); }
.viewer[data-spinning] .spin { border-color: var(--accent-line); color: var(--accent-text); }
.viewer[data-state="loading"] .spin { cursor: progress; opacity: 0.6; }
/* nothing to press if the browser cannot draw it */
[data-no-webgl] .spin,
[data-no-webgl] .expand,
[data-no-webgl] .recentre,
[data-no-webgl] .spin-plate,
[data-no-webgl] .plate-picture .turn,
[data-no-webgl] .viewer-note { display: none; }
/* without WebGL a plate's picture is a picture, not a way into the viewer */
[data-no-webgl] .plate-picture { cursor: default; }

.viewer-note {
  margin: 8px 0 0;
  font: 400 12px/1.4 var(--font);
  color: var(--muted);
}
.viewer-note:empty { display: none; }

/* how far up the print to draw, and what the colours in it mean */
.viewer .layers { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
.viewer .layers input { flex: 1; padding: 0; accent-color: var(--accent); }
.viewer .layers span { font: 400 11px/1 var(--mono); color: var(--muted); white-space: nowrap; }
.viewer .legend {
  display: flex;
  flex-wrap: wrap;
  gap: 3px 12px;
  margin: 8px 0 0;
  font: 400 11px/1.6 var(--font);
  color: var(--muted);
}
.viewer .legend span { display: inline-flex; align-items: center; gap: 5px; }
.viewer .legend i { width: 9px; height: 9px; border-radius: 2px; flex: none; }

/* A button that is a switch shows which way it will go. The viewer marks
   whichever button it is drawing for; these two labels take turns on that. */
.switch__on { display: none; }
.switch[data-showing] .switch__off { display: none; }
.switch[data-showing] .switch__on { display: inline; }
.switch[data-showing] { border-color: var(--accent-line); color: var(--accent-text); }

/* a plate's picture is the way into that plate: pressing it turns it above */
.plate-picture {
  position: relative;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  line-height: 0;
  border-radius: var(--r-sm);
  flex: none;
}
.plate-picture .turn {
  position: absolute;
  right: 3px;
  bottom: 3px;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 20px;
  border-radius: var(--r-pill);
  background: var(--panel);
  border: 1px solid var(--line);
  color: var(--muted);
  opacity: 0;
  transition: opacity 0.12s;
}
.plate-picture:hover .turn,
.plate-picture:focus-visible .turn { opacity: 1; }
.plate-picture:hover img,
.plate-picture[data-showing] img { outline: 2px solid var(--accent-line); outline-offset: 1px; }
.plate-picture[data-showing] .turn {
  opacity: 1;
  color: var(--accent-text);
  border-color: var(--accent-line);
}
