/* Claypath app — "Draft Table".
   A drawing sheet: ruled columns, mono figures, the uploaded file in the middle.
   Standalone from landing.css, but the five terracotta/paper/ink values below are
   the same tokens that file declares — keep them in step if the palette moves. */

:root {
  --terra: #C67B5C;
  --terra-dark: #A8624A;
  --terra-light: #E8C4B4;
  --paper: #F2EBE4;
  --ink: #241F1A;

  /* Sheet extensions */
  --sheet: #F5EFE7;
  --panel: #EFE8DF;
  --panel-dark: #EDE5DB;
  --field: #FCF9F4;
  --ink-2: #3C352E;
  --ink-3: #564E44;
  --muted: #8A7F70;
  --faint: #B5A894;
  --rule: #D3C7B7;
  --rule-soft: #E4DBCD;
  --rule-hair: #C6B9A8;
  --grid-fine: #E1D7C9;
  --grid-coarse: #D8CCBB;

  --ok: #5E8452;
  --warn: #C67B5C;
  --bad: #C4574E;
  /* The fourth banner hue, and it had to be new: --warn is literally --terra,
     so "a change is coming" and "here is a new feature" would have been the
     same colour. Same chroma and lightness family as the three above, hue
     turned toward ochre. */
  --notice: #C08A3E;
  /* The same hue as *text*, which is a different colour — `--notice` is a fill
     and reads 2.56:1 on paper, well under the line. Same relationship
     `--terra-dark` has to `--terra`, and the same value the banner already
     uses for its light-theme ochre fill. It goes *lighter* in dark, where the
     fill shade would be the unreadable one. */
  --notice-ink: #7E5A1C;

  /* Semantic pairs. Every place style.css used to write a light value in by
     hand — the tooltip chip, the error wash, the white checkbox — is one of
     these, because "invert the token" is the wrong answer for all of them. */
  --sel-bg: var(--ink);        /* a chosen chip: mold type, STEP/STL, VIEW */
  --sel-fg: var(--paper);
  --sel-fg-dim: rgba(242, 235, 228, 0.6);
  --box-bg: #fff;
  --cell-shadow: rgba(36, 31, 26, 0.05);
  --prog-track: #DDD2C2;
  --drag-bg: rgba(198, 123, 92, 0.08);
  --tip-bg: var(--ink);
  --tip-fg: rgba(242, 235, 228, 0.88);
  --tip-rule: transparent;
  --tip-shadow: rgba(36, 31, 26, 0.6);
  --err-bg: rgba(196, 87, 78, 0.08);
  --err-fg: #8E332C;
  /* The one destructive control there is — deleting the account — and its
     text link. Not --bad: paper on --bad is 3.69:1, which is too thin for a
     button label, so the button takes a deeper red and the fill that charges
     across it deeper again. Measured: 4.88:1 and 8.79:1 for the label, 4.93:1
     for the link on --panel. */
  --danger-text: #A8423A;
  --danger: #B84A41;
  --danger-on: #FCF9F4;
  --danger-fill: #7F2B25;
  --danger-fill-on: #FCF9F4;
  /* The banner is a filled block with the type inverted onto it, so each
     kind needs a fill that holds 4.5:1 against cream at 12.5px. The accents
     above do not: paper on --terra is 2.65:1 and on --bad 3.69:1. These are
     the deep shade of each of the same four hues, measured against --paper.
     Inverted, the theme uses the light accents with dark type instead —
     which is exactly what --sel-bg/--sel-fg already do on this sheet. */
  --banner-fg: #F5EFE7;
  --banner-fg-dim: rgba(245, 239, 231, 0.66);
  --banner-new: #8C4A2F;
  --banner-notice: #7E5A1C;
  --banner-outage: #8E332C;
  --banner-resolved: #3F5C35;
  /* A plan that takes effect at the renewal: a terracotta wash of the
     card surface. It cannot borrow --field, which is what "the plan you
     are on" already means, and a third state needs a third surface. */
  --pending-bg: #EEE0D6;
  /* A choice made inside another choice — the container under One-piece. It
     is outlined in --sel-bg (what "chosen" looks like in each theme) over a
     faint wash rather than filled with it, so it reads as a step below the
     mold type's solid fill. Light keeps the sheet's ink-for-chosen; dark
     takes the terracotta its --sel-bg already is. */
  --sub-sel-bg: #F6EFE6;
  --sub-sel-icon: var(--ink);
  --view-hover: #fff;
  --overlay: rgba(36, 31, 26, 0.5);
  /* viewer.js reads these off the root; the light values are the ones it
     used to carry as literals, so the 3D pane renders identically. */
  --viewer-bg: #F5EFE7;
  --viewer-grid: #C9BDAB;
  --viewer-grid-sub: #DFD5C6;
  --viewer-part: #C67B5C;

  color-scheme: light;

  --sans: 'Instrument Sans', -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Arial, sans-serif;
  --serif: 'Instrument Serif', Georgia, 'Times New Roman', serif;
  --mono: 'IBM Plex Mono', ui-monospace, SFMono-Regular, Menlo, monospace;

  --bar: 54px;
  --head: 38px;
  /* Both disclosure lists cap here. The two panels are bottom-anchored to
     their columns' floors, so equal caps are what puts their head rows on the
     same line when both are open — the same reasoning as the 3px band each
     ends on. One token so they can't drift apart. */
  --disclosure-list: 132px;
}

/* ── Dark sheet · "Fired Clay" ────────────────────────────────────────
   Four surface steps — chrome, columns, stage, cells — in a warmer clay
   body than the landing page's night, and terracotta rather than cream is
   what "chosen" looks like. `color-scheme` is load-bearing: the parameter
   cells keep the native number spinner, which renders light without it.
   Set on the root by the inline script in index.html, before first paint. */
:root[data-theme="dark"] {
  --terra: #D9885F;
  --terra-dark: #E5A17C;   /* accent *text*: #A8624A is unreadable here */
  --terra-light: #E8C4B4;
  --paper: #14100C;
  --ink: #F5EEE6;

  --sheet: #241D15;
  --panel: #1C1712;
  --panel-dark: #0F0C09;
  --field: #2E2619;
  --ink-2: #E0D6C8;
  --ink-3: #C4B7A5;
  --muted: #A0927F;
  --faint: #8D8172;
  --rule: #382E24;
  --rule-soft: #2A231B;
  --rule-hair: #4A3B2C;
  --grid-fine: #3C3125;
  --grid-coarse: #413628;

  --ok: #8CAE78;
  --warn: #D9885F;
  --bad: #DE8474;
  --notice: #DDA55F;
  --notice-ink: var(--notice);

  --sel-bg: var(--terra);
  --sel-fg: #1B1512;
  --sel-fg-dim: rgba(27, 21, 18, 0.6);
  --box-bg: #3A2F20;
  --cell-shadow: rgba(245, 238, 230, 0.06);
  --prog-track: #3E3226;
  --drag-bg: rgba(217, 136, 95, 0.14);
  /* The tooltip lifts off the column instead of inverting: --ink is cream
     here, so the old `background: var(--ink)` would print cream on cream. */
  --tip-bg: #372C1E;
  --tip-fg: #E0D6C8;
  --tip-rule: #4A3B2C;
  --tip-shadow: rgba(0, 0, 0, 0.55);
  --err-bg: #2C1E18;
  --err-fg: #F2AA9C;
  /* Dark inverts the pair the way --sel-bg does: the button is the light
     accent with dark type (6.59:1), and the fill charges it *down* to a deep
     red with paper type (6.21:1). */
  --danger-text: #DE8474;
  --danger: #DE8474;
  --danger-on: #1B1512;
  --danger-fill: #9E3F36;
  --danger-fill-on: #FCF9F4;
  /* Light accent, dark type — the same inversion --sel-bg makes here, so the
     band reads as belonging to this theme rather than as the light one's
     colours carried over. */
  --banner-fg: #1B1512;
  --banner-fg-dim: rgba(27, 21, 18, 0.62);
  --banner-new: var(--terra);
  --banner-notice: var(--notice);
  --banner-outage: var(--bad);
  --banner-resolved: var(--ok);
  --pending-bg: #3F2A1F;
  --sub-sel-bg: #3F2A1F;
  --sub-sel-icon: var(--terra-dark);
  --view-hover: #3A2F20;
  --overlay: rgba(0, 0, 0, 0.62);
  --viewer-bg: #241D15;
  --viewer-grid: #4A3B2C;
  --viewer-grid-sub: #382E24;
  --viewer-part: #D9885F;

  color-scheme: dark;
}

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

body {
  height: 100vh;
  overflow: hidden;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 14px;
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--terra-dark); text-decoration: none; }
a:hover { color: var(--terra); }

button, input { font-family: inherit; font-size: inherit; color: inherit; }

/* `.key` — the sheet's smallest voice — and `.sr-only` are defined in
   account.css, which loads before this file. */
.key-strong { color: var(--ink); }

.mono { font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* ── Rule bar ─────────────────────────────────────────────────────── */

.rulebar {
  height: var(--bar);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 20px;
  border-bottom: 1px solid var(--ink);
  background: var(--panel-dark);
}

.rulebar-left { display: flex; align-items: baseline; gap: 16px; }
.rulebar-right { display: flex; align-items: center; gap: 18px; }
/* The wordmark is aligned to the label's baseline, so a label allowed to wrap
   aligns it to the *first* line and hangs it off the top of a bar that has
   grown to three. Nothing in the bar wraps; what won't fit comes out. */
.rulebar .key { white-space: nowrap; }

.rulebar h1 { font-family: var(--serif); font-size: 21px; font-weight: 400; line-height: 1; }
.rulebar h1 a { color: var(--ink); white-space: nowrap; }
.rulebar h1 a:hover { color: var(--terra-dark); }

/* The mark is the letter C, not an icon beside the word, so it is sized in em
   off the wordmark's own type and its viewBox is cropped to the foot line the
   pot stands on (y=88) — that puts the foot on the type's baseline for free,
   which a box cropped to the glyph's full depth would not. It keeps terracotta
   through the link's hover; only the word beside it takes the hover colour. */
.mark {
  width: 0.676em;   /* 147.5:192, the viewBox's own ratio */
  height: 0.88em;
  margin-right: 0.09em;
  color: var(--terra);
  /* The crop puts the jar's base exactly on the baseline, which reads a
     hair high next to type — a round glyph overshoots. -0.03em is that
     overshoot, measured against 0 / -0.015 / -0.045 / -0.06. */
  vertical-align: -0.03em;
  overflow: visible;
}
.rulebar h1 a:hover .mark { color: var(--terra-dark); }

/* Appearance switch. Same control vocabulary as a STEP/STL toggle — hairline
   cell, filled when chosen — so it reads as one of the sheet's own controls
   rather than as chrome bolted on. The icons are pyrometric cones: standing
   for a cold kiln, slumped for one that has reached temperature. */
.theme-toggle {
  align-self: center;   /* the group is baseline-aligned; a 26px box is not */
  display: flex;
  align-items: stretch;
  height: 26px;
  border: 1px solid var(--rule-hair);
  background: var(--field);
}

.theme-cell {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}
.theme-cell + .theme-cell { border-left: 1px solid var(--rule-hair); }
.theme-cell:hover { color: var(--ink); }
.theme-cell[aria-checked="true"] { background: var(--sel-bg); color: var(--sel-fg); }
.theme-cell:focus-visible { outline: 2px solid var(--terra); outline-offset: 2px; }

/* ── Notification banner ──────────────────────────────────────────── */

/* The one filled block in the rule bar that is not a chosen control. It takes
   the middle of the bar between the two groups, and it is the flexible one —
   the wordmark, the switch, the readout and the account all keep their size
   and this truncates. */
.banner-slot {
  flex: 1;
  min-width: 0;
  display: flex;
  justify-content: center;
  padding: 0 24px;
}
/* `display: flex` above beats the UA's `[hidden]` rule — the same trap `.btn`
   and `.acct` carry. Without this, a page with no notice reserves the middle
   of the bar for nothing. */
.banner-slot[hidden] { display: none; }

.banner {
  display: flex;
  align-items: center;
  gap: 11px;
  max-width: 100%;
  height: 32px;
  padding: 0 6px 0 13px;
  background: var(--banner-new);
  color: var(--banner-fg);
}
/* The severity carried as an attribute rather than a class: it comes from the
   server as one of four words, and an attribute selector says "this is that
   word" where four classes would need app.js to remember to remove the other
   three. */
.banner[data-kind="notice"] { background: var(--banner-notice); }
.banner[data-kind="outage"] { background: var(--banner-outage); }
.banner[data-kind="resolved"] { background: var(--banner-resolved); }

.banner-icon { width: 15px; height: 15px; flex-shrink: 0; }

/* The kind word rides the inverted colour, not the key tier's --muted. */
.banner-kind { color: var(--banner-fg); }

.banner-text {
  min-width: 0;
  font-size: 12.5px;
  line-height: 1;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.banner-link {
  flex-shrink: 0;
  color: var(--banner-fg);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
}
.banner-link:hover { color: var(--banner-fg); opacity: 0.78; }
.banner-link[hidden] { display: none; }

.banner-close {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--banner-fg-dim);
  cursor: pointer;
  transition: color 0.15s;
}
.banner-close svg { width: 11px; height: 11px; }
.banner-close:hover { color: var(--banner-fg); }
.banner-close:focus-visible { outline: 2px solid var(--banner-fg); outline-offset: -2px; }
/* Same trap again: flex above beats [hidden]. A notice with dismissible=false
   has no close button at all rather than a disabled one. */
.banner-close[hidden] { display: none; }

/* ── Account and billing ──────────────────────────────────────────── */

/* The chip, its menu and the four billing dialogs are in account.css. They
   appear on every page now, and style.css is the one page stylesheet the
   others cannot load. The rule bar's own placement of the chip stays here. */


/* ── The sheet ────────────────────────────────────────────────────── */

.sheet {
  height: calc(100vh - var(--bar));
  display: grid;
  grid-template-columns: 372px minmax(0, 1fr) 336px;
}

.col { display: flex; flex-direction: column; min-height: 0; min-width: 0; }
.col-params, .col-stage { border-right: 1px solid var(--rule); }
.col-stage { background: var(--sheet); }

.col-head {
  height: var(--head);
  position: relative;   /* containing block for the reset control's tooltip */
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 0 18px;
  border-bottom: 1px solid var(--rule);
  background: var(--panel);
}
.col-head .key { white-space: nowrap; }
/* The head is one row of fixed height, so a hint too long for the column has
   to be trimmed rather than pushed out: unshrinkable, it widened the whole
   document on a phone instead of just itself. The tabs are the fixed half of
   the pair, so the trim always comes off the hint. */
.col-head .tabs { flex-shrink: 0; }
.col-head .key { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.col-head-inner { border-bottom: 1px solid var(--rule-soft); height: 32px; }

.col-body { flex-grow: 1; overflow-y: auto; padding: 14px 18px; min-height: 0; }

.col-foot {
  flex-shrink: 0;
  padding: 14px 18px;
  border-top: 1px solid var(--rule);
  background: var(--panel);
}
.col-foot-list { padding: 0; }

/* ── Column 1 · parameters ───────────────────────────────────────── */

.mold-type-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }

.mold-type-card { cursor: pointer; }
.mold-type-card input { position: absolute; opacity: 0; pointer-events: none; }

.mold-type-body {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 44px;
  padding: 0 12px;
  border: 1px solid var(--rule-hair);
  background: var(--field);
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.mold-type-card:hover .mold-type-body { border-color: var(--ink); }
/* A plan view of what comes back, in place of the old 01/02 numerals: the
   difference between the two molds is a shape, and the numerals only ever
   said which one was listed first. */
.mold-type-icon { width: 26px; height: 26px; color: var(--muted); }
.mold-type-name { font-size: 13px; color: var(--ink-3); }

.mold-type-card input:checked + .mold-type-body {
  border-color: var(--sel-bg);
  background: var(--sel-bg);
}
.mold-type-card input:checked + .mold-type-body .mold-type-name { color: var(--sel-fg); font-weight: 500; }
.mold-type-card input:checked + .mold-type-body .mold-type-icon { color: var(--sel-fg); }
.mold-type-card:hover .mold-type-icon { color: var(--ink-3); }
.mold-type-card input:focus-visible + .mold-type-body { outline: 2px solid var(--terra); outline-offset: 2px; }

/* The one-piece container, a choice under the mold type. The line (in --sel-bg) runs from
   the One-piece card's icon down to whichever card is chosen — straight down
   to Fitted, down-across-down to Standard — and eases between the two. Its
   x positions are tied to the card geometry: the One-piece icon's centre is
   26px into its card (1px border + 12px padding + half of 26), and each
   container card's icon is padded to land at the same 26px (1 + 7 + 18). */
.container-mode[hidden] { display: none; }

.container-link { position: relative; height: 14px; }
/* `:where()` so the per-segment rules below can override this one: as a
   plain `.container-link span` it outranks them and zeroes every border. */
:where(.container-link) span {
  position: absolute;
  top: 0;
  left: 25px;
  border: 0 solid var(--sel-bg);
  transition: width 0.18s ease, height 0.18s ease, opacity 0.18s ease;
}
.container-link-down { width: 0; height: 14px; border-left-width: 1px; }
.container-link-across { top: 7px; width: 0; height: 0; border-top-width: 1px; opacity: 0; }
.container-link-land {
  top: 7px;
  left: calc(50% + 29px);
  width: 0;
  height: 7px;
  border-left-width: 1px;
  opacity: 0;
}
.container-link[data-mode="standard"] .container-link-down { height: 7px; }
.container-link[data-mode="standard"] .container-link-across { width: calc(50% + 4px); opacity: 1; }
.container-link[data-mode="standard"] .container-link-land { opacity: 1; }

.container-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.container-card { cursor: pointer; }
.container-card input { position: absolute; opacity: 0; pointer-events: none; }

.container-card-body {
  display: flex;
  align-items: center;
  gap: 10px;
  height: 60px;
  padding: 0 10px 0 7px;
  border: 1px solid var(--rule-hair);
  background: var(--field);
  transition: background 0.15s, border-color 0.15s, box-shadow 0.15s;
}
.container-card:hover .container-card-body { border-color: var(--ink); }
.container-icon { flex: none; width: 36px; height: 36px; color: var(--faint); stroke-width: 1.2; }
.container-icon-pot { opacity: 0.55; }
.container-card-text { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.container-card-name { font-size: 13px; color: var(--ink-3); }
.container-card-caption { font-family: var(--mono); font-size: 10px; line-height: 1.5; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

.container-card input:checked + .container-card-body {
  border-color: var(--sel-bg);
  box-shadow: inset 0 0 0 1px var(--sel-bg);
  background: var(--sub-sel-bg);
}
.container-card input:checked + .container-card-body .container-icon { color: var(--sub-sel-icon); }
.container-card input:checked + .container-card-body .container-card-name { color: var(--ink); font-weight: 500; }
.container-card input:focus-visible + .container-card-body { outline: 2px solid var(--terra); outline-offset: 2px; }

.mold-type-note {
  margin: 9px 0 16px;
  font-family: var(--mono);
  font-size: 10.5px;
  line-height: 1.65;
  color: var(--muted);
}

fieldset { border: 0; margin-bottom: 6px; }
legend { display: block; width: 100%; padding: 0; margin-bottom: 2px; color: var(--terra-dark); }
fieldset legend.key { color: var(--terra-dark); }

.fieldset-note {
  padding: 6px 0 10px;
  font-family: var(--mono);
  font-size: 10.5px;
  line-height: 1.65;
  color: var(--muted);
}

/* A parameter row: the name on the left, a filled ruled cell on the right.
   The fill is what says the number is yours to change — a printed value on
   the page background reads as a result, not a control. */
.field {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  min-height: 38px;
}

/* `.field` sets `display: flex`, which beats the UA's `[hidden]` rule — the
   container mode hides the rows the other mode uses. */
.field[hidden] { display: none; }

.field-head { display: inline-flex; align-items: center; gap: 7px; min-width: 0; }

.field-head label,
.field-label {
  font-size: 13px;
  color: var(--ink-2);
  border-bottom: 1px dotted var(--rule-hair);
  padding-bottom: 1px;
  cursor: help;
}
.field:hover .field-head label,
.field:hover .field-label { border-bottom-color: var(--terra-dark); color: var(--ink); }

.cell {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  width: 116px;
  height: 30px;
  flex-shrink: 0;
  padding: 0 8px 0 4px;
  border: 1px solid var(--rule-hair);
  background: var(--field);
  box-shadow: inset 0 1px 0 var(--cell-shadow);
  transition: border-color 0.15s;
}
.cell:hover { border-color: var(--ink); }
.cell:focus-within { border-color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink); }

.cell input[type="number"] {
  width: 100%;
  min-width: 0;
  height: 26px;
  padding: 0 0 0 6px;
  border: 0;
  background: transparent;
  font-family: var(--mono);
  font-variant-numeric: tabular-nums;
  font-size: 13px;
  text-align: right;
  outline: none;
}

.unit {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--muted);
  flex-shrink: 0;
}

/* The two steppers are keys beside the cell, not the browser's own spinner:
   they wear the STEP/STL toggle's dress, so the column has one button
   vocabulary instead of a control the app did not draw, and the target goes
   from the spinner's ~8px arrow to 28x30px. app.js builds them per number
   input (`buildSteppers`), which is also why the spinner is hidden here
   rather than on `.cell` — without that script there are no keys, and the
   browser's arrows are the only way left to step a field. */
.stepper { display: inline-flex; align-items: center; gap: 4px; flex-shrink: 0; }
.stepper .cell { width: 82px; padding: 0 8px; }
.stepper .cell input[type="number"] {
  padding-left: 0;
  -moz-appearance: textfield;
  appearance: textfield;
}
.stepper .cell input[type="number"]::-webkit-outer-spin-button,
.stepper .cell input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

/* `.step-key`, never `.key`: the sheet already owns that name for its
   smallest voice — every column header, unit and annotation wears it — and a
   second definition here boxed all of them in 28x30 borders. */
.step-key {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 30px;
  flex: none;
  padding: 0;
  border: 1px solid var(--rule-hair);
  background: var(--field);
  color: var(--ink-3);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.step-key:hover:not(:disabled) { border-color: var(--ink); color: var(--ink); }
.step-key:active:not(:disabled) { background: var(--sel-bg); border-color: var(--sel-bg); color: var(--sel-fg); }
.step-key:focus-visible { outline: 2px solid var(--terra); outline-offset: 2px; }
/* At the end of a field's range the key goes quiet rather than clicking into
   nothing — something the native spinner never said out loud. */
.step-key:disabled { color: var(--faint); border-color: var(--rule-soft); background: transparent; cursor: default; }
.step-key svg { display: block; }

/* A parameter the server would reject is redlined the moment it is typed,
   rather than surviving as far as a 422. The cell takes --bad over the
   --err-bg wash the error strip already uses, the `i` badge becomes a
   permanent `!` — hidden-until-hover is right for an explanation and wrong
   for a fault — and the reason prints under the row in .fieldset-note's
   tier. `.field-flag` is a sibling of `.field`, so the row keeps its own
   height and the cell keeps its baseline. */
.field.bad .cell { border-color: var(--bad); background: var(--err-bg); }
.field.bad .cell:hover,
.field.bad .cell:focus-within { border-color: var(--bad); box-shadow: inset 0 0 0 1px var(--bad); }
.field.bad .cell input[type="number"] { color: var(--err-fg); }
.field.bad .field-head label { color: var(--err-fg); border-bottom-color: var(--bad); }

/* A parameter the mode on show does not use — the slip well's width and
   flare while the standard container fixes the collar, or the container's own
   sizes while it is off. Greyed by colour rather than opacity, for the same
   reason as the `i` badge: opacity would fade the tooltip explaining why. */
.field:has(.cell input:disabled) .field-head label { color: var(--faint); }
.cell:has(input:disabled) { background: transparent; box-shadow: none; border-color: var(--rule-soft); }
.cell:has(input:disabled):hover { border-color: var(--rule-soft); }
.cell input[type="number"]:disabled,
.cell:has(input:disabled) .unit { color: var(--faint); }
/* Beats both `.field:hover .info` and `.field:hover .info:hover`, which would
   otherwise hold the badge at --muted or --terra-dark over a flagged row. */
.field.bad .info,
.field.bad:hover .info,
.field.bad:hover .info:hover { color: var(--bad); border-color: var(--bad); }

.field-flag {
  display: flex;
  align-items: flex-start;
  gap: 6px;
  margin: -3px 0 9px;
  font-family: var(--mono);
  font-size: 10.5px;
  line-height: 1.55;
  color: var(--err-fg);
}
.field-flag svg { flex: none; margin-top: 2px; color: var(--bad); }

/* Checkbox settings wear the same cell, so the column has one control
   vocabulary rather than two. */
.format-toggles { display: flex; gap: 6px; flex-shrink: 0; }

.toggle {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  height: 30px;
  padding: 0 10px;
  border: 1px solid var(--rule-hair);
  background: var(--field);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.toggle:hover { border-color: var(--ink); }
.toggle input { position: absolute; opacity: 0; pointer-events: none; }

.toggle-box {
  position: relative;
  width: 12px;
  height: 12px;
  flex-shrink: 0;
  border: 1px solid var(--rule-hair);
  background: var(--box-bg);
}
/* The filled box already reads as "on", but only against its neighbour. The
   tick says so on its own, which is what a box scanned in isolation needs. */
.toggle-box::after {
  content: "";
  position: absolute;
  top: 1.5px;
  left: 3.5px;
  width: 3px;
  height: 6px;
  border: solid var(--sel-bg);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
  opacity: 0;
  transition: opacity 0.15s;
}
.toggle-text { font-family: var(--mono); font-size: 11px; color: var(--muted); }

.toggle input:checked ~ .toggle-box {
  border-color: var(--sel-fg);
  background: var(--sel-fg);
}
.toggle input:checked ~ .toggle-box::after { opacity: 1; }
.toggle input:checked ~ .toggle-text { color: var(--sel-fg); }
.toggle:has(input:checked) { border-color: var(--sel-bg); background: var(--sel-bg); }
.toggle input:focus-visible ~ .toggle-box { outline: 2px solid var(--terra); outline-offset: 2px; }

#imported-model-settings .toggle { width: 116px; justify-content: flex-start; }

/* Tooltips. The name carries a dotted rule as the standing hint; hovering the
   row surfaces the `i`, and the explanation opens only on the badge itself
   (or its focus). Two steps, so running the pointer down the column doesn't
   throw a popover over the rows below every field it passes. It is laid out
   inside the column, because the column scrolls and would clip anything
   reaching past its edge. */
.info {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 15px;
  height: 15px;
  padding: 0;
  border: 1px solid transparent;
  border-radius: 50%;
  background: transparent;
  /* Hidden by colour rather than by `opacity`: opacity composites the whole
     element, tooltip included, so the popover below could never be more
     opaque than the badge fading in above it. */
  color: transparent;
  font: 600 9px/1 var(--sans);
  cursor: help;
  transition: color 0.15s, border-color 0.15s;
}
.field:hover .info { color: var(--muted); border-color: var(--rule-hair); }
/* The `.field:hover` prefix is not redundant — it buys the specificity to
   beat the rule above, which would otherwise hold the badge at --muted. */
.field:hover .info:hover { color: var(--terra-dark); border-color: var(--terra); }
.info:focus-visible { outline: none; border-color: var(--terra); color: var(--terra-dark); }

.info::after,
.reset::after {
  content: attr(data-tip);
  position: absolute;
  z-index: 30;
  width: 300px;
  padding: 12px 14px;
  border: 1px solid var(--tip-rule);
  background: var(--tip-bg);
  color: var(--tip-fg);
  font: 400 12px/1.6 var(--sans);
  letter-spacing: 0;
  text-align: left;
  text-transform: none;
  box-shadow: 0 14px 34px -12px var(--tip-shadow);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.15s;
  pointer-events: none;
}
/* Both are laid out inside the parameters column, which is the only thing
   that keeps them off the scrolling body's clip. The badge hangs from its own
   row; the reset's anchor is the head — the one box up there that is not the
   scrolling region — so it lines up with the column's content edge instead of
   with a button sitting 100px into it. */
.info::after { top: calc(100% - 2px); left: 0; }
.reset::after { top: calc(100% + 1px); left: 18px; }

.info:hover::after,
.info:focus-visible::after,
.reset:hover::after,
.reset:focus-visible::after { opacity: 1; visibility: visible; }

/* ── Reset parameters ────────────────────────────────────────────────
   Held rather than clicked: a reset discards every number in the column, and
   the two seconds are the warning that it is about to. Two copies of the one
   glyph share a grid cell — the muted outline, and an inverted terracotta
   chip clipped from the bottom by --charge, which app.js drives 0 → 1 across
   the hold. Solid is the signal that releasing now will reset.

   Stacked by grid rather than by `position: absolute`, so the button itself
   stays unpositioned and its tooltip still resolves to the head. */

.head-group { display: inline-flex; align-items: center; gap: 9px; min-width: 0; }

.reset {
  --charge: 0;
  flex: none;
  display: grid;
  width: 20px;
  height: 20px;
  padding: 0;
  border: 1px solid var(--rule-hair);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  /* A hold is not a scroll: without this, holding on a touch screen drags
     the page out from under the press and the charge never completes. */
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
  transition: color 0.15s, border-color 0.15s;
}
.reset:hover { color: var(--terra-dark); border-color: var(--terra); }
.reset:focus-visible { outline: none; border-color: var(--terra); color: var(--terra-dark); }
.reset.is-armed { border-color: var(--terra); }
.reset svg { width: 13px; height: 13px; }

.reset-face { grid-area: 1 / 1; display: flex; align-items: center; justify-content: center; }
.reset-fill {
  background: var(--terra);
  /* Terracotta stays light in both themes, so the ink over it is a literal —
     the same pairing .btn uses for the same reason. */
  color: #1B1512;
  clip-path: inset(calc((1 - var(--charge)) * 100%) 0 0 0);
  transition: clip-path 0.2s ease-out;
}
/* The animation frame owns the fill while it is filling; the transition is
   only there to drain it on release. */
.reset.is-holding .reset-fill { transition: none; }
/* A reset that has actually happened has to look different from one that was
   abandoned half-charged, and both end at empty. The class is cleared at the
   start of the next hold rather than on `animationend`, so nothing it sets may
   outlive its own animation — hence the flash and no lasting border. */
.reset.is-done { animation: reset-flash 0.5s ease-out; }
@keyframes reset-flash {
  from { background: var(--terra); color: #1B1512; }
  to   { background: transparent; }
}

/* ── Buttons ─────────────────────────────────────────────────────── */

/* `.btn` and `.btn-secondary` themselves are in account.css, which loads
   first: the billing dialogs are built from them and appear on every page.
   What follows tunes them for the app. */

/* Shut, but not `disabled`.
 *
 * Three of the reasons Generate cannot run are things the app can *do*
 * something about — not signed in, a mesh on the free tier, a month's
 * allowance spent — and each has a dialog that offers the fix. A `disabled`
 * button fires no click, so it can only refuse silently and leave the user
 * to guess which of the four columns is at fault. So those three wear the
 * disabled dress and stay clickable, and the click opens the answer.
 *
 * The two states are told apart by `aria-disabled` rather than by the class,
 * so a screen reader is told "unavailable" in both cases while only one of
 * them can be pressed. */
.btn.is-locked {
  background: var(--field);
  border-color: var(--rule-hair);
  color: var(--faint);
  cursor: pointer;
}
.btn.is-locked:hover { background: var(--field); border-color: var(--ink-3); color: var(--ink-3); }

/* The allowance, at the button's right edge.
 *
 * Absolutely placed rather than a third flex child: the label and its arrow
 * are centred as a pair, and a count in the flow would push them off-centre
 * by however many characters it happened to be — the button would shift when
 * "5/5" became "10/5" on a plan with a larger allowance. Positioned, the
 * label stays where it is and the count sits in the margin the 50px height
 * already provides. */
.btn-primary { position: relative; }

.gen-quota {
  position: absolute;
  top: 50%;
  right: 14px;
  transform: translateY(-50%);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  /* Its own colour rather than the button's: on the terracotta face the
     label is near-black and a count at that weight competes with it. */
  color: #4A3229;
  cursor: help;
}
.btn.is-locked .gen-quota,
.btn:disabled .gen-quota { color: var(--faint); }

.download-btn { height: 46px; background: var(--ink); color: var(--paper); font-size: 14px; }
.download-btn:hover { background: var(--ink-2); color: var(--paper); }

.btn-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border: 0;
  background: transparent;
  color: var(--muted);
  font-size: 17px;
  line-height: 1;
  cursor: pointer;
}
.btn-icon:hover { color: var(--bad); }

/* ── Column 2 · the file on screen ───────────────────────────────── */

.tabs { display: flex; gap: 20px; }

.tab {
  padding: 0 0 2px;
  border: 0;
  border-bottom: 2px solid transparent;
  background: none;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
  cursor: pointer;
}
.tab:hover:not(:disabled) { color: var(--ink); }
.tab.is-active { color: var(--ink); border-bottom-color: var(--terra); }
.tab:disabled { color: var(--faint); cursor: default; }

.stage {
  position: relative;
  flex-grow: 1;
  min-height: 0;
  background-image:
    repeating-linear-gradient(to right, var(--grid-fine) 0 1px, transparent 1px 16px),
    repeating-linear-gradient(to bottom, var(--grid-fine) 0 1px, transparent 1px 16px),
    repeating-linear-gradient(to right, var(--grid-coarse) 0 1px, transparent 1px 80px),
    repeating-linear-gradient(to bottom, var(--grid-coarse) 0 1px, transparent 1px 80px);
}

/* The viewer paints its own background and its own grid, so the sheet's grid
   is not underneath it — it is the band of squares left showing beside the
   preview header, cut off where the canvas starts. A drawing keeps the grid;
   a mesh replaces it. */
.stage.has-viewer { background-image: none; }

.stage-view { position: absolute; inset: 0; display: flex; flex-direction: column; }
.stage-view[hidden] { display: none; }

/* The README pane. Rough pass — it is the generated text as written, on the
   sheet's own paper, scrolling in the stage the way the viewer does. */
#readme-view { overflow: auto; background: var(--sheet); }
#readme-text {
  margin: 0;
  padding: 18px 20px;
  font-family: var(--mono);
  font-size: 11px;
  line-height: 1.55;
  color: var(--ink);
  white-space: pre;
}

/* Drop zone: the empty state fills the pane rather than sitting in it. */
.drop-zone {
  flex-grow: 1;
  margin: 22px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px dashed var(--rule-hair);
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
}
.drop-zone:hover, .drop-zone:focus-visible { border-color: var(--ink); outline: none; }
.drop-zone.dragover { border-color: var(--terra); background: var(--drag-bg); }
.drop-zone[hidden] { display: none; }

.drop-zone-content { display: flex; flex-direction: column; align-items: center; gap: 8px; text-align: center; padding: 24px; }
.drop-icon { color: var(--faint); margin-bottom: 6px; }
#drop-label { font-size: 15px; color: var(--ink-2); }
.link-text { color: var(--terra-dark); text-decoration: underline; text-underline-offset: 2px; }
.drop-hint { font-family: var(--mono); font-size: 10.5px; line-height: 1.6; color: var(--muted); max-width: 380px; }

/* Uploaded file, shown as supplied. */
.svg-preview { flex-grow: 1; display: flex; flex-direction: column; min-height: 0; }
.svg-preview[hidden] { display: none; }

.preview-header {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 14px 18px 10px;
}
.preview-header-right { display: flex; align-items: center; gap: 10px; min-width: 0; }
.filename { font-family: var(--mono); font-size: 12px; color: var(--ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* The pane is a viewport, not a slot: a drawing is fitted into it and can
   then be panned and zoomed, and a mesh gets the parts viewer mounted
   straight into it. Both need a positioned box with nothing spilling out. */
.svg-render {
  position: relative;
  flex-grow: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 24px 24px;
  overflow: hidden;
}
.svg-render.is-drawing { padding: 0; cursor: grab; touch-action: none; }
.svg-render.is-drawing.is-panning { cursor: grabbing; }
.svg-render.is-model { padding: 0; }

/* Fitted at scale 1, so "Fit" is the identity transform and the readout has
   a baseline that means something. */
.svg-stage {
  position: absolute;
  inset: 18px 24px 56px;
  transform-origin: center center;
}
.svg-stage svg { width: 100%; height: 100%; overflow: visible; }

.preview-toolbar {
  position: absolute;
  left: 18px;
  right: 18px;
  bottom: 16px;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
}
.preview-toolbar .viewer-btn { min-width: 40px; }


.model-note {
  max-width: 420px;
  font-family: var(--mono);
  font-size: 11.5px;
  line-height: 1.7;
  color: var(--muted);
  text-align: center;
}

/* ── Disclosures ─────────────────────────────────────────────────── */
/* Two panels collapse — the job log and Recent — and they use one control so
   the gesture is the same in both: the whole head row is the <button>, and a
   chevron on the far right is the only thing that moves. The head keeps its
   own layout (.logstrip-head, .col-head); this only strips the button's UA
   styling and adds the affordance. */

.disclose {
  width: 100%;
  /* A <button> brings a 2px outset border with it, and the heads only ever
     set border-bottom — so without this the row is boxed in UA white. */
  border: 0;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.disclose:focus-visible { outline: 1px solid var(--terra); outline-offset: -2px; }
/* Nothing to disclose — the control says so rather than opening onto a box
   with nothing in it. */
.disclose[disabled] { cursor: default; }
.disclose[disabled] .chevron { opacity: 0.25; }
.disclose:hover:not([disabled]) .chevron { color: var(--ink); }

/* The right-hand group: whatever the row reports while closed, then the
   chevron last so it sits hard against the row's end. */
.disclose-meta { display: flex; align-items: center; gap: 14px; min-width: 0; }

.chevron {
  flex-shrink: 0;
  width: 12px;
  height: 12px;
  color: var(--muted);
  transition: transform 0.18s ease;
}
.disclose[aria-expanded="true"] .chevron { transform: rotate(180deg); }

/* ── Job log ─────────────────────────────────────────────────────── */

.logstrip { flex-shrink: 0; border-top: 1px solid var(--rule); background: var(--panel); }
.logstrip[hidden] { display: none; }

/* The head row is the disclosure control for the stage list below it (see
   .disclose above). It keeps its bottom rule only while the list it
   introduces is actually open. */
.logstrip-head {
  height: 32px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 0 18px;
  border-bottom: 1px solid transparent;
  background: none;
}
.logstrip.open .logstrip-head { border-bottom-color: var(--rule-soft); }
#progress-stage { color: var(--ink); text-transform: none; letter-spacing: 0.04em; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
#progress-text { color: var(--terra-dark); }

.status-badge {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}
.status-badge.queued { color: var(--muted); }
.status-badge.processing { color: var(--terra-dark); }
.status-badge.completed { color: var(--ok); }
.status-badge.failed { color: var(--bad); }
/* Not --bad. Nothing about the generation failed and the user has their
   parts; what was lost is the copy in their library, which is the ochre the
   banner uses for "you should know this" rather than the red for "this
   broke". */
.status-badge.storage_failed { color: var(--notice-ink); }

.stage-list {
  list-style: none;
  max-height: var(--disclosure-list);
  overflow-y: auto;
  padding: 8px 18px 10px;
}
.stage-list[hidden] { display: none; }

.stage-item {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 16px;
  font-family: var(--mono);
  font-size: 11.5px;
  line-height: 1.9;
  color: var(--muted);
}
.stage-item.running { color: var(--ink); }
.stage-item.failed { color: var(--bad); }
.stage-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.stage-time { flex-shrink: 0; color: var(--faint); }
.stage-item.running .stage-time::after { content: '…'; }

.progress-track { height: 3px; background: var(--prog-track); }
.progress-bar { height: 100%; background: var(--terra); transition: width 0.3s ease; }

.error-display {
  flex-shrink: 0;
  padding: 12px 18px;
  border-top: 1px solid var(--bad);
  background: var(--err-bg);
  color: var(--err-fg);
  font-size: 13px;
  line-height: 1.6;
  white-space: pre-line;   /* the geometry errors are written in paragraphs */
  max-height: 176px;
  overflow-y: auto;
}
.error-display[hidden] { display: none; }

/* ── Column 3 · manifest ─────────────────────────────────────────── */

.col-out .col-body { padding: 0; }

.file-list { display: flex; flex-direction: column; }

/* One row per PART, not per file. A part's .step and .stl are the same object
   in two formats, so they share a row: the name line carries the identity and
   the View button (STL-only, so it reads as "look at this part" rather than
   "open this file"), and the two format buttons below it are the individual
   downloads, each with its own size. */
.part-item {
  padding: 7px 10px 8px 18px;
  border-bottom: 1px solid var(--rule-soft);
  min-width: 0;    /* flex items default to auto and would spill the column */
}
.part-item:hover { background: var(--panel); }

.part-head { display: flex; align-items: center; gap: 8px; height: 22px; min-width: 0; }
.part-name {
  font-family: var(--mono);
  font-size: 12.5px;
  color: var(--ink);
  flex: 1 1 0;
  min-width: 0;
  white-space: pre;   /* the fit is measured, so the browser must not wrap it */
}

.part-formats { display: flex; gap: 6px; margin-top: 5px; }
.file-fmt {
  flex: 1 1 0;
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  height: 24px;
  padding: 0 8px;
  border: 1px solid var(--rule-hair);
  background: var(--field);
  color: var(--ink-2);
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.file-fmt:hover { border-color: var(--ink); background: var(--view-hover); color: var(--ink); }
.file-fmt svg { flex-shrink: 0; color: var(--muted); }
.file-fmt:hover svg { color: var(--terra-dark); }
.file-ext { text-transform: uppercase; }
.file-size { margin-left: auto; color: var(--faint); font-variant-numeric: tabular-nums; }

.file-view {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  gap: 6px;
  height: 22px;
  padding: 0 9px;
  border: 1px solid var(--rule-hair);
  background: var(--field);
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  color: var(--terra-dark);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}
.file-view:hover { border-color: var(--ink); background: var(--view-hover); }
.file-view.is-viewing { background: var(--sel-bg); border-color: var(--sel-bg); color: var(--sel-fg); }

.manifest-expected { display: flex; flex-direction: column; }
.manifest-expected[hidden] { display: none; }
.manifest-expected div {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 36px;
  padding: 0 18px;
  border-bottom: 1px solid var(--rule-soft);
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--faint);
}

/* The storage line. Mono like every other annotation in this column, and a
   tier quieter than a manifest row while it is only saying "in progress" —
   the parts below it are the news, this is a footnote to them. Failed, it
   takes --bad and the wash the error strip uses, because at that point it is
   the only place the loss is stated. */
.storage-note {
  margin: 0;
  padding: 10px 18px;
  border-bottom: 1px solid var(--rule-soft);
  font-family: var(--mono);
  font-size: 11px;
  line-height: 1.55;
  color: var(--muted);
}
.storage-note[hidden] { display: none; }

/* Adjustments the job made to its settings — a kerf count reduced to fit.
   The storage line's dress, in the accent: the parts are fine and this is
   worth reading before printing them, but it is not a failure. */
.job-warnings {
  margin: 0;
  padding: 10px 18px 10px 32px;
  border-bottom: 1px solid var(--rule-soft);
  font-family: var(--mono);
  font-size: 11px;
  line-height: 1.55;
  color: var(--terra-dark);
}
.job-warnings[hidden] { display: none; }
.job-warnings li + li { margin-top: 6px; }
.storage-note.is-failed {
  color: var(--err-fg);
  background: var(--err-bg);
  border-bottom-color: var(--bad);
}

/* Download all takes a line of its own; "Apply these settings" and "New job"
   are peers and share the next one, which is what keeps this foot two rows
   deep instead of three in a column that already ends in RECENT. Wrapping
   rather than a nested row: a hidden button then takes no space at all, and
   there is no empty wrapper for anything to have to collapse. */
.results-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.results-actions .download-btn { flex: 1 0 100%; }
.results-actions .btn-secondary { flex: 1 1 0; }
.results-actions[hidden], .results-actions .btn[hidden] { display: none; }

/* Collapsed by default, same as the job log. Both rules are --rule-soft, the
   weight the manifest rows use — the heavier --rule the other column foot
   carries reads as a boxed-off panel on a head this small. */
.col-foot-list { border-top-color: var(--rule-soft); }
.history-head { border-bottom: 1px solid transparent; }
.history-head[aria-expanded="true"] { border-bottom-color: var(--rule-soft); }

.history-list { overflow-y: auto; max-height: var(--disclosure-list); padding: 4px 0 8px; }
.history-list[hidden] { display: none; }

/* The same 3px band the job log ends on, so the two column feet bottom out
   together. The dividers are the container showing through 1px grid gaps —
   one line between cells and none at either end, at the weight of every other
   rule on the sheet. The column count comes from HISTORY_LIMIT, so the bar
   can't disagree with the list about how many slots there are. */
.status-track { display: grid; gap: 1px; height: 3px; background: var(--rule-soft); }
.status-cell { background: var(--prog-track); }
.status-cell.completed { background: var(--ok); }
.status-cell.failed { background: var(--bad); }
.status-cell.processing { background: var(--terra-dark); }
.status-cell.queued { background: var(--muted); }

.history-item {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  height: 32px;
  padding: 0 18px;
  border: 0;
  background: none;
  text-align: left;
}
.history-item.replayable { cursor: pointer; }
.history-item.replayable:hover { background: var(--panel-dark); }

/* Rows whose bytes are in the bucket rather than in this browser, and the
   line that fetches the next page of them. Both are tuned as the quiet tier
   the sheet already owns rather than as new components: a stored job is an
   ordinary history row that opens a little slower, not a different kind of
   thing, so it wears the same 32px height and the same hover. */
.history-item.history-older { cursor: pointer; color: var(--muted); }
.history-item.history-older:hover:not(:disabled) { background: var(--panel-dark); }
.history-item.history-older:disabled { cursor: default; color: var(--faint); }
.history-item.history-older .history-label {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

/* The `stored` marker on a remote row. `--faint` carries it at 10px, which is
   the key tier rather than real text, so it is held to that contrast and not
   to the 4.5:1 the .stage-time / .empty-state sizes need. */
.history-remote {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--faint);
  flex-shrink: 0;
}

.history-label { display: flex; align-items: baseline; gap: 10px; min-width: 0; }
.history-name { font-family: var(--mono); font-size: 11.5px; color: var(--ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.job-time { font-family: var(--mono); font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); flex-shrink: 0; }

.empty-state {
  padding: 6px 18px;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
}

/* ── 3D viewer ───────────────────────────────────────────────────── */
/* Mounted into the stage pane (#viewer-host), so the part is shown where the
   source file was rather than over the top of the page. The overlay rules
   below are the fallback for a page that provides no host. */

#viewer-host { inset: 0; }

/* Mounted into a pane that already names the file and offers a way to drop
   it — a second title and close would be two controls for one thing. */
.viewer-bare .viewer-header { display: none; }

.viewer-inline { position: absolute; inset: 0; }
.viewer-inline[hidden] { display: none; }
.viewer-inline .viewer-panel { position: relative; width: 100%; height: 100%; }

.viewer-canvas-wrap { position: absolute; inset: 0; }
.viewer-canvas-wrap canvas { display: block; width: 100%; height: 100%; }

.viewer-header {
  position: absolute;
  top: 14px;
  left: 18px;
  right: 18px;
  z-index: 2;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  pointer-events: none;
}
.viewer-title {
  font-family: var(--serif);
  font-size: 26px;
  line-height: 1.1;
  color: var(--ink);
  word-break: break-all;
}
.viewer-close { pointer-events: auto; font-size: 20px; }

.viewer-toolbar {
  position: absolute;
  left: 18px;
  right: 18px;
  bottom: 16px;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
}
.viewer-views { display: flex; gap: 6px; }

.viewer-btn {
  height: 34px;
  padding: 0 14px;
  border: 1px solid var(--rule-hair);
  background: var(--field);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-3);
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, color 0.15s;
}
.viewer-btn:hover { border-color: var(--ink); }
.viewer-btn.active { background: var(--sel-bg); border-color: var(--sel-bg); color: var(--sel-fg); }

.viewer-meta {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}
/* Inline, the stage header already carries this hint. */
.viewer-inline .viewer-hint { display: none; }

.viewer-hint {
  position: absolute;
  right: 18px;
  top: 20px;
  z-index: 2;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
}

.viewer-status {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  background: var(--sheet);
  font-family: var(--mono);
  font-size: 11.5px;
  color: var(--muted);
}
.viewer-status[hidden] { display: none; }

.viewer-spinner {
  width: 22px;
  height: 22px;
  border: 1px solid var(--rule-hair);
  border-top-color: var(--terra);
  border-radius: 50%;
  animation: viewer-spin 0.8s linear infinite;
}
.viewer-spinner[hidden] { display: none; }
@keyframes viewer-spin { to { transform: rotate(360deg); } }

/* Overlay fallback (no #viewer-host on the page). */
.viewer-overlay {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 40px;
  background: var(--overlay);
}
.viewer-overlay[hidden] { display: none; }
body.viewer-open { overflow: hidden; }
.viewer-overlay .viewer-panel {
  position: relative;
  width: min(1000px, 100%);
  height: min(720px, 100%);
  background: var(--sheet);
  border: 1px solid var(--ink);
}

/* ── Narrow screens ──────────────────────────────────────────────── */

@media (max-width: 1180px) {
  body { overflow: auto; }
  /* See `.is-modal-open` above: this is the half that only applies where the
     document actually scrolls. */
  :root.is-modal-open body {
    position: fixed;
    width: 100%;
  }
  .sheet { height: auto; grid-template-columns: minmax(0, 1fr); }
  .col { min-height: 0; }
  .col-params, .col-stage { border-right: 0; border-bottom: 1px solid var(--rule); }
  .col-body { overflow: visible; }
  .stage { height: 60vh; min-height: 380px; }
  /* Stacked, a row would otherwise stretch its label and its cell to opposite
     ends of the window. */
  .col-params .col-body, .col-params .col-foot,
  .col-out .col-body, .col-out .col-foot { max-width: 680px; margin: 0 auto; width: 100%; }
  .history-list, .stage-list { max-height: none; }
  .info::after { width: min(300px, calc(100vw - 60px)); }
}

/* ── The banner takes a row of its own ────────────────────────────────
   Not at the 700px phone tier: at 860 the bar still carries the readout and
   "MOLD GENERATOR", so the middle is squeezed from both sides and a message
   is ellipsed to a few words long before anything else is dropped. 860 is the
   number landing.css already uses for the same judgement.

   Grid rather than `flex-wrap`, and the reason is the wordmark: `.rulebar-left`
   aligns on the *baseline*, so giving it a 54px height to hold the first line
   open would align its contents to the top of that box instead of centring
   them. As a grid row of a fixed height with a shrink-to-fit group centred in
   it, the group's own internal alignment is untouched — the bar above the
   banner looks exactly as it does today.

   Nothing above 1180 is affected, which is where `.sheet` subtracts `--bar`
   from the viewport; below it the sheet is already `height: auto` and the
   page scrolls, so a bar that grows a second row costs no layout math. */
@media (max-width: 860px) {
  .rulebar {
    display: grid;
    grid-template-columns: auto auto;
    /* Row 2 is `auto`, and `.banner-slot[hidden]` removes the item entirely —
       so with no notice the bar is exactly the height it always was. */
    grid-template-rows: var(--bar) auto;
    align-items: center;
    height: auto;
  }
  .rulebar-left { grid-row: 1; grid-column: 1; }
  .rulebar-right { grid-row: 1; grid-column: 2; }
  /* Full-bleed: the bar's own side padding is cancelled so the band meets
     both edges, the way the rule beneath it does. */
  .banner-slot {
    grid-row: 2;
    grid-column: 1 / -1;
    width: calc(100% + 40px);
    margin: 0 -20px;
    padding: 0;
  }
  .banner {
    width: 100%;
    max-width: none;
    height: 44px;
    padding: 0 0 0 14px;
  }
  /* The dismiss is the only control in the band and a phone is where it is
     hardest to hit. */
  .banner-close { width: 44px; height: 44px; }
}

/* ── Phones ──────────────────────────────────────────────────────── */

@media (max-width: 700px) {
  /* Nothing in the bar wraps, so a phone is a budget. What comes out is the
     two labels that restate something already on screen — the readout names
     the mold-type card selected and outlined just below it, and "MOLD
     GENERATOR" says what the wordmark beside it already says. What stays is
     the wordmark, the appearance switch and the account, which is now one
     glyph wide when signed in. The health badge that used to take 90px of
     this budget is gone entirely. */
  .rulebar-readout { display: none; }
  .rulebar-left .key { display: none; }
  .rulebar { padding: 0 14px; }
  /* The bleed is cancelling the bar's padding, so it tracks it. */
  .banner-slot { width: calc(100% + 28px); margin: 0 -14px; }
  /* The word comes out too and the mark carries the wordmark alone — it is the
     C, so what is left still reads as the logo rather than as a cropped one.
     The h1 grows because the mark is sized in em: nothing else is left in it. */
  .wordmark-tail { display: none; }
  .rulebar h1 { font-size: 27px; }
  /* Baselines are for a row of type. With the label gone the row is a
     wordmark and a boxed control, and the toggle — having no text baseline
     of its own — would hang its bottom edge off the wordmark's. */
  .rulebar-left { gap: 12px; align-items: center; }
}
