/* Morcano - dark, quiet, one column, nothing decorative. */

:root {
  /* PURE NEUTRAL GREYS. R=G=B on every surface colour: the previous near-blacks
     (#0d0d0f blue-biased, #0F110E green-biased) are gone. --bg is the SAME value
     the renderer clears the canvas to (BACKDROP in app.js), so the opaque
     viewport and the page are one colour with no seam. */
  --bg: #101010;

  /* Panel cards: solid, one step lighter than the page, with a subtle neutral
     hairline (brighter at the top edge to read as a lit rim). No heavy fill. */
  --card: #161616;
  --glass-border: rgba(255, 255, 255, 0.09);
  --glass-border-top: rgba(255, 255, 255, 0.14);
  --glass-shadow:
    0 18px 48px -18px rgba(0, 0, 0, 0.55),
    0 2px 8px -2px rgba(0, 0, 0, 0.45);
  --surface: #161616;

  /* Form field fill -- inputs, selects, the dial fields. One consistent step
     above the panel so every control reads as the same kind of surface. */
  --field: #1b1b1b;
  --field-hover: #202020;

  --line: rgba(255, 255, 255, 0.07);
  --line-strong: rgba(255, 255, 255, 0.13);
  --ink: rgba(255, 255, 255, 0.95);
  --ink-mid: rgba(255, 255, 255, 0.64);
  --ink-dim: rgba(255, 255, 255, 0.5);
  /* 0.48: secondary text still reads at 4.5:1 on the dark ground (was 0.34, 3.1:1). */
  --ink-faint: rgba(255, 255, 255, 0.48);
  --track: rgba(255, 255, 255, 0.12);
  --fill: rgba(255, 255, 255, 0.9);
  --radius: 16px;
  --radius-ctl: 10px;

  --serif: "Playfair Display", Georgia, "Times New Roman", serif;

  /* The accent is the landing page's: a warm light ink for what is ON, and a
     graphite gradient for the heavy buttons. GREEN IS NOT AN ACCENT here -- it
     is kept for print-readiness alone (the status pill and the gate chips), so
     green on this screen always means "this will print".
     It must never reach the canvas or the environment map -- that is the whole
     lesson of the violet cast, which came from the material and the tone
     mapping rather than from the page, and was only ever visible because the
     model was being lit by something tinted. A button in the side panel cannot
     do that; a background behind a transparent canvas can. */
  --accent: #e9d8d0;
  --accent-hover: #f6ece7;
  --accent-soft: rgba(233, 216, 208, 0.12);
  --accent-line: rgba(233, 216, 208, 0.34);
  --accent-ink: #171513;                 /* text on a filled accent */
  /* The primary button is plain white with dark ink: no gradient, no glow, no
     bold. One face for every primary action on both screens. */
  --btn-face: #ffffff;
  --btn-face-hover: #eceae7;
  --btn-face-line: transparent;
  --btn-face-ink: #121212;
}

* { box-sizing: border-box; }

/* Links inherit. Without this, any anchor drawn before its stylesheet has
   arrived - the desk menu is loaded at runtime, so its links are - paints in the
   browser's own link colours for a frame: blue, and PURPLE once visited. That
   flash of violet down the left edge on the way into the studio was the browser
   showing its defaults, not the app showing a colour. Each place that wants an
   underline still asks for one. */
a { color: inherit; text-decoration: none; }

html, body {
  height: 100%;
  margin: 0;
}

body {
  background: var(--bg);
  color: var(--ink);
  font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  font-weight: 300;
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}

/* The violet/blue radial gradient that used to sit here is gone. The canvas was
   transparent so it could show through, which meant the page behind the model
   was saturated colour -- and a filament colour cannot be judged against that
   however neutral the lighting is. The backdrop is now a flat colour set on the
   renderer itself. */

.shell {
  position: relative;
  z-index: 1;
  height: 100%;
  display: grid;
  grid-template-columns: 1fr 372px;
  /* Bound the row to the viewport height (minmax floor of 0, not auto) so the
     columns cannot grow past the window -- that is what lets the panel scroll
     internally instead of the whole page being clipped and EXPORT lost. */
  grid-template-rows: minmax(0, 1fr);
  gap: 40px;
  padding: 40px 40px 36px;
}

/* ---------------------------------------------------------------- stage */

/* The stage is a positioning context: the viewport fills it edge to edge and
   the logo + meta float over the top-left corner. Because the canvas and the
   page share one colour there is no seam -- the overlay looks like it sits on
   the canvas, but it is a sibling in normal document order. */
.stage {
  position: relative;
  min-width: 0;
  min-height: 0;
}

.titles {
  position: absolute;
  top: 4px;
  left: 0;                /* flush with the library block below (and == the
                             panel's right margin), so both sides inset equally */
  z-index: 3;
  width: 300px;
  pointer-events: none;   /* the canvas orbits underneath the empty space */
}
.titles > * { pointer-events: auto; }

/* MORCANO: high-contrast editorial serif, small-caps proportions -- a full-cap
   initial with smaller caps following, from font-variant rather than by
   re-typing the letters. Small, spaced, white. */
.wordmark {
  margin: 0;
  font-family: var(--serif);
  font-size: 25px;
  font-weight: 600;
  font-variant: small-caps;
  letter-spacing: 0.06em;
  line-height: 1;
  color: #fff;
}

/* The status card under the logo: verdict, then the facts, then the gate's own
   measurements, then alerts. It reads top-down in decreasing urgency, and every
   part except the verdict disappears when it has nothing to say -- so a healthy
   ring is three short lines rather than a paragraph. */
.meta {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* Air between the wordmark and the status: the pill was sitting right under
     the logo and reading as part of it. */
  margin-top: 22px;
  gap: 14px;
}

/* The verdict, as a pill so it reads as a state rather than a sentence. */
.meta .status {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 10px 4px 8px;
  border-radius: 999px;
  border: 1px solid var(--line-strong);
  font-size: 11.5px;
  letter-spacing: 0.01em;
  color: var(--ink-mid);
  cursor: help;
}
/* The dot is the status colour; the label stays readable in every state. */
.meta .status::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: currentColor;
  flex: none;
}
.meta .status[data-ok] { color: #4ADE80; border-color: rgba(74, 222, 128, 0.32); }
.meta .status[data-idle] { color: var(--ink-dim); }
.meta .status[data-ok][data-warn] { color: #FDBA74; border-color: rgba(253, 186, 116, 0.34); }
.meta .status[data-bad] {
  color: rgba(255, 150, 150, 0.95);
  border-color: rgba(255, 150, 150, 0.34);
}

/* The facts: a label/value table, so the numbers line up in a column instead of
   running together as prose. Values are tabular so they do not jitter as the
   sliders move. */
.meta .facts {
  margin: 0;
  display: grid;
  gap: 8px;
  width: 100%;
  font-size: 12px;
}
.meta .fact {
  display: grid;
  grid-template-columns: 74px 1fr;
  align-items: baseline;
  gap: 8px;
}
/* display:grid beats the UA rule for [hidden], so the rows that are toggled by
   the attribute (Size on a product with no size, Filament before the first
   estimate) stayed on screen -- a ring's "US 7" survived the switch to a plate.
   Same guard as .busy[hidden] below. */
.meta .fact[hidden] { display: none; }
.meta .fact dt {
  color: var(--ink-faint);
  font-size: 11px;
  letter-spacing: 0.01em;
}
.meta .fact dd {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 6px;
  color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
}
/* "est." -- the caveat carried as a marker on the label rather than as a
   trailing clause on the value. The full explanation is the row's tooltip. */
.meta .fact dt .est {
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.meta .fact[title] { cursor: help; }

/* The material picker, sized to sit inline beside the number it scales. */
.mini-select {
  appearance: none;
  padding: 4px 23px 4px 10px;
  border-radius: 8px;
  border: 1px solid var(--line-strong);
  background-color: var(--field);
  color: var(--ink);
  font-family: inherit;
  font-size: 11.5px;
  letter-spacing: 0.02em;
  cursor: pointer;
  /* A drawn down-chevron, so it reads as a picker rather than a static tag. */
  background-image:
    linear-gradient(45deg, transparent 50%, var(--ink-dim) 50%),
    linear-gradient(135deg, var(--ink-dim) 50%, transparent 50%);
  background-position: right 11px center, right 7px center;
  background-size: 4px 4px, 4px 4px;
  background-repeat: no-repeat;
  transition: border-color 0.16s ease, background-color 0.16s ease;
}
.mini-select:hover { border-color: rgba(255, 255, 255, 0.26); background-color: var(--field-hover); }
.mini-select option { background: #1a1a1a; color: var(--ink); }

/* The gate readout in the meta block: the same #gate element the EXPORT block
   used, but stripped of its pill chrome into one compact grey dot-separated
   line -- "watertight yes · shells 1 · wall 2.02 mm". Values are live (from the
   gate); the seam chip is dropped here to keep the line short. */
.meta .gate {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0;
  padding-top: 3px;
  font-size: 11px;
  color: var(--ink-dim);
}
.meta .gate .chip {
  border: none;
  background: none;
  border-radius: 0;
  padding: 0;
  gap: 4px;
  font-size: 11px;
  cursor: default;
}
.meta .gate .chip:nth-child(n + 4) { display: none; }   /* drop seam (4th+) */
.meta .gate .chip:not(:last-child)::after {
  content: "·";
  margin: 0 6px;
  color: var(--ink-faint);
}
.meta .gate .chip .k {
  text-transform: none;
  font-size: 11px;
  letter-spacing: 0;
  color: var(--ink-dim);
}
/* Each token is coloured by its own gate status, read straight off the
   data-ok / data-bad the chip already carries (paintGate stamps one per gate).
   A pass goes green, a fail red, and a neutral chip -- "not sampled" or any
   not-applicable reading, which carries neither attribute -- stays the muted
   grey below so it raises no alarm. This is display only; the gate's own
   measurement is unchanged. */
.meta .gate .chip .v { color: var(--ink-dim); }
.meta .gate .chip[data-ok] .v,
.meta .gate .chip[data-ok] .k { color: #4ADE80; }
.meta .gate .chip[data-bad] .v,
.meta .gate .chip[data-bad] .k { color: #EF4444; }

.viewport {
  position: absolute;
  inset: 0;
}

#canvas {
  width: 100%;
  height: 100%;
  display: block;
  cursor: grab;
}
#canvas:active { cursor: grabbing; }

.viewport-hint {
  /* The brief lists only the logo, meta and status pill as things that may sit
     over the canvas, so the orbit hint is hidden. Kept in the DOM (its
     pointerdown handler is harmless) rather than removed. */
  display: none;
  position: absolute;
  left: 50%;
  bottom: 6px;
  translate: -50% 0;
  font-size: 11px;
  letter-spacing: 0.06em;
  color: var(--ink-faint);
  pointer-events: none;
  transition: opacity 0.6s ease;
}
.viewport-hint[data-gone] { opacity: 0; }

/* Floating view shortcuts, bottom-right of the canvas. Same frosted-pill
   language as the busy indicator so they read as part of the viewport chrome
   rather than the panel. */
/* The left rail: the model library, then the studio controls (lighting + view)
   stacked under it, anchored below the wordmark at the stage's left edge -- so
   their left margin equals the panel's right margin (the shell's 40px padding).
   The rail passes the pointer through in the gaps; each block re-enables it. */
.left-rail {
  position: absolute;
  left: 0;
  top: 60px;
  z-index: 3;
  width: 300px;
  max-height: calc(100% - 72px);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 14px;
  pointer-events: none;
}
.left-rail > * { pointer-events: auto; }

/* Studio controls, floated at the TOP-RIGHT of the stage beside the Ring/Soon
   family switch: the lighting row then the view row, right-aligned so the pills
   hug the panel edge. Absolute inside .stage (the positioning context), clear of
   the canvas orbit in the empty top-right corner. */
.stage-tools {
  position: absolute;
  top: 4px;
  right: 0;
  z-index: 4;
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: flex-end;
}

/* Library shelf: same graphite card language as the panel. A two-tab header
   (Library / My design) over one scrolling 2-up grid of square thumbnail
   buttons. Hidden until app.js finds at least one item, so a deploy without a
   library shows nothing rather than an empty box. */
.library {
  flex: 0 0 auto;
  width: 300px;
  max-height: 100%;
  min-height: 0;
  display: flex;
  flex-direction: column;
  padding: 14px 14px 12px;
  background: rgba(17, 17, 17, 0.72);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  backdrop-filter: blur(10px);
}
.library[hidden] { display: none; }

/* Tabs: text labels that switch the grid below, like the Ring/Soon family
   switch. The active tab reads bright, the other dim. */
.lib-tabs { display: flex; gap: 18px; margin: 0 2px 12px; flex: 0 0 auto; }
.lib-tab {
  padding: 0;
  border: 0;
  background: none;
  font-family: inherit;
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  cursor: pointer;
  transition: color 0.16s ease;
}
.lib-tab:hover { color: var(--ink-mid); }
.lib-tab[aria-selected="true"] { color: var(--ink); }

.lib-note {
  margin: 10px 2px 2px;
  font-size: 11px;
  line-height: 1.4;
  color: var(--ink-faint);
  flex: 0 0 auto;
}
.lib-note a { color: var(--ink-mid); text-decoration: underline; cursor: pointer; }
.lib-note a:hover { color: var(--ink); }
/* The readout now sits beside the library; nudge it to share the same top line. */
/* The print readout now floats in the bottom-left corner of the stage, over the
   canvas -- clear of the library/tools column above it. */
.meta.readout-corner {
  position: absolute;
  left: 0;
  bottom: 6px;
  z-index: 3;
  margin-top: 0;
  max-width: 300px;
}
.library-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 8px;
  /* Cap at ~8 cells (four 2-up rows) then scroll; shrink on shorter windows so
     the studio tools and the bottom-left readout keep their room. 132px cells
     in a 300px shelf: 4*132 + 3*8 = 552. */
  flex: 1 1 auto;
  min-height: 0;
  max-height: min(552px, calc(100vh - 420px));
  overflow-y: auto;
  padding-bottom: 4px;
  /* room for the scrollbar so cells don't jump when it appears */
  scrollbar-width: thin;
  scrollbar-color: var(--line-strong) transparent;
}
.library-grid[hidden] { display: none; }
.library-grid::-webkit-scrollbar { width: 8px; }
.library-grid::-webkit-scrollbar-thumb {
  background: var(--line-strong);
  border-radius: 999px;
}
.lib-cell {
  position: relative;
  aspect-ratio: 1 / 1;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: var(--radius-ctl);
  color: var(--ink-faint);
  cursor: pointer;
  overflow: hidden;
  transition: border-color 0.16s ease, background 0.16s ease, color 0.16s ease;
}
.lib-cell:hover { border-color: var(--line-strong); color: var(--ink-mid); }
.lib-cell[aria-pressed="true"] {
  border-color: var(--ink-dim);
  background: #1c1c1c;
}
.lib-cell img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}
/* Thumbnail still rendering: a faint pulse where the image will land. */
.lib-cell.is-loading { color: var(--ink-faint); }
.lib-cell.is-loading::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg,
    transparent 30%, rgba(255,255,255,0.05) 50%, transparent 70%);
  background-size: 220% 100%;
  animation: lib-shimmer 1.1s ease-in-out infinite;
}
@keyframes lib-shimmer { to { background-position: -120% 0; } }
/* The clear/reset cell -- an X that returns to the configured design. */
.lib-cell.lib-clear { color: var(--ink-dim); }
.lib-cell.lib-clear:hover { color: var(--ink); }
.lib-cell .lib-icon { width: 30%; height: 30%; }
.lib-cell .lib-name {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 3px 6px;
  font-size: 10px;
  letter-spacing: 0.02em;
  color: var(--ink-dim);
  background: linear-gradient(to top, rgba(0,0,0,0.55), transparent);
  text-align: left;
  pointer-events: none;
}

.view-tools {
  display: flex;
  gap: 6px;
  z-index: 2;
}
.view-btn {
  padding: 7px 12px;
  font-family: inherit;
  font-size: 11px;
  letter-spacing: 0.01em;
  color: var(--ink-mid);
  background: rgba(17, 17, 17, 0.72);
  border: 1px solid var(--line);
  border-radius: 999px;
  backdrop-filter: blur(8px);
  cursor: pointer;
  transition: color 0.16s ease, border-color 0.16s ease, background 0.16s ease;
}
.view-btn:hover { color: var(--ink); border-color: var(--line-strong, rgba(255,255,255,0.28)); }
.view-btn[aria-pressed="true"] { color: var(--ink); border-color: var(--line-strong, rgba(255,255,255,0.28)); }

/* Measuring stage row (pose + units). Only a product with a stage shows it. */
.stage-tools-row { gap: 10px; align-items: center; }
.stage-tools-row[hidden] { display: none; }
/* On a painted wall the light background would swallow the corner readout's
   dim text: give it the same graphite card as the tool pills. */
body.wall-painted .meta.readout-corner {
  background: rgba(17, 17, 17, 0.78);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 8px 12px;
  backdrop-filter: blur(8px);
}
body.wall-painted .wordmark { text-shadow: 0 1px 10px rgba(0, 0, 0, 0.55); }
.wall-paints { display: inline-flex; align-items: center; gap: 5px; padding-left: 2px; }
.wall-paints[hidden] { display: none; }
.wall-swatch, .wall-picker {
  width: 20px; height: 20px; padding: 0;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.28);
  cursor: pointer;
  flex: none;
}
.wall-swatch[aria-pressed="true"] { box-shadow: 0 0 0 2px var(--accent); }
.wall-picker {
  position: relative; overflow: hidden;
  background: conic-gradient(#e66, #eb5, #9d6, #5bd, #86e, #e6b, #e66);
}
.wall-picker input { position: absolute; inset: -6px; opacity: 0; cursor: pointer; }
/* A segmented control, not a row of separate pills welded together: ONE track
   with the choices inside it, so the row reads as a single control and the gap
   between two groups (where it sits / in what units) is the only seam.
   The chosen segment is a lifted NEUTRAL slab -- grey, not a hue. Colour on this
   toolbar has to mean something, and the only thing that means anything here is
   the print verdict; a warm tint on "mm" was decoration competing with it. */
.seg-pills {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  border-radius: 999px;
  background: rgba(17, 17, 17, 0.72);
  border: 1px solid var(--line);
  backdrop-filter: blur(8px);
}
.seg-pills .view-btn {
  margin: 0;
  padding: 5px 11px;
  border: none;
  border-radius: 999px;
  background: transparent;
  backdrop-filter: none;
}
.seg-pills .view-btn:hover {
  color: var(--ink);
  background: rgba(255, 255, 255, 0.06);
}
.seg-pills .view-btn[aria-pressed="true"] {
  color: var(--ink);
  background: rgba(255, 255, 255, 0.13);
}

/* Live dimension labels, pinned over the canvas at points on the stage. app.js
   (stage.js) moves each one to its projected position after every frame. */
.stage-labels {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 1;
}
.stage-labels[hidden] { display: none; }
.stage-label {
  position: absolute;
  left: 0;
  top: 0;
  margin: 0;
  translate: -50% -50%;
  padding: 3px 7px;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
  white-space: nowrap;
  color: #171513;
  background: rgba(233, 216, 208, 0.9);
  border-radius: 999px;
  will-change: transform;
}
.stage-label.is-bed {
  translate: 0 0;
  color: var(--ink-dim);
  background: transparent;
  font-size: 10px;
}
.stage-label.is-warn { background: rgba(170, 40, 30, 0.9); color: #fff; }

/* Lighting dropdown + brightness, sitting just above the view shortcuts. */
.light-tools {
  display: flex;
  align-items: center;
  gap: 8px;
  z-index: 2;
}
/* Toolbar variant of the shared dropdown (web/dropdown.css): the lighting
   preset floats over the canvas, so it wears the same graphite pill as the view
   buttons beside it instead of the panel's field. */
.select-trigger-tool {
  padding: 6px 11px;
  border-radius: 999px;
  border-color: var(--line);
  background: rgba(17, 17, 17, 0.72);
  color: var(--ink-mid);
  font-size: 11px;
  letter-spacing: 0.01em;
  backdrop-filter: blur(8px);
}
.select-trigger-tool:hover {
  color: var(--ink);
  background: rgba(17, 17, 17, 0.85);
  border-color: var(--line-strong);
}
/* The little label icon at the head of each toolbar -- a bulb for the lighting
   row, a camera for the view row. Non-interactive; it just names the row. */
.tools-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  color: var(--ink-dim);
  flex: none;
}
.tools-icon svg { width: 15px; height: 15px; display: block; }

.light-presets { display: flex; gap: 6px; }
.light-btn {
  padding: 7px 12px;
  font-family: inherit;
  font-size: 11px;
  letter-spacing: 0.01em;
  color: var(--ink-mid);
  background: rgba(17, 17, 17, 0.72);
  border: 1px solid var(--line);
  border-radius: 999px;
  backdrop-filter: blur(8px);
  cursor: pointer;
  transition: color 0.16s ease, border-color 0.16s ease, background 0.16s ease;
}
.light-btn:hover { color: var(--ink); border-color: var(--line-strong, rgba(255,255,255,0.28)); }
.light-btn[aria-pressed="true"] {
  color: var(--bg);
  background: var(--ink);
  border-color: var(--ink);
}
/* Brightness: a compact slider in a pill to match the buttons. */
.light-bright {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 6px 12px 6px 11px;
  background: rgba(17, 17, 17, 0.72);
  border: 1px solid var(--line);
  border-radius: 999px;
  backdrop-filter: blur(8px);
}
.light-bright-icon { font-size: 12px; color: var(--ink-mid); line-height: 1; }
.light-bright input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  width: 74px;
  height: 3px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.24);
  cursor: pointer;
}
.light-bright input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 12px; height: 12px;
  border-radius: 50%;
  background: var(--ink);
  border: none;
  cursor: pointer;
}
.light-bright input[type="range"]::-moz-range-thumb {
  width: 12px; height: 12px;
  border-radius: 50%;
  background: var(--ink);
  border: none;
  cursor: pointer;
}

.busy {
  position: absolute;
  left: 50%;
  top: 4px;                 /* level with the Morcano studio wordmark */
  translate: -50% 0;
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 7px 15px 7px 13px;
  font-size: 12px;
  letter-spacing: 0.02em;
  color: var(--ink-mid);
  background: rgba(17, 17, 17, 0.86);
  border: 1px solid var(--line);
  border-radius: 999px;
  backdrop-filter: blur(8px);
}
/* The pill is display:flex, which beats the UA rule for [hidden] -- so without
   this the "generating" indicator was never actually hidden and its dot pulsed
   the whole time, even at rest. This restores the hide. */
.busy[hidden] { display: none; }

/* Waiting its turn (a build is slow to start, or the server said busy and the
   client is retrying): the pill opens into a small card that says so calmly,
   so a wait reads as demand, not as something broken. */
.busy-text { display: flex; flex-direction: column; gap: 3px; }
.busy-sub { display: none; }
.busy[data-queue] {
  /* Centred over the model rather than up in the header row, where the card
     would sit on the wordmark and the view toolbar. */
  top: 50%;
  translate: -50% -50%;
  z-index: 5;
  background: rgba(17, 17, 17, 0.94);
  align-items: flex-start;
  width: max-content;
  max-width: min(340px, calc(100% - 32px));
  padding: 10px 16px 11px 14px;
  border-radius: 14px;
}
.busy[data-queue] .busy-dot { margin-top: 5px; flex: none; }
.busy[data-queue] .busy-title { color: var(--ink); font-size: 12.5px; }
.busy[data-queue] .busy-sub {
  display: block;
  font-size: 11.5px;
  line-height: 1.45;
  letter-spacing: 0.01em;
  color: var(--ink-mid);
}

.busy-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.92);
  animation: pulse 1.15s ease-in-out infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 0.25; transform: scale(0.75); }
  50%      { opacity: 1;    transform: scale(1); }
}

@media (prefers-reduced-motion: reduce) {
  .busy-dot { animation: none; opacity: 0.9; }
}

.readout {
  flex: none;
  display: flex;
  align-items: center;
  gap: 10px;
  padding-top: 14px;
  font-size: 12.5px;
  letter-spacing: 0.01em;
  color: var(--ink-dim);
  font-variant-numeric: tabular-nums;
}
.readout .sep { color: rgba(255, 255, 255, 0.18); }
.readout .status[data-ok] { color: rgba(150, 230, 180, 0.85); }
.readout .status[data-ok][data-warn] { color: rgba(255, 190, 140, 0.9); }
/* Three states, not two. "not exported" is where a ring sits while the gate is
   still deciding or while a setting is being changed -- neutral, so it reads
   grey. Red is kept for a real failure, so that when it does appear it means
   something rather than being the colour half the session is spent in. */
.readout .status[data-idle] { color: var(--ink-dim); }
.readout .status[data-bad] { color: rgba(255, 150, 150, 0.9); }

/* What the gate measured. Sits under the readout, one chip per check. */
.gate {
  flex: none;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding-top: 10px;
}
.gate:empty { display: none; }

.chip {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  padding: 4px 10px;
  border-radius: 999px;
  border: 1px solid var(--line);
  background: rgba(255, 255, 255, 0.03);
  font-size: 11px;
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;
  cursor: default;
}
.chip .k {
  color: var(--ink-faint);
  text-transform: uppercase;
  font-size: 9.5px;
  letter-spacing: 0.11em;
}
.chip .v { color: var(--ink-mid); }
.chip[data-ok] { border-color: rgba(150, 230, 180, 0.28); }
.chip[data-ok] .v { color: rgba(150, 230, 180, 0.9); }
.chip[data-bad] { border-color: rgba(255, 150, 150, 0.34); }
.chip[data-bad] .v { color: rgba(255, 150, 150, 0.95); }

/* Alerts. A real bordered box with a coloured left edge, so a warning is
   distinguishable at a glance from the facts above it -- the old version was
   loose orange text that merged into the paragraph under the logo. Each message
   is its own item, separated by a hairline, rather than joined with dots. */
.notice {
  flex: none;
  margin: 0;
  width: 100%;
  max-width: 62ch;
  padding: 9px 11px;
  border: 1px solid var(--line);
  border-left: 2px solid rgba(255, 190, 140, 0.75);
  border-radius: 10px;
  background: rgba(255, 255, 255, 0.03);
  font-size: 11.5px;
  line-height: 1.5;
  color: rgba(255, 190, 140, 0.86);
}
.notice .notice-item { margin: 0; }
.notice .notice-item + .notice-item {
  margin-top: 7px;
  padding-top: 7px;
  border-top: 1px solid var(--line);
}
/* The size the piece does print at, offered as one click (a refused size). */
.notice .notice-fix {
  margin-top: 8px;
  padding: 5px 11px;
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.06);
  color: var(--ink);
  font: inherit;
  cursor: pointer;
}
.notice .notice-fix:hover { background: rgba(255, 255, 255, 0.11); }
.notice .notice-fix:focus-visible { outline: 2px solid var(--line-strong); outline-offset: 2px; }
/* A failed request, as opposed to a gate verdict or a warning. */
.notice[data-kind="error"] {
  border-left-color: rgba(255, 150, 150, 0.8);
  color: rgba(255, 150, 150, 0.92);
}
/* A gate refusal is the tool working, not a fault -- so it is neutral, matching
   the grey "Not exported" verdict rather than shouting in red. */
.notice[data-kind="gate"] {
  border-left-color: var(--line-strong);
  color: var(--ink-mid);
}

/* ---------------------------------------------------------------- panel */

/* Three bands: a fixed head (family + status), the scrolling sections, and a
   fixed foot (export). Only the middle band scrolls, which is what keeps the
   status readout and the download buttons on screen at any panel length. */
.panel {
  min-height: 0;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  overflow: hidden;
  /* The panel is a single, distinct surface floating against the canvas -- a
     hairline-bordered card one step above the page, the way a professional
     editor's inspector reads as its own pane rather than loose controls on the
     background. */
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  box-shadow: var(--glass-shadow);
}

.panel-head {
  display: flex;
  align-items: center;
  padding: 14px 18px;
  border-bottom: 1px solid var(--line);
}

/* "Reset all" on the Base section header, opposite the title. block-title's
   flex:1 pushes it to the right edge. Compact ghost with an undo glyph. */
.block-reset {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  flex: none;
  padding: 5px 11px 5px 9px;
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  background: transparent;
  color: var(--ink-dim);
  font-family: inherit;
  font-size: 11.5px;
  letter-spacing: 0.005em;
  cursor: pointer;
  transition: color 0.16s ease, border-color 0.16s ease, background 0.16s ease;
}
.block-reset:hover {
  color: var(--ink);
  border-color: rgba(255, 255, 255, 0.26);
  background: rgba(255, 255, 255, 0.03);
}
.block-reset svg {
  width: 13px;
  height: 13px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.panel-scroll {
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 4px 18px;
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.14) transparent;
}
.panel-scroll::-webkit-scrollbar { width: 6px; }
.panel-scroll::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.12);
  border-radius: 99px;
}

.panel-foot {
  padding-top: 16px;
  border-top: 1px solid var(--line);
  background: var(--bg);
}
.panel-foot .block-body { padding: 0; }

/* Rounded card: solid neutral fill one step above the page, a subtle 1px
   hairline (brighter on top to read as a lit edge), a soft neutral shadow. */
.group {
  position: relative;
  background: var(--card);
  border: 1px solid var(--glass-border);
  border-top-color: var(--glass-border-top);
  border-radius: var(--radius);
  padding: 18px 20px 8px;
  box-shadow: var(--glass-shadow);
}

.group-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 10.5px;
  font-weight: 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 14px;
}

/* ---------------------------------------------------------------- blocks */
/*
   A collapsible panel block. Built on <details>/<summary> rather than a div
   and a click handler, so open/closed state, keyboard operation and the
   screen-reader announcement come from the element instead of being
   reimplemented -- and a closed block genuinely removes its contents from the
   tab order, which a `hidden` class does not.
*/
/* Sections are flat rows separated by hairlines, not floating cards. A card per
   section gave every one of them the same visual weight and a lot of padding;
   dividers put the weight on the titles and let the panel breathe. */
.block {
  position: relative;
  border-top: 1px solid var(--line);
}
.block:first-child { border-top: none; }

.block-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 17px 0;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: -0.005em;
  text-transform: none;
  color: var(--ink);
  cursor: pointer;
  user-select: none;
  list-style: none;                 /* Firefox's default marker */
  transition: color 0.16s ease;
}
.block-head::-webkit-details-marker { display: none; }
/* An off section is quiet: the title greys out so the on ones lead the eye. */
.block.gated:not([data-on]) > .block-head { color: var(--ink-faint); }
.block.gated:not([data-on]) > .block-head:hover { color: var(--ink-dim); }

/* The title fills the header so the chevron (or the switch) is pushed to the
   far edge and every block's control lines up in one column down the panel. */
.block-title { flex: 1 1 auto; }

/* A gated block: the master switch IS the disclosure, so there is no chevron.
   The whole header row is clickable to open/close the block (same as its
   switch), so it carries the pointer and brightens on hover the way any control
   does. Off, the block is this one row; on, the settings sit underneath. */
.block.gated > .block-head.clickable { cursor: pointer; }
.block.gated > .block-head:hover { color: var(--ink-dim); }
.block.gated[data-on] > .block-head { color: var(--ink-mid); }
.block.gated > .block-head .toggle { flex: none; }
/* The header's padding already spaces the row; the body then only needs to
   close the gap under it. */
.block.gated > .block-body { padding-top: 2px; }

.chevron {
  width: 7px;
  height: 7px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform 0.2s ease;
  flex: none;
}
.block[open] > .block-head .chevron {
  transform: rotate(-135deg) translate(-2px, -2px);
}

.block-body { padding: 0 0 22px; }

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

/* Paired fields: System/Size, band width/thickness, inner-mm/calculate. Each
   cell stacks its label over its control so a narrow column still reads. */
.pair {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px 14px;
  width: 100%;
  /* A pair holds two controls (sliders/selects) whose inner rows carry no
     bottom padding, so the pair itself owns the rhythm to the next row -- the
     same 26px every .row uses. Without this the two-up sliders collided with
     the single slider under them. */
  padding-bottom: 26px;
}
.pair:last-child { padding-bottom: 0; }
/* The sizer's pair lives inside a .row that already spaces itself. */
.sizer-pair { padding-bottom: 0; }
.pair-cell { min-width: 0; }
.pair-cell .row {
  grid-template-columns: 1fr auto;
  padding-bottom: 0;
}
.pair-cell .row .select,
.pair-cell .row .text-input,
.pair-cell .row .btn { grid-column: 1 / -1; width: 100%; margin-top: 7px; }

/* The section-title info affordance. */
.info {
  margin-right: auto;
  padding: 0 0 0 7px;
  background: none;
  border: none;
  color: var(--ink-faint);
  font-size: 13px;
  line-height: 1;
  cursor: help;
}
.info:hover { color: var(--ink-mid); }
/* On a touch screen the ⓘ glyph (12 px) and the small view buttons are too small
   to hit. The tap area grows around them; the look stays the same. */
@media (pointer: coarse) {
  .info, .view-btn { position: relative; }
  .info::after, .view-btn::after { content: ""; position: absolute; inset: -9px -6px; }
}
.info-text {
  margin: 0;
  max-width: 30ch;
  font-size: 12px;
  line-height: 1.55;
  color: var(--ink-mid);
}

/* A measurement field's ⓘ shows a schematic above its words: how to hold the
   ruler, with the one dimension it asks for picked out in green. */
.popover.diagram-pop {
  min-width: 210px;
  max-width: min(248px, 88vw);
}
.diagram-fig {
  margin: -2px 0 9px;
  padding: 8px;
  background: rgba(255, 255, 255, 0.02);
  border: 1px solid var(--line);
  border-radius: 8px;
}
.diagram-fig .measure-diagram {
  display: block;
  width: 100%;
  height: auto;
}
.diagram-fig .measure-diagram text {
  font-family: inherit;
  font-weight: 500;
}

/* An ⓘ sitting inline after a control's label, standing in for a line of fine
   print. The label becomes a flex row so the glyph rides beside the text and
   stays vertically centred. */
.row > label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}
.row > label .info,
.sizer-row label .info {
  margin: 0;
  padding: 0;
  font-size: 12px;
}
.info[hidden] { display: none; }
/* A constraint worth noticing (an AMS requirement) tints its ⓘ amber. */
.info[data-warn] { color: rgba(255, 190, 140, 0.9); }
.info[data-warn]:hover { color: rgba(255, 205, 160, 1); }

/* Text input, matching the select's fill and radius. */
.text-input {
  padding: 10px 12px;
  border-radius: var(--radius-ctl);
  border: 1px solid var(--line-strong);
  background: var(--field);
  color: var(--ink);
  font-family: inherit;
  font-size: 13px;
  transition: border-color 0.16s ease, background 0.16s ease;
}
.text-input:hover { background: var(--field-hover); }
.text-input::placeholder { color: var(--ink-faint); }
.text-input:focus-visible { outline: none; border-color: rgba(255,255,255,0.4); }

/* Mirrors the Size-system / Size pair above it: equal halves, so the diameter
   field lines up under the system dropdown. */
.sizer-pair {
  margin-top: 8px;
  grid-template-columns: 1fr 1fr;
  align-items: center;
}
.sizer-pair .pair-cell .text-input { width: 100%; height: 44px; }

/* Custom number stepper, in the panel's own style. The native spinner is hidden
   and replaced by two chevrons stacked at the right edge of the field. */
.stepper-field {
  position: relative;
  width: 100%;
}
.stepper-field .text-input {
  width: 100%;
  padding-right: 30px;                 /* clear the stepper */
}
.stepper-field .text-input::-webkit-outer-spin-button,
.stepper-field .text-input::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}
.stepper-field .text-input[type="number"] { -moz-appearance: textfield; }
.stepper {
  position: absolute;
  right: 6px;
  top: 50%;
  translate: 0 -50%;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.stepper-btn {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 20px;
  height: 15px;
  padding: 0;
  border: none;
  border-radius: 5px;
  background: transparent;
  color: var(--ink-dim);
  cursor: pointer;
  transition: color 0.14s ease, background 0.14s ease;
}
.stepper-btn:hover { color: var(--ink); background: rgba(255, 255, 255, 0.07); }
.stepper-btn svg {
  width: 9px;
  height: 6px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
/* The right half is the Enter hint, not a button -- the field commits on Enter
   or blur. Muted, so it reads as guidance rather than a control. */
.sizer-enter {
  display: flex;
  align-items: center;
  font-size: 12px;
  color: var(--ink-faint);
  letter-spacing: 0.01em;
}

/* The three-way Top selector sits on its own line under the platform dials. */
.top-mode .segmented { grid-column: 1 / -1; margin-top: 8px; }

/* A block that points at another block, rather than duplicating its control. */
.block-link {
  grid-column: 1 / -1;
  justify-self: start;
  margin: 0 0 10px;
  padding: 0;
  background: none;
  border: none;
  font-family: inherit;
  font-size: 11px;
  color: var(--ink-mid);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}
.block-link:hover { color: var(--ink); }

/* Brief highlight when a link jumps you to a block, so it is obvious which
   one answered. */
@keyframes block-flash {
  from { border-color: var(--accent); }
  to   { border-color: var(--line); }
}
.block.flash { animation: block-flash 1.1s ease-out; }

@media (prefers-reduced-motion: reduce) {
  .chevron, .block-head { transition: none; }
  .block.flash { animation: none; }
}

/* A comfortable rhythm: field groups were 14px apart and read as a wall. */
.row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 4px 10px;
  padding-bottom: 26px;
}
.row:last-child { padding-bottom: 0; }

.row label {
  font-size: 13px;
  color: var(--ink-mid);
  letter-spacing: -0.005em;
}

/* The live value, right-aligned against its label. Brighter than before so the
   number reads as the readout it is, not a footnote. */
.row .val {
  font-size: 12px;
  color: #8a8a8a;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.01em;
  text-align: right;
}

.row input[type="range"] {
  grid-column: 1 / -1;
  width: 100%;
  margin: 12px 0 0;
  --pct: 0%;
  appearance: none;
  background: transparent;
  cursor: pointer;
}

/* The track is filled up to the thumb: white to the left, muted to the right,
   the fraction driven by --pct which app.js updates as the value moves. This is
   the small readout that makes a slider read as a slider at a glance. */
.row input[type="range"]::-webkit-slider-runnable-track {
  height: 3px;
  border-radius: 999px;
  background: linear-gradient(to right,
    var(--fill) 0 var(--pct), var(--track) var(--pct) 100%);
}
.row input[type="range"]::-moz-range-track {
  height: 3px;
  border-radius: 999px;
  background: linear-gradient(to right,
    var(--fill) 0 var(--pct), var(--track) var(--pct) 100%);
}

/* A solid pure-white knob: no ring, no border, no outline. */
.row input[type="range"]::-webkit-slider-thumb {
  appearance: none;
  width: 13px;
  height: 13px;
  margin-top: -5px;
  border-radius: 50%;
  background: #fff;
  border: none;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
  transition: transform 0.12s ease;
}
.row input[type="range"]::-moz-range-thumb {
  width: 13px;
  height: 13px;
  border: none;
  border-radius: 50%;
  background: #fff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.35);
}
.row input[type="range"]:hover::-webkit-slider-thumb { transform: scale(1.22); }
.row input[type="range"]:focus-visible { outline: none; }
.row input[type="range"]:focus-visible::-webkit-slider-thumb {
  box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.22);
}

.sub-rows { transition: opacity 0.22s ease; }
.sub-rows[data-off] {
  opacity: 0.32;
  pointer-events: none;
}

/* segmented control: preset switcher, and any enum parameter */
.segmented,
.preset-switch {
  display: flex;
  gap: 4px;
  padding: 4px;
  border-radius: 999px;
  background: rgba(0, 0, 0, 0.25);
  border: 1px solid var(--line);
}

/* The family switch spans the head -- Ring and Soon each take half, edge to
   edge, as the top-level tab bar. */
.preset-switch {
  margin-bottom: 0;
  display: flex;
  flex: 1;
}
/* Both tabs share the width equally -- Soon is a parked beta-toggle that would
   otherwise shrink to its text and leave Ring hogging the bar. */
.preset-switch .seg,
.preset-switch .seg.beta-toggle { flex: 1; padding: 9px 12px; }
/* Holding the generator switcher instead of tabs, the head is no longer a track:
   the switcher is its own card, and the tab bar's dark pill behind it read as a
   stray black box. */
.preset-switch:has(> .gen-switch) {
  padding: 0;
  border: 0;
  border-radius: 0;
  background: none;
}
.row .segmented { grid-column: 1 / -1; margin-top: 8px; }
/* An enum with more options than fit one row (a tray plate's five shapes) wraps
   onto a second row rather than running its labels past the panel's edge; the
   track's corners are the pill's radius, so one row still reads as a pill. */
.segmented { flex-wrap: wrap; border-radius: 20px; }
.segmented .seg { flex: 1 0 auto; white-space: nowrap; }
/* An unavailable family reads as unavailable rather than merely unselected. */
.seg[disabled] { opacity: 0.45; cursor: not-allowed; }
.seg[disabled]:hover { color: var(--ink-dim); }

.seg {
  flex: 1;
  padding: 8px 10px;
  border: none;
  border-radius: 999px;
  background: transparent;
  color: var(--ink-dim);
  font-family: inherit;
  font-size: 12.5px;
  font-weight: 400;
  letter-spacing: 0.005em;
  text-transform: capitalize;
  cursor: pointer;
  transition: background 0.16s ease, color 0.16s ease;
}
.seg:hover { color: var(--ink); }
.seg[aria-pressed="true"] {
  background: rgba(255, 255, 255, 0.95);
  color: #080808;
  font-weight: 500;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
}

/* profile picker: a grid of icon chips, one per profile. Selected chip carries
   a white outline, a subtle fill and a white icon; unselected chips are a muted
   grey stroke on the card. Three columns, matching the mockup. */
/* One row of circular icon buttons, the selected one ringed in white. The name
   moved into the title/aria-label: at this size the icon is the affordance and
   five stacked captions were most of the row's height. */
.profile-grid {
  grid-column: 1 / -1;
  margin-top: 12px;
  display: flex;
  gap: 6px;
  /* Even across the full width so the row is balanced rather than clustered to
     the left (which is what capped, left-packed chips looked like). */
  justify-content: space-between;
}
/* No chip, no ring around the glyph -- just the profile's own shape, big enough
   to read. Selection is carried by weight and colour: the chosen profile is
   solid white, the rest a muted grey that lifts on hover. */
.profile-chip {
  flex: 0 1 auto;                    /* a longer row (7 shapes) shrinks to fit */
  min-width: 36px;
  width: 48px;
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4px;
  border: none;
  border-radius: 12px;
  background: none;
  color: var(--ink-faint);          /* drives the icon stroke via currentColor */
  font-family: inherit;
  cursor: pointer;
  transition: color 0.14s ease, background 0.14s ease;
}
.profile-chip svg {
  width: 100%;
  height: 100%;
  max-width: 38px;
  max-height: 38px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linejoin: round;
}
.profile-chip-label { display: none; }

/* A pattern library: labelled swatches in a wrapping grid. The swatch ground is
   a tint of the chip colour and the pattern the colour itself, so the chosen
   one turns white like every other chip. */
.pattern-grid {
  grid-column: 1 / -1;
  margin-top: 12px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(64px, 1fr));
  gap: 8px;
}
.pattern-chip {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 6px 4px;
  border: none;
  border-radius: 12px;
  background: none;
  color: var(--ink-faint);
  font-family: inherit;
  cursor: pointer;
  transition: color 0.14s ease, background 0.14s ease;
}
.pattern-chip svg { width: 100%; max-width: 52px; aspect-ratio: 1; }
.pattern-chip .pt-ground { fill: currentColor; opacity: 0.22; }
.pattern-chip .pt-ink { fill: currentColor; }
.pattern-chip .profile-chip-label { display: block; font-size: 11px; line-height: 1.2; }
.pattern-chip:hover { color: var(--ink-mid); background: rgba(255, 255, 255, 0.03); }
.pattern-chip[aria-pressed="true"] { color: #fff; background: rgba(255, 255, 255, 0.08); }
.pattern-chip:focus-visible { outline: none; color: var(--ink); background: rgba(255, 255, 255, 0.08); }
.pattern-chip:disabled { opacity: 0.28; cursor: not-allowed; }

/* The plate's faceplate shapes: filled silhouettes in a wrapping grid. Same
   selection language as the profile chips -- muted grey, white when chosen --
   but filled rather than stroked, since a shape reads as its fill. */
.shape-grid {
  grid-column: 1 / -1;
  margin-top: 12px;
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 8px;
}
.shape-chip {
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 7px;
  border: none;
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.03);
  color: var(--ink-faint);          /* drives the fill via currentColor */
  cursor: pointer;
  transition: color 0.14s ease, background 0.14s ease;
}
.shape-chip svg { width: 100%; height: 100%; fill: currentColor; stroke: none; }
.shape-chip:hover { color: var(--ink-mid); background: rgba(255, 255, 255, 0.06); }
.shape-chip[aria-pressed="true"] {
  color: #fff;
  background: rgba(255, 255, 255, 0.10);
}
.shape-chip:focus-visible {
  outline: none;
  color: var(--ink);
  background: rgba(255, 255, 255, 0.08);
}
/* A shape not available at the current section count (deep lobes past one gang). */
.shape-chip:disabled {
  opacity: 0.28;
  cursor: not-allowed;
  background: none;
}
.shape-chip:disabled:hover { background: none; color: inherit; }
.profile-chip:hover {
  color: var(--ink-mid);
  background: rgba(255, 255, 255, 0.03);
}
.profile-chip[aria-pressed="true"] { color: #fff; }
.profile-chip:focus-visible {
  outline: none;
  color: var(--ink);
  background: rgba(255, 255, 255, 0.05);
}

/* dropdown: naming a part, where a segmented control would keep subdividing */
.select {
  grid-column: 1 / -1;
  margin-top: 8px;
  width: 100%;
  padding: 11px 34px 11px 13px;
  border-radius: var(--radius-ctl);
  border: 1px solid var(--line-strong);
  background-color: var(--field);
  color: var(--ink);
  font-family: inherit;
  font-size: 13px;
  letter-spacing: -0.005em;
  text-transform: capitalize;
  cursor: pointer;
  appearance: none;
  /* The chevron, drawn rather than fetched -- the page loads no remote assets. */
  background-image:
    linear-gradient(45deg, transparent 50%, var(--ink-dim) 50%),
    linear-gradient(135deg, var(--ink-dim) 50%, transparent 50%);
  background-position: right 16px center, right 11px center;
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  transition: border-color 0.16s ease, background-color 0.16s ease;
}
.select:hover { border-color: rgba(255, 255, 255, 0.26); background-color: var(--field-hover); }
.select:focus-visible {
  outline: none;
  border-color: rgba(255, 255, 255, 0.4);
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
}
/* The popup list is drawn by the OS, so it needs its own colours to stay dark. */
.select option {
  background: #151515;
  color: var(--ink);
}

/* The custom dropdown (trigger, option list, popover shell) lives in
   web/dropdown.css -- the studio and the order desk link the same file so a
   dropdown looks the same wherever it is drawn. */



/* beta tray: kept, not foregrounded */
.beta-tray {
  display: none;
  flex-wrap: wrap;
  gap: 4px;
  margin-top: 8px;
  padding-top: 8px;
  border-top: 1px solid var(--line);
}
.beta-tray[data-open] { display: flex; }
.beta-tray .seg { flex: 0 0 auto; padding: 6px 12px; }
.seg.beta-toggle { flex: 0 0 auto; color: var(--ink-faint); }
/* Parked Beta tab: visibly present but inert, so it reads as "coming soon"
   rather than broken. */
.seg.beta-toggle:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
.seg.beta-toggle:disabled:hover { color: var(--ink-faint); }

/* file upload */
.file-row {
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  gap: 4px;
  margin-top: 8px;
}
.file-buttons {
  display: flex;
  gap: 6px;
  align-items: center;
  flex-wrap: wrap;
}
.custom-file-btn {
  width: max-content;
  padding: 6px 14px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.2);
  background: rgba(255, 255, 255, 0.08);
  color: var(--ink);
  font-family: inherit;
  font-size: 12.5px;
  cursor: pointer;
  transition: background 0.15s ease;
}
.custom-file-btn:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.15);
}
.custom-file-btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}
/* Remove is the secondary action: quieter than Browse, and it hides itself
   when there is no file to clear. */
.remove-file-btn {
  background: transparent;
  color: var(--ink-mid);
}
.remove-file-btn:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.08);
  color: var(--ink);
}
.remove-file-btn[hidden] { display: none; }
.file-status {
  display: flex;
  align-items: center;
  gap: 7px;
  font-size: 11px;
  color: var(--ink-faint);
  padding-left: 2px;
  max-width: 100%;
}
/* The uploaded file's own name -- clipped with an ellipsis if long, full name on
   hover -- so the wearer always sees which file is on the ring. */
.file-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  min-width: 0;
}
/* The format tag beside it (SVG / STL / 3MF): neutral, so it reads as a kind
   marker rather than part of the green "loaded" confirmation. */
.file-format {
  flex: none;
  padding: 1px 6px;
  border-radius: 5px;
  border: 1px solid var(--line-strong);
  background: rgba(255, 255, 255, 0.05);
  color: var(--ink-mid);
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0.06em;
}
/* Uploading: a muted line with a small spinner so the round trip is visible. */
.file-status[data-state="uploading"] { color: var(--ink-mid); }
.file-status[data-state="uploading"]::before {
  content: "";
  width: 11px;
  height: 11px;
  border-radius: 50%;
  border: 1.5px solid var(--line-strong);
  border-top-color: var(--ink);
  animation: file-spin 0.7s linear infinite;
  flex: none;
}
/* Confirmed: green, matching the print-ready readout. */
.file-status[data-state="ok"] { color: var(--accent); }
/* Failed: a clear red, the one place the panel uses it. */
.file-status[data-state="error"] { color: #ff6a6b; }

@keyframes file-spin { to { transform: rotate(360deg); } }

/* phrase + live character counter */
.text-row {
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
}
.phrase {
  flex: 1 1 auto;
  min-width: 0;
  padding: 9px 12px;
  border-radius: 12px;
  border: 1px solid var(--line-strong);
  background: rgba(255, 255, 255, 0.04);
  color: var(--ink);
  font-family: inherit;
  font-size: 12.5px;
  letter-spacing: 0.01em;
}
.phrase:focus-visible {
  outline: none;
  border-color: rgba(255, 255, 255, 0.4);
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
}
.counter {
  flex: none;
  font-size: 11px;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}
.counter[data-over] { color: rgba(255, 150, 150, 0.95); }

/* filament swatches */
.colour-row { align-items: start; }
.swatches {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: 7px;
}
.swatch {
  width: 19px;
  height: 19px;
  border-radius: 5px;
  border: 1px solid rgba(255, 255, 255, 0.18);
  padding: 0;
  cursor: pointer;
  transition: transform 0.12s ease, box-shadow 0.12s ease;
}
.swatch:hover { transform: scale(1.14); }
.swatch[aria-pressed="true"] {
  box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.9);
}

/* Current-colour chip: what the inline swatch strip collapsed into. Shows the
   part's colour and its filament name, and opens the full grid on click. */
.swatch-current {
  grid-column: 2;
  justify-self: end;
  display: inline-flex;
  align-items: center;
  gap: 9px;
  min-width: 128px;
  padding: 8px 12px 8px 11px;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-ctl);
  background: var(--field);
  color: var(--ink);
  font-family: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
}
.swatch-current:hover { border-color: rgba(255,255,255,0.28); background: var(--field-hover); }
.swatch-name { flex: 1 1 auto; text-align: left; }
.swatch-dot {
  flex: none;
  width: 18px; height: 18px; border-radius: 50%;
  border: 1px solid rgba(255,255,255,0.28);
}
/* The chevron that says the chip opens a picker -- drawn, like the select's. */
.swatch-caret {
  flex: none;
  width: 8px; height: 8px;
  margin-left: 2px;
  border-right: 1.5px solid var(--ink-dim);
  border-bottom: 1.5px solid var(--ink-dim);
  transform: translateY(-2px) rotate(45deg);
}
.swatch-current:hover .swatch-caret { border-color: var(--ink); }

/* The .popover shell is in web/dropdown.css; the swatch grid below rides on it. */
.swatch-grid {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: 6px;
}

/* The filament palette popover: laid out nine to a row like Bambu's own swatch
   card, on the panel's dark surface. Wider than the default popover so the full
   row fits. */
.popover.swatch-pop {
  min-width: 320px;
  max-width: min(340px, 90vw);
  padding: 14px;
}
.swatch-pop-title {
  margin: 0 0 11px;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.02em;
  color: var(--ink-mid);
}
.swatch-pop .swatch-grid {
  grid-template-columns: repeat(9, 1fr);
  gap: 8px;
}
.swatch-pop .swatch {
  width: auto;
  aspect-ratio: 1;
  height: auto;
  border-radius: 50%;
}

/* Ring sizer */
.sizer-link {
  grid-column: 1 / -1;
  justify-self: start;
  margin-top: 6px;
  padding: 0;
  border: none;
  background: none;
  color: var(--ink-mid);
  font-family: inherit;
  font-size: 11.5px;
  text-decoration: underline;
  text-underline-offset: 2px;
  cursor: pointer;
}
.sizer-link:hover { color: var(--ink); }
.sizer-hint { margin: 0 0 9px; font-size: 11.5px; line-height: 1.45; color: var(--ink-mid); }
.sizer-field { display: flex; align-items: center; gap: 7px; }
.sizer-input {
  width: 100px; padding: 7px 9px; border-radius: 8px;
  border: 1px solid var(--line); background: var(--field, #1c1c1c);
  color: var(--ink); font-family: inherit; font-size: 13px; outline: none;
}
.sizer-input:focus { border-color: rgba(255,255,255,0.35); }
.sizer-unit { font-size: 12px; color: var(--ink-mid); }
.sizer-result { margin: 9px 0 10px; font-size: 12px; color: var(--ink-mid); min-height: 1.2em; }
.sizer-result strong { color: var(--ink); }
.sizer-apply { width: 100%; height: 34px; font-size: 12px; }

.colour-note {
  margin: 4px 0 10px;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--ink-dim);
}
.colour-note[data-warn] { color: rgba(255, 190, 140, 0.85); }

/* A standing caption under a control, declared by the schema. Same voice as
   .colour-note but attached to one parameter rather than to a whole block. */
.param-note {
  grid-column: 1 / -1;
  margin: -2px 0 8px;
  font-size: 11px;
  line-height: 1.5;
  color: var(--ink-faint);
}

/* a bool sitting on its own row rather than in a group head */
.row > .toggle { justify-self: end; }

/* toggle */
.toggle {
  width: 34px;
  height: 19px;
  padding: 2px;
  border-radius: 999px;
  border: 1px solid var(--line-strong);
  background: rgba(255, 255, 255, 0.04);
  cursor: pointer;
  transition: background 0.18s ease, border-color 0.18s ease;
}
.toggle .knob {
  display: block;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.45);
  transition: transform 0.18s ease, background 0.18s ease;
}
.toggle[aria-checked="true"] {
  background: rgba(255, 255, 255, 0.92);
  border-color: transparent;
}
.toggle[aria-checked="true"] .knob {
  transform: translateX(15px);
  background: #0a0a0a;
}

/* actions */
/* The EXPORT block shows only the two download buttons now, so the Randomize
   action is hidden. The element stays in the DOM because app.js binds to it on
   load; this hides it without removing it. */
.actions {
  display: none;
  grid-template-columns: 1fr;
  gap: 10px;
  padding-top: 2px;
}

/* Share: a quiet ghost button under the downloads. (Reset moved to the head.) */
.panel-actions {
  display: grid;
  grid-template-columns: 1fr;
  gap: 8px;
  margin-top: 10px;
}
.panel-actions .btn { height: 36px; font-size: 12px; }
.panel-actions .btn.copied { color: var(--ink); border-color: rgba(120, 200, 140, 0.6); }

.btn {
  height: 42px;
  /* Room either side of the label, so a button is as wide as what it says. */
  padding: 0 18px;
  border-radius: 999px;
  font-family: inherit;
  font-size: 13px;
  font-weight: 400;
  letter-spacing: -0.005em;
  cursor: pointer;
  transition: background 0.16s ease, border-color 0.16s ease, opacity 0.16s ease;
}

.btn.primary {
  background: #fff;
  color: #060606;
  border: 1px solid #fff;
}
.btn.primary:hover { background: rgba(255, 255, 255, 0.88); }

.btn.ghost {
  background: transparent;
  color: var(--ink-mid);
  border: 1px solid var(--line-strong);
}
.btn.ghost:hover {
  color: var(--ink);
  border-color: rgba(255, 255, 255, 0.26);
}

/* The primary download. Filled, so the hierarchy against the outlined STL is
   carried by weight and not only by colour. */
.btn.accent {
  background: var(--btn-face);
  color: var(--btn-face-ink);
  border: 1px solid var(--btn-face-line);
  font-weight: 500;
}
.btn.accent:hover {
  background: var(--btn-face-hover);
}

.btn.outline {
  background: transparent;
  color: var(--ink-mid);
  border: 1px solid var(--line-strong);
}
.btn.outline:hover {
  color: var(--ink);
  border-color: rgba(255, 255, 255, 0.26);
}

/* The STL download is the secondary export, but it still has to read as a real
   button next to the green 3MF -- as an outline it disappeared beside the ghost
   "Copy share link" below it. A light-grey fill gives it its own weight. */
.downloads .btn.outline {
  background: transparent;
  color: var(--ink);
  border-color: var(--line-strong);
}
.downloads .btn.outline:hover {
  background: rgba(255, 255, 255, 0.07);
  border-color: rgba(255, 255, 255, 0.28);
}
/* Disabled (flat/separate export modes) still has to read as off, not as a
   dimmed light button -- drop it back to a faint outline. */
.downloads .btn.outline[disabled] {
  background: transparent;
  color: var(--ink-dim);
  border: 1px solid var(--line-strong);
}

.btn[disabled] { opacity: 0.4; cursor: default; }

.downloads {
  display: grid;
  gap: 8px;
  padding: 10px 0 8px;
}
.downloads .btn { width: 100%; }
.row:first-child {
  margin-top: 0;
}
.disabled-row {
  opacity: 0.5;
}
.disabled-row input[type="range"]::-webkit-slider-thumb {
  cursor: not-allowed;
}
.download-note {
  margin: 0;
  font-size: 11px;
  line-height: 1.5;
  color: var(--ink-faint);
  text-align: center;
}
.download-note + .download-note { margin-top: -2px; }

/* ---------------------------------------------------------------- narrow */

@media (max-width: 900px) {
  body { overflow: auto; }
  .shell {
    grid-template-columns: 1fr;
    height: auto;
    min-height: 100%;
    gap: 26px;
    padding: 26px 20px 32px;
  }
  /* The desktop overlay does not apply when stacked: the logo, library and meta
     return to normal flow above a fixed-height viewport. */
  .stage { position: static; }
  /* The account chip is fixed to the top-right corner. Stacked, the wordmark
     runs the full width and disappeared under it (BETA was clipped), so keep
     that corner reserved and cap a long email instead of letting it grow. */
  .titles { position: static; width: auto; padding-right: 168px; }
  .auth-chip {
    max-width: 44vw;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }
  .left-rail {
    position: static;
    width: auto;
    max-height: none;
    margin-top: 18px;
    flex-direction: column;
    align-items: stretch;
    gap: 18px;
  }
  .library { width: auto; max-height: none; }
  .library-grid { max-height: none; }
  /* Stacked, the top-right float returns to normal flow above the viewport. */
  .stage-tools {
    position: static;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    gap: 8px;
  }
  .meta { margin-top: 18px; }
  .meta.readout-corner {
    position: static;
    max-width: none;
    margin-top: 18px;
  }
  .viewport { position: relative; inset: auto; height: 54vh; margin-top: 18px; }
  .panel { overflow: visible; }
}

/* Phones: tighten the frame and give the viewport a touch more room. */
@media (max-width: 560px) {
  .shell { gap: 16px; padding: 16px 14px 28px; }
  .viewport { height: 48vh; }
  .wordmark { font-size: 22px; }
  .swatch-grid { grid-template-columns: repeat(6, 1fr); }
  .panel-actions { grid-template-columns: 1fr; }
}

/* Touch devices: bump the small hit targets to something a fingertip can hit
   reliably, independent of screen width (a tablet is wide but still touched). */
@media (pointer: coarse) {
  .swatch { width: 30px; height: 30px; border-radius: 7px; }
  .swatch-grid { gap: 8px; }
  .seg { padding: 10px 12px; }
  .profile-chip { min-height: 60px; }
  .view-btn { padding: 10px 14px; }
  /* The library tabs are bare text labels -- ~13px tall, far too small to tap.
     Pad them out (the row keeps its look; only the hit area grows). */
  .lib-tab { padding: 11px 2px; }
  .lib-tabs { margin-top: -8px; }
  .swatch-current { padding: 8px 13px 8px 8px; }
  input[type="range"] { height: 28px; }
  input[type="range"]::-webkit-slider-thumb { width: 22px; height: 22px; }
  input[type="range"]::-moz-range-thumb { width: 22px; height: 22px; }
}

/* --- Account widget (auth.js) ------------------------------------------- */
.auth-widget {
  position: fixed;
  top: 14px;
  right: 18px;
  z-index: 300;
}
.auth-chip {
  font: inherit;
  font-size: 12.5px;
  color: var(--ink-mid);
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  padding: 7px 14px;
  cursor: pointer;
  backdrop-filter: blur(6px);
}
.auth-chip:hover { color: var(--ink); border-color: rgba(255, 255, 255, 0.28); }
.auth-menu {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  min-width: 120px;
  background: #191919;
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 5px;
  box-shadow: 0 18px 44px -16px rgba(0, 0, 0, 0.7);
}
.auth-menu-item {
  display: block;
  width: 100%;
  text-align: left;
  font: inherit;
  font-size: 13px;
  color: var(--ink-mid);
  background: none;
  border: none;
  border-radius: 7px;
  padding: 8px 10px;
  cursor: pointer;
}
.auth-menu-item:hover { background: rgba(255, 255, 255, 0.06); color: var(--ink); }

.auth-overlay {
  position: fixed;
  inset: 0;
  z-index: 400;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.55);
  backdrop-filter: blur(3px);
}
.auth-card {
  position: relative;
  width: min(360px, 92vw);
  background: #171717;
  border: 1px solid var(--line-strong);
  border-radius: 16px;
  padding: 26px 24px 22px;
  box-shadow: 0 30px 80px -20px rgba(0, 0, 0, 0.8);
}
.auth-close {
  position: absolute;
  top: 12px;
  right: 14px;
  font-size: 22px;
  line-height: 1;
  color: var(--ink-faint);
  background: none;
  border: none;
  cursor: pointer;
}
.auth-close:hover { color: var(--ink); }
.auth-tabs {
  display: flex;
  gap: 6px;
  margin-bottom: 18px;
}
.auth-tab {
  font: inherit;
  font-size: 13px;
  color: var(--ink-faint);
  background: none;
  border: none;
  border-bottom: 2px solid transparent;
  padding: 4px 2px 8px;
  cursor: pointer;
}
.auth-tab[aria-pressed="true"] { color: var(--ink); border-bottom-color: var(--accent); }
.auth-form { display: flex; flex-direction: column; gap: 10px; }
.auth-input {
  font: inherit;
  font-size: 14px;
  color: var(--ink);
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--line-strong);
  border-radius: 10px;
  padding: 11px 13px;
  outline: none;
}
.auth-input:focus { border-color: rgba(255, 255, 255, 0.3); }
.auth-error {
  margin: 0;
  font-size: 12.5px;
  color: rgba(255, 150, 150, 0.95);
}
.auth-submit {
  margin-top: 4px;
  font: inherit;
  font-size: 14px;
  font-weight: 500;
  color: var(--btn-face-ink);
  background: var(--btn-face);
  border: 1px solid var(--btn-face-line);
  border-radius: 10px;
  padding: 12px;
  cursor: pointer;
}
.auth-submit:hover { background: var(--btn-face-hover); }
.auth-submit:disabled { opacity: 0.6; cursor: default; }
/* Secondary action in the save dialog: "Save as a copy" next to the green
   primary. Reads as the quieter of the two. */
.auth-submit-alt {
  margin-top: 8px;
  color: var(--ink-mid);
  background: transparent;
  border: 1px solid var(--line-strong);
}
.auth-submit-alt:hover { color: var(--ink); background: rgba(255, 255, 255, 0.05); }
.auth-hint {
  margin: 14px 0 0;
  font-size: 11.5px;
  line-height: 1.5;
  color: var(--ink-faint);
}
.auth-menu-item.auth-menu-accent { color: var(--accent); }
.auth-menu-item.auth-menu-accent:hover {
  color: var(--accent-hover);
  background: var(--accent-soft);
}

/* --- My designs gallery --------------------------------------------------- */
.auth-card-wide { width: min(720px, 94vw); }
.auth-title {
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
  margin: 0 0 16px;
}
/* Toolbar above the grid: search by client/name + a status filter. */
.design-toolbar { display: flex; gap: 8px; margin: 0 0 12px; }
.design-search {
  flex: 1;
  font: inherit;
  font-size: 12.5px;
  padding: 8px 12px;
  border-radius: 8px;
  border: 1px solid var(--line-strong);
  background: var(--field, #1c1c1c);
  color: var(--ink);
  outline: none;
}
.design-search::placeholder { color: var(--ink-faint); }
.design-search:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.design-filter {
  font: inherit;
  font-size: 12.5px;
  padding: 8px 10px;
  border-radius: 8px;
  border: 1px solid var(--line-strong);
  background: var(--field, #1c1c1c);
  color: var(--ink);
  cursor: pointer;
}
/* Both are kept only for their value and change event; the themed trigger
   (web/dropdown.js) is what is actually on screen. */
.design-filter-trigger { flex: none; align-self: stretch; padding: 4px 12px; }
.design-status-trigger { align-self: flex-start; }

.design-grid {
  display: grid;
  /* Fixed two-up so four saved designs read as a 2x2 grid, all visible at once,
     rather than auto-fill widening to two tall cards that scroll. */
  grid-template-columns: repeat(2, 1fr);
  gap: 12px;
  max-height: min(80vh, 700px);
  overflow-y: auto;
  padding-right: 4px;
}
.design-card {
  border: 1px solid var(--line-strong);
  border-radius: 12px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.02);
  display: flex;
  flex-direction: column;
}
.design-thumb {
  position: relative;
  /* Fixed height (not an aspect ratio that scales with the wider card) keeps each
     card short enough that two rows -- four designs -- fit without scrolling. The
     saved thumbnail is a centred square, so a cover-crop to this box stays centred. */
  height: 168px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #101010;
  color: var(--ink-faint);
  font-size: 32px;
}
.design-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Which generator made it -- a small corner tag on the preview. */
.design-badge {
  position: absolute;
  top: 6px; left: 6px;
  font-size: 10px;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  padding: 2px 6px;
  border-radius: 5px;
  background: rgba(0, 0, 0, 0.55);
  color: var(--ink-mid);
  backdrop-filter: blur(2px);
}
.design-body { display: flex; flex-direction: column; gap: 6px; padding: 8px 8px 9px; }
/* Name and note are edited in place; borderless until focused. */
.design-name-input,
.design-note-input {
  font: inherit;
  width: 100%;
  border: 1px solid transparent;
  border-radius: 6px;
  background: none;
  padding: 3px 5px;
  outline: none;
}
.design-name-input { font-size: 12.5px; color: var(--ink); font-weight: 500; }
.design-note-input { font-size: 11.5px; color: var(--ink-mid); }
.design-note-input::placeholder { color: var(--ink-faint); }
.design-name-input:hover,
.design-note-input:hover { border-color: var(--line); }
.design-name-input:focus,
.design-note-input:focus { border-color: var(--accent); background: var(--field, #1c1c1c); }
.design-spec {
  font-size: 10.5px;
  color: var(--ink-faint);
  padding: 0 5px;
  line-height: 1.4;
}
.design-status {
  font: inherit;
  font-size: 11px;
  padding: 4px 8px;
  border-radius: 6px;
  border: 1px solid var(--line-strong);
  background: var(--field, #1c1c1c);
  color: var(--ink-mid);
  cursor: pointer;
  align-self: flex-start;
}

/* The order stage tints the card's left edge so a shelf reads at a glance. */
.design-card[data-status] { border-left-width: 3px; }
.design-card[data-status="new"] { border-left-color: var(--line-strong); }
.design-card[data-status="to_print"] { border-left-color: #d8a531; }
.design-card[data-status="printed"] { border-left-color: #3a8fd8; }
.design-card[data-status="delivered"] { border-left-color: var(--accent); }
.design-actions { display: flex; gap: 6px; margin-top: 2px; }
.design-btn {
  font: inherit;
  font-size: 11.5px;
  flex: 1;
  padding: 5px 0;
  border-radius: 7px;
  border: 1px solid var(--line-strong);
  background: none;
  color: var(--ink-faint);
  cursor: pointer;
}
.design-btn:hover { color: var(--ink); background: rgba(255, 255, 255, 0.05); }
.design-del:hover { color: #ff6b6b; border-color: rgba(255, 107, 107, 0.4); }

/* Field caption in the Save dialog. */
.auth-field-label {
  font-size: 11px;
  color: var(--ink-faint);
  margin: 2px 0 -4px;
}

/* "A client sent you this design" card. A compact floating pill at top-centre,
   clear of the logo (top-left), the account chip (top-right) and the panel. */
.share-bar {
  position: fixed;
  top: 14px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 60;
  max-width: min(520px, calc(100vw - 32px));
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 9px 10px 9px 16px;
  border-radius: 12px;
  background: rgba(30, 27, 25, 0.92);
  border: 1px solid var(--accent-line);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  backdrop-filter: blur(8px);
}
.share-bar-text { font-size: 12.5px; color: var(--ink); flex: 1; }
.share-bar-save { padding: 7px 14px; font-size: 12.5px; white-space: nowrap; }
.share-bar-close {
  font: inherit;
  font-size: 20px;
  line-height: 1;
  background: none;
  border: none;
  color: var(--ink-faint);
  cursor: pointer;
  padding: 0 4px;
}
.share-bar-close:hover { color: var(--ink); }

/* Client → maker handoff row under the panel actions. */
.handoff { margin-top: 10px; }
.handoff-input {
  width: 100%;
  font: inherit;
  font-size: 12.5px;
  padding: 8px 12px;
  border-radius: 8px;
  border: 1px solid var(--line-strong);
  background: var(--field, #1c1c1c);
  color: var(--ink);
  outline: none;
}
.handoff-input::placeholder { color: var(--ink-faint); }
.handoff-input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
/* The free-tier line under the download buttons: what this month's allowance has
   left for the generator on screen. Styled as a note rather than an offer -- it
   states a fact, and the buttons above it stay the loud thing on the block. */
.free-quota {
  margin: 10px 2px 0;
  font-size: 11px;
  line-height: 1.45;
  color: var(--ink-faint);
}

/* What the 3MF and the STL each hold, under the download buttons. */
.export-hint {
  margin: 10px 2px 0;
  font-size: 11px;
  line-height: 1.5;
  color: var(--ink-faint);
}
.export-hint b { color: var(--ink-dim, inherit); font-weight: 600; }

.handoff-hint {
  margin: 6px 2px 0;
  font-size: 11px;
  line-height: 1.45;
  color: var(--ink-faint);
}

/* --- Account settings ----------------------------------------------------- */
.settings-section {
  border-top: 1px solid var(--line-strong);
  padding: 16px 0 4px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.settings-section:first-of-type { border-top: none; padding-top: 4px; }
.settings-title {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.settings-current { font-size: 13px; color: var(--ink); margin: 0; }
.settings-btn { margin-top: 2px; align-self: flex-start; padding: 8px 16px; }
.settings-msg { margin: 2px 0 0; }
.settings-msg.settings-ok { color: var(--accent); }
.settings-danger {
  color: #ff6b6b;
  border: 1px solid rgba(255, 107, 107, 0.4);
  background: none;
}
.settings-danger:hover { background: rgba(255, 107, 107, 0.12); }

/* "Forgot password?" link inside the sign-in modal. */
.auth-link {
  align-self: flex-start;
  margin: -6px 0 2px;
  padding: 0;
  font: inherit;
  font-size: 12px;
  color: var(--ink-faint);
  background: none;
  border: none;
  cursor: pointer;
  text-decoration: underline;
}
.auth-link:hover { color: var(--ink); }

/* Clickwrap legal line under the sign-in / register form. */
.auth-legal {
  margin: 12px 0 0;
  font-size: 11px;
  line-height: 1.5;
  color: var(--ink-faint);
  text-align: center;
}
.auth-legal a { color: var(--ink-soft, #b9bcbb); text-decoration: underline; }
.auth-legal a:hover { color: var(--ink); }

/* "Continue with Google" and its "or" divider in the sign-in modal. */
.auth-oauth { margin-bottom: 14px; }
.auth-google {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  font: inherit;
  font-size: 13px;
  font-weight: 500;
  padding: 10px 12px;
  border-radius: 9px;
  border: 1px solid var(--line-strong);
  background: #fff;
  color: #1f1f1f;
  cursor: pointer;
}
.auth-google:hover { background: #f2f2f2; }
.auth-google svg { flex: none; }
.auth-divider {
  display: flex;
  align-items: center;
  text-align: center;
  color: var(--ink-faint);
  font-size: 11px;
  margin: 12px 0 2px;
}
.auth-divider::before, .auth-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line-strong);
}
.auth-divider span { padding: 0 10px; }


/* ---------------------------------------------------------------- client mode
   The studio opened from a printer owner's client link (client-mode.js): a
   banner naming the shop, a "Send to <shop>" card in the export slot, and the
   request form. Downloads, the account widget and the design shelf are hidden -
   a client orders, they do not download. */
body.client-mode #exportBlock,
body.client-mode .auth-widget,
body.client-mode #authWidget,
body.client-mode .share-bar,
body.client-mode #library,
body.client-mode .panel-dock { display: none !important; }
/* The print shop's side of a build - the printability checks, the filament
   estimate and its material, the cost card - is the owner's, not the client's. */
body.client-mode #gate,
body.client-mode #factFilamentRow,
body.client-mode #printPlanBtn { display: none !important; }

.cm-banner {
  /* Sits where the design shelf is on the normal studio (hidden for a client),
     under the wordmark and clear of the view tools on the right. */
  position: fixed; top: 100px; left: 40px; z-index: 60;
  display: flex; align-items: center; gap: 10px; max-width: min(560px, calc(100vw - 32px));
  padding: 8px 8px 8px 14px; border-radius: 999px;
  background: rgba(30, 27, 25, 0.92); border: 1px solid var(--accent-line);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45); backdrop-filter: blur(8px);
  font-size: 13px; color: var(--ink-mid);
}
.cm-banner b { color: var(--ink); font-weight: 600; }
.cm-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent);
  box-shadow: 0 0 0 4px var(--accent-soft); flex: none; }
.cm-banner-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.cm-more { border: 1px solid var(--line-strong); background: transparent; color: var(--ink);
  border-radius: 999px; height: 26px; padding: 0 10px; font: inherit; font-size: 12px; cursor: pointer; }
.cm-welcome { position: absolute; top: calc(100% + 8px); left: 0; right: 0; padding: 12px 14px;
  border-radius: 12px; background: var(--card); border: 1px solid var(--glass-border-top);
  box-shadow: var(--glass-shadow); color: var(--ink); white-space: pre-wrap; line-height: 1.5; }

/* The one call to action for a client: pinned to the bottom of the panel so it
   stays in reach however far the settings scroll. */
/* The one call to action for a client: the panel's own bottom row, so it is
   fixed under the controls however far they scroll. */
.cm-send { display: grid; gap: 8px; padding: 16px 18px 18px; margin: 0;
  background: var(--surface); border: 0; border-top: 1px solid var(--line-strong); }
.cm-send-title { font-size: 15px; font-weight: 500; color: var(--ink); }
.cm-send-sub { margin: 0; font-size: 12px; line-height: 1.5; color: var(--ink-mid); }
.cm-send-btn { width: 100%; height: 44px; margin-top: 6px; font-size: 14px; }

.cm-modal { position: fixed; inset: 0; z-index: 90; display: grid; place-items: center; padding: 16px;
  background: rgba(0, 0, 0, 0.6); backdrop-filter: blur(3px); }
.cm-sheet { width: min(520px, 100%); max-height: calc(100vh - 32px); overflow: auto; display: grid; gap: 14px;
  grid-template-columns: minmax(0, 1fr);   /* a long order link must not widen the sheet */
  padding: 22px; border-radius: 18px; background: var(--card); border: 1px solid var(--glass-border-top);
  box-shadow: var(--glass-shadow); color: var(--ink); }
.cm-head { display: flex; align-items: center; gap: 10px; }
.cm-head h2, .cm-sheet h2 { margin: 0; flex: 1; font-size: 19px; font-weight: 500; }
.cm-close { width: 32px; height: 32px; border-radius: 50%; border: 0; background: var(--field);
  color: var(--ink-mid); font-size: 18px; cursor: pointer; }
.cm-summary { display: flex; align-items: center; gap: 12px; padding: 10px; border-radius: 12px; background: var(--field); }
.cm-thumb { width: 64px; height: 64px; border-radius: 10px; object-fit: cover; background: #0c0c0c; }
.cm-summary-text { display: grid; gap: 2px; font-size: 13px; }
.cm-summary-text span { color: var(--ink-dim); font-size: 12px; }
.cm-grid { display: grid; gap: 10px; }
.cm-two { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.cm-two:empty { display: none; }
.cm-field { display: grid; gap: 5px; font-size: 12px; color: var(--ink-dim); }
.cm-input { width: 100%; box-sizing: border-box; height: 38px; padding: 0 12px; border-radius: var(--radius-ctl);
  border: 1px solid var(--line-strong); background: var(--field); color: var(--ink); font: inherit; font-size: 14px; outline: none; }
textarea.cm-input { height: auto; padding: 10px 12px; resize: vertical; line-height: 1.5; }
.cm-input:focus { border-color: var(--accent); }
.cm-trap { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }
.cm-hint { margin: 0; font-size: 12px; line-height: 1.5; color: var(--ink-dim); }
.cm-error { margin: 0; font-size: 13px; color: #ff8a8a; }
.cm-stock { margin: 0; padding: 9px 11px; border-radius: 10px; font-size: 12px; line-height: 1.45; color: rgba(255, 205, 170, 0.95); background: rgba(255, 150, 90, 0.08); border: 1px solid rgba(255, 150, 90, 0.22); }
.cm-stock[hidden] { display: none; }
.cm-submit { width: 100%; height: 42px; }
/* A list for a batch (client-mode.js listField). */
.cm-list { border: 1px solid var(--line-strong); border-radius: 12px; padding: 10px 12px; }
.cm-list summary { cursor: pointer; font-size: 13px; color: var(--ink); }
.cm-list-body { display: grid; gap: 8px; margin-top: 10px; }
.cm-list-bar { display: flex; align-items: center; justify-content: space-between; gap: 8px; font-size: 12px; color: var(--ink-dim); }
.cm-upload { position: relative; display: inline-flex; align-items: center; width: fit-content; padding: 6px 10px;
  border-radius: 999px; border: 1px solid var(--line-strong); font-size: 12px; color: var(--ink-mid); cursor: pointer; }
.cm-upload input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.cm-input:disabled { opacity: 0.6; }
.cm-count.cm-over { color: #ff8a8a; }
.cm-tick { width: 48px; height: 48px; border-radius: 50%; display: grid; place-items: center; font-size: 24px;
  background: var(--accent-soft); color: var(--accent); }
.cm-link { display: flex; align-items: center; gap: 8px; padding: 6px 6px 6px 12px; border-radius: 999px;
  border: 1px solid var(--line-strong); background: var(--field); }
.cm-link code { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
.cm-actions { display: flex; gap: 8px; flex-wrap: wrap; }
@media (max-width: 520px) { .cm-two { grid-template-columns: 1fr; } }
@media (max-width: 900px) {
  .cm-banner { top: 12px; left: 50%; transform: translateX(-50%); }
}


/* ---------------------------------------------------------------- generator
   switch

   The panel head on a studio that has more than one generator. A tab per
   product was right at two and wrong at six: the labels wrapped, every new
   product stole width from the rest, and the bar stopped saying which one you
   were in. This is one row whatever the catalogue does -- the current generator
   with its cover, a menu of the others, and the home page underneath for the
   whole shelf. (A single-generator deploy keeps the tab bar; see app.js.) */

.gen-switch {
  flex: 1;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 11px 7px 7px;
  border: 1px solid var(--line);
  border-radius: 12px;
  background: rgba(255, 255, 255, 0.04);
  color: var(--ink);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: background 0.16s ease, border-color 0.16s ease;
}
.gen-switch:hover { background: rgba(255, 255, 255, 0.07); border-color: var(--line-strong); }
.gen-switch:focus-visible {
  outline: none;
  border-color: rgba(255, 255, 255, 0.4);
  box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.1);
}
.gen-switch .select-caret { margin-left: auto; }

/* The cover, by the home page's convention (web/covers/<id>.png). A transparent
   render on a graphite tile, so a piece reads at 30px without a box around it. */
.gen-thumb {
  flex: none;
  width: 32px; height: 32px;
  display: grid;
  place-items: center;
  overflow: hidden;
  border-radius: 8px;
  background: rgba(255, 255, 255, 0.06);
}
.gen-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gen-thumb-sm { width: 26px; height: 26px; border-radius: 7px; }
.gen-mono {
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.04em;
  color: var(--ink-dim);
}
.gen-face { display: contents; }

.gen-meta { display: flex; flex-direction: column; gap: 1px; min-width: 0; }
.gen-kicker {
  font-size: 10.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.gen-name {
  font-size: 13px;
  font-weight: 500;
  color: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* The menu: the home page's shelf in miniature - a photo tile per generator,
   two to a row at the panel's width, the name on
   the picture. The one-liner moved to the tile's
   tooltip; at this size the photo says what a generator makes. */
.popover.gen-pop {
  width: min(360px, calc(100vw - 16px));   /* app.js sets the panel's width */
  min-width: 0;
  max-width: none;
  max-height: min(78vh, 640px);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: 8px;
  /* The panel's own thin scrollbar, not the system one. */
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
}
.popover.gen-pop::-webkit-scrollbar { width: 6px; }
.popover.gen-pop::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.18);
  border-radius: 99px;
}
.gen-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 8px;
}
.gen-tile {
  position: relative;
  display: block;
  aspect-ratio: 3 / 4;
  padding: 0;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: 10px;
  background: radial-gradient(120% 90% at 50% 10%, #3a3a3a 0%, #1c1c1c 60%, #121212 100%);
  color: #fff;
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color 0.15s ease, transform 0.15s ease;
}
.gen-tile:hover { border-color: var(--line-strong); }
.gen-tile:hover .gen-tile-art img { transform: scale(1.04); }
.gen-tile:focus-visible { outline: none; border-color: rgba(255, 255, 255, 0.5); }
.gen-tile.is-current {
  border-color: rgba(255, 255, 255, 0.75);
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.75);
}
.gen-tile.is-soon { cursor: default; opacity: 0.45; }
.gen-tile-art {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
}
.gen-tile-art img {
  width: 78%;
  height: 78%;
  object-fit: contain;
  transition: transform 0.3s ease;
}
.gen-tile-art img.is-photo { width: 100%; height: 100%; object-fit: cover; }
/* The name sits on a dark fade at the foot of the picture, as on the home page. */
.gen-tile-name {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  padding: 22px 8px 7px;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.78), rgba(0, 0, 0, 0));
  font-size: 12px;
  font-weight: 500;
  line-height: 1.2;
}
.gen-tile .gen-tag {
  position: absolute;
  top: 6px; right: 6px;
  margin: 0;
}
.gen-tag {
  margin-left: auto;
  flex: none;
  font-size: 9.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
/* The whole shelf is a page, not a menu: the last row goes there. */
.gen-all {
  display: block;
  margin-top: 4px;
  padding: 9px 8px 5px;
  border-top: 1px solid var(--line);
  color: var(--ink-dim);
  font-size: 12px;
  text-decoration: none;
}
.gen-all:hover { color: var(--ink); }

/* ------------------------------------------------------------ first-steps tips
   web/tour.js, after a shop is opened: the studio dims, one part of it stays lit,
   and a card beside it says what that part is for. Above the desk menu (250) and
   the popovers (200), so nothing on the page can be clicked past it. */
.tour {
  position: fixed;
  inset: 0;
  z-index: 400;
  animation: tour-in 0.2s ease;
}
.tour.closing { opacity: 0; transition: opacity 0.18s ease; }
@keyframes tour-in { from { opacity: 0; } to { opacity: 1; } }
/* The lit part: a hole in a dark veil, made by the hole's own giant shadow. */
.tour-hole {
  position: fixed;
  border-radius: 14px;
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.62), 0 0 0 1px rgba(255, 255, 255, 0.35) inset;
  pointer-events: none;
  transition: left 0.25s ease, top 0.25s ease, width 0.25s ease, height 0.25s ease;
}
.tour-card {
  position: fixed;
  width: 300px;
  padding: 16px 18px 14px;
  background: var(--pop-face, #191919);
  border: 1px solid var(--line-strong);
  border-radius: 16px;
  box-shadow: 0 24px 60px -20px rgba(0, 0, 0, 0.8), 0 2px 10px -2px rgba(0, 0, 0, 0.5);
  color: var(--ink);
  transition: left 0.25s ease, top 0.25s ease;
}
.tour-count {
  font-size: 9.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.tour-title {
  margin: 6px 0 4px;
  font-family: var(--serif);
  font-size: 21px;
  font-weight: 500;
  line-height: 1.2;
}
.tour-text {
  margin: 0;
  font-size: 13px;
  line-height: 1.5;
  color: var(--ink-mid);
}
.tour-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 16px;
}
.tour-gap { flex: 1; }
.tour-actions .btn { height: 34px; padding: 0 16px; }
.tour-skip {
  padding: 0;
  border: none;
  background: none;
  color: var(--ink-dim);
  font-family: inherit;
  font-size: 12px;
  cursor: pointer;
}
.tour-skip:hover { color: var(--ink); }
/* A phone: the card is a sheet along the bottom, the lit part stays above it. */
@media (max-width: 639px) {
  .tour-card { left: 12px; right: 12px; bottom: 12px; width: auto; }
}
@media (prefers-reduced-motion: reduce) {
  .tour, .tour-hole, .tour-card { animation: none; transition: none; }
}

/* ------------------------------------------------------------ print estimate
   The readout's "Cost & plastic" button and the card it opens (web/printplan.js):
   a fixed sheet over the viewport's bottom-left corner, above the readout. The
   button is the one accent-tinted thing in the readout - the cost is the number
   a printer owner came for, so it should not read as a footnote. */
.cost-btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin-top: 2px;
  padding: 7px 12px 7px 11px;
  border: 1px solid var(--accent-line);
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--ink);
  font: inherit;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.01em;
  cursor: pointer;
  transition: background 0.15s ease, border-color 0.15s ease;
}
.cost-btn::after {                     /* "opens something" */
  content: "";
  width: 6px; height: 6px;
  border-top: 1.5px solid currentColor;
  border-right: 1.5px solid currentColor;
  transform: rotate(45deg);
  opacity: 0.7;
}
.cost-btn:hover { background: rgba(233, 216, 208, 0.2); border-color: var(--accent); }
.cost-btn[hidden] { display: none; }
.cost-btn-value { font-weight: 500; color: var(--accent); font-variant-numeric: tabular-nums; }
.cost-btn-value:empty { display: none; }
/* A client on a shop link sees the grams, not the owner's cost sheet. */
body.client-mode #printPlanBtn { display: none; }
.pp-pick { display: grid; gap: 8px; margin: 4px 0 10px; }
.pp-pick .mini-select { width: 100%; }
.pp-colour small { display: block; font-size: 10.5px; margin-top: 1px; }
.pp-colour small.pp-ok { color: var(--ink-faint); }
.pp-colour small.pp-short { color: rgba(255, 190, 150, 0.95); }
.pp-hint { grid-column: 1 / -1; margin: 2px 0 0; font-size: 11px; line-height: 1.45; color: var(--ink-dim); }
.pp-hint a { color: var(--accent); }

.print-plan {
  position: fixed;
  z-index: 150;
  left: 16px;
  bottom: 16px;
  width: min(360px, calc(100vw - 32px));
  max-height: calc(100vh - 32px);
  overflow: auto;
  padding: 14px 16px 12px;
  background: var(--pop-face, #191919);
  border: 1px solid var(--line-strong);
  border-radius: 14px;
  box-shadow: 0 18px 44px -16px rgba(0, 0, 0, 0.7), 0 2px 8px -2px rgba(0, 0, 0, 0.5);
  color: var(--ink);
  font-size: 12.5px;
}
.pp-head { display: flex; align-items: center; justify-content: space-between; }
.pp-title { font-size: 14px; font-weight: 600; letter-spacing: 0.01em; }
.pp-close {
  width: 26px; height: 26px;
  border: 0; border-radius: 8px;
  background: none; color: var(--ink-dim);
  font-size: 18px; line-height: 1; cursor: pointer;
}
.pp-close:hover { background: var(--field-hover); color: var(--ink); }
.pp-sub { margin: 2px 0 10px; color: var(--ink-dim); font-size: 11.5px; }
.pp-rows { margin: 0; display: grid; gap: 6px; }
.pp-rows + .pp-rows, .pp-money { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--line); }
.pp-row { display: flex; justify-content: space-between; gap: 12px; }
.pp-row dt { color: var(--ink-dim); }
.pp-row dd { margin: 0; font-variant-numeric: tabular-nums; text-align: right; }
.pp-strong dt, .pp-strong dd { color: var(--ink); font-weight: 600; }
.pp-colours { display: grid; gap: 4px; margin: -2px 0 4px 2px; }
.pp-colour { display: flex; align-items: center; gap: 8px; color: var(--ink-mid); font-size: 11.5px; }
.pp-colour i {
  width: 11px; height: 11px; border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.25); flex: none;
}
.pp-colour span { flex: 1 1 auto; }
.pp-colour b { font-weight: 400; font-variant-numeric: tabular-nums; }
.pp-tips { margin-top: 12px; padding: 10px 12px; border-radius: 10px; background: var(--accent-soft); }
.pp-label { font-size: 10.5px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-dim); }
.pp-tip { margin: 6px 0 0; line-height: 1.45; color: var(--ink-mid); }
.pp-settings { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 10px; }
.pp-settings summary { cursor: pointer; color: var(--ink-mid); }
.pp-settings summary:hover { color: var(--ink); }
.pp-form { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 10px; margin-top: 10px; }
.pp-field { display: grid; gap: 4px; font-size: 11px; color: var(--ink-dim); }
.pp-field:first-child { grid-column: 1 / -1; }
.pp-field .mini-select, .pp-input { width: 100%; box-sizing: border-box; }
.pp-input {
  padding: 5px 8px;
  border: 1px solid var(--line-strong);
  border-radius: 8px;
  background: var(--field);
  color: var(--ink);
  font: inherit;
  font-size: 12px;
}
.pp-check { grid-column: 1 / -1; display: flex; align-items: center; gap: 8px; color: var(--ink-mid); font-size: 11.5px; }
.pp-note { margin: 12px 0 0; color: var(--ink-faint); font-size: 10.5px; line-height: 1.45; }

/* --------------------------------------------------- the shop's filament stock
   On a shop link the swatch card splits into what the printer has in stock and
   the rest, which stay visible - to compare - but are marked (or locked, when
   the shop allows only its own). */
.swatch-pop-section { margin-top: 10px; font-size: 11px; color: var(--ink-dim); }
.swatch-pop-section:first-of-type { margin-top: 0; }
.swatch-grid.is-out .swatch { opacity: 0.55; position: relative; }
.swatch-grid.is-out .swatch::after {
  content: "";
  position: absolute; inset: 45% -2px auto -2px;
  height: 1.5px; background: rgba(255, 255, 255, 0.75);
  transform: rotate(-35deg);
}
.swatch-grid.is-out .swatch:disabled { cursor: not-allowed; transform: none; }
.swatch-pop-hint { margin: 8px 0 0; font-size: 10.5px; color: var(--ink-faint); line-height: 1.4; max-width: 240px; }
.swatch-stock { font-size: 10.5px; color: rgba(255, 190, 150, 0.95); white-space: nowrap; }
/* The cost card's inputs in the site's language: no browser spinners, no white
   focus ring, a drawn checkbox. */
.pp-input { -moz-appearance: textfield; appearance: textfield; transition: border-color 0.15s ease; }
.pp-input::-webkit-inner-spin-button,
.pp-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.pp-input:hover { border-color: rgba(255, 255, 255, 0.26); }
.pp-input:focus, .pp-input:focus-visible { outline: none; border-color: var(--accent-line); background: var(--field-hover); }
.pp-check input {
  appearance: none; -webkit-appearance: none; flex: none;
  width: 16px; height: 16px; margin: 0; border-radius: 5px;
  border: 1px solid var(--line-strong); background: var(--field);
  display: grid; place-items: center; cursor: pointer;
}
.pp-check input:checked { background: var(--accent); border-color: var(--accent); }
.pp-check input:checked::after {
  content: ""; width: 4px; height: 8px; margin-top: -2px;
  border: solid var(--accent-ink); border-width: 0 2px 2px 0; transform: rotate(45deg);
}
.pp-check input:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--accent-line); }
.print-plan .select-trigger { width: 100%; }
.print-plan {
  overflow-x: hidden;
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.2) transparent;
}
.print-plan::-webkit-scrollbar { width: 8px; }
.print-plan::-webkit-scrollbar-track { background: transparent; }
.print-plan::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.2); border-radius: 8px; border: 2px solid transparent; background-clip: padding-box; }
.pp-form .pp-input { width: 100%; }

/* The studio's way into a batch (/bulk): under the downloads, full width. */
.batch-btn { width: 100%; margin-top: 8px; }
.batch-btn[hidden] { display: none; }
/* The cost card's way to the Workshop. */
.pp-workshop { display: flex; align-items: center; justify-content: center; width: 100%;
  margin-top: 12px; height: 38px; font-size: 12.5px; }

/* ---- Batch: the panel section (batch-studio.js) ---- */
.batch-block .batch-tag { font-size: 11px; color: var(--ink-dim); margin-right: 8px; letter-spacing: 0.02em; }
.batch-block[hidden] { display: none; }
#batchBody .bs-intro { margin: 0 0 14px; font-size: 12.5px; line-height: 1.5; color: var(--ink-mid); }
#batchBody .bs-label { display: block; margin: 14px 0 6px; font-size: 12px; color: var(--ink-dim); }
#batchBody .bs-rows { width: 100%; min-height: 150px; resize: vertical; background: var(--field);
  color: var(--ink); border: 1px solid var(--line-strong); border-radius: var(--radius-ctl);
  padding: 10px 12px; font: 13px/1.55 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
#batchBody .bs-rows:focus { outline: none; border-color: rgba(255, 255, 255, 0.32); }
#batchBody .bs-rowbar { display: flex; justify-content: space-between; align-items: center; margin-top: 6px; }
#batchBody .bs-count { font-size: 12.5px; color: var(--ink); }
#batchBody .bs-upload { font-size: 12px; color: var(--ink-mid); cursor: pointer; text-decoration: underline;
  text-underline-offset: 3px; }
#batchBody .bs-upload input { display: none; }
#batchBody .bs-hint { margin: 6px 0 0; font-size: 11.5px; line-height: 1.45; color: var(--ink-dim); }
#batchBody .bs-select { width: 100%; height: 38px; background: var(--field); color: var(--ink);
  border: 1px solid var(--line-strong); border-radius: var(--radius-ctl); padding: 0 10px; font: inherit; font-size: 13px; }
#batchBody .bs-check { margin-top: 12px; padding: 12px; border-radius: 12px;
  border: 1px solid rgba(232, 193, 112, 0.35); background: rgba(232, 193, 112, 0.06); font-size: 12.5px; }
#batchBody .bs-check[hidden], #batchBody [hidden] { display: none; }
#batchBody .bs-check b { color: #e8c170; font-weight: 500; }
#batchBody .bs-check p { margin: 6px 0 0; color: var(--ink-mid); line-height: 1.45; }
#batchBody .bs-fixes { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
#batchBody .bs-fixes .btn { height: 32px; padding: 0 12px; font-size: 12px; }
#batchBody .bs-make { width: 100%; margin-top: 16px; height: 44px; font-size: 14px; }
#batchBody .bs-error { color: #ff8a7a; font-size: 12.5px; margin: 8px 0 0; }
#batchBody .bs-error:empty { display: none; }
#batchBody .bs-gate p { margin: 0 0 12px; font-size: 13px; color: var(--ink-mid); line-height: 1.5; }
#batchBody .bs-gate .btn { display: inline-flex; align-items: center; }
#batchBody .bs-status { margin-top: 18px; padding: 14px; border-radius: 12px; background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--line-strong); }
#batchBody .bs-status b { font-size: 14px; font-weight: 500; }
#batchBody .bs-bar { height: 6px; border-radius: 99px; background: var(--track); overflow: hidden; margin: 10px 0 12px; }
#batchBody .bs-bar i { display: block; height: 100%; width: 0; background: var(--fill); transition: width 0.6s ease; }
#batchBody .bs-bar i.done { background: var(--fill); }  /* full is "done"; green only means print-ready */
#batchBody .bs-bar i.stopped { background: var(--ink-dim); }
#batchBody .bs-actions { display: flex; flex-wrap: wrap; gap: 6px; }
#batchBody .bs-actions .btn { height: 34px; padding: 0 14px; font-size: 12.5px; display: inline-flex; align-items: center; }
#batchBody .bs-all { display: inline-block; margin-top: 14px; font-size: 12px; color: var(--ink-mid);
  text-decoration: underline; text-underline-offset: 3px; }

/* ---- Batch: the view over the viewport ---- */
.batch-stage { position: absolute; inset: 0; z-index: 20; display: flex; flex-direction: column;
  background: var(--bg); border-radius: var(--radius); padding: 18px 20px; gap: 14px; }
.batch-stage[hidden] { display: none; }
body.batch-view .stage-tools, body.batch-view .titles { visibility: hidden; }
.bv-head { display: flex; align-items: flex-start; gap: 14px; flex-wrap: wrap; }
.bv-titles { flex: 1 1 260px; }
.bv-kicker { font-size: 11px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--ink-dim); }
.bv-title { font-size: 22px; font-weight: 500; letter-spacing: -0.015em; margin: 2px 0; }
.bv-sub { font-size: 13px; color: var(--ink-mid); }
.bv-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.bv-actions .btn, .bv-back { display: inline-flex; align-items: center; height: 38px; }
.bv-grid { flex: 1; overflow: auto; display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: 8px; align-content: start; }
.bv-grid[hidden] { display: none; }
.bv-card { padding: 12px 14px; border-radius: 12px; font-size: 13px; color: var(--ink-faint);
  background: rgba(255, 255, 255, 0.03); border: 1px dashed var(--line-strong);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: all 0.35s ease; }
.bv-card.made { color: var(--ink); background: rgba(255, 255, 255, 0.07); border-style: solid;
  border-color: rgba(111, 211, 154, 0.45); }
.bv-result { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 10px; }
.bv-result[hidden] { display: none; }
.bv-tabs { display: flex; gap: 6px; flex-wrap: wrap; }
.bv-tab { height: 32px; padding: 0 14px; border-radius: 999px; border: 1px solid var(--line-strong);
  background: none; color: var(--ink-mid); font: inherit; font-size: 12.5px; cursor: pointer; }
.bv-tab.on { background: rgba(255, 255, 255, 0.92); color: #111; border-color: transparent; }
.bv-canvas { position: relative; flex: 1; min-height: 240px; border-radius: 14px; overflow: hidden;
  border: 1px solid var(--line); }
.bv-canvas canvas { width: 100%; height: 100%; display: block; }
.bv-tip { position: absolute; pointer-events: none; padding: 6px 10px; border-radius: 8px; font-size: 12px;
  background: rgba(0, 0, 0, 0.8); border: 1px solid var(--line-strong); max-width: 320px; }
.bv-tip[hidden], .bv-loading[hidden], .bv-needs[hidden] { display: none; }
.bv-loading { position: absolute; inset: 0; display: grid; place-items: center; color: var(--ink-mid); font-size: 13px; }
.bv-needs { font-size: 12.5px; }
.bv-needs summary { cursor: pointer; color: #e8c170; }
.bv-needs table { width: 100%; border-collapse: collapse; margin-top: 8px; }
.bv-needs td { padding: 6px 8px; border-bottom: 1px solid var(--line); vertical-align: top; }
.bv-needs td.n { color: var(--ink-dim); width: 30px; }
.bv-needs td.err { color: #ff8a7a; white-space: nowrap; }
.bv-needs td.warn { color: #e8c170; white-space: nowrap; }
.bv-needs td.why { color: var(--ink-mid); }
@media (max-width: 900px) {
  .batch-stage { position: fixed; inset: 0; z-index: 60; border-radius: 0; }
}
/* Under the batch view the studio's model is not shown (it would show through
   the gap beside the panel) and not drawn for nothing. */
body.batch-view #canvas { visibility: hidden; }

/* ---- Batch: logos for the rows (batch-studio.js) ---- */
#batchBody .bs-logos { margin-top: 14px; }
#batchBody .bs-logos .bs-inline { display: inline; margin: 0; }
#batchBody .bs-link { background: none; border: 0; padding: 0; margin-right: 12px; font: inherit; font-size: 12px;
  color: var(--ink-dim); cursor: pointer; text-decoration: underline; text-underline-offset: 3px; }
#batchBody .bs-refused { margin: 6px 0 0; padding-left: 16px; font-size: 11.5px; line-height: 1.45; color: #e8c170; }
#batchBody .bs-refused:empty { display: none; }
#batchBody .bs-match { margin-top: 10px; border: 1px solid var(--line-strong); border-radius: 12px; font-size: 12px; }
#batchBody .bs-match.bad { border-color: rgba(255, 138, 122, 0.45); }
#batchBody .bs-match summary { padding: 10px 12px; cursor: pointer; color: var(--ink); }
#batchBody .bs-match.bad summary { color: #ff8a7a; }
#batchBody .bs-match table { width: 100%; border-collapse: collapse; display: block; max-height: 220px;
  overflow: auto; padding: 0 12px 10px; }
#batchBody .bs-match td { padding: 3px 8px 3px 0; vertical-align: top; color: var(--ink-mid); word-break: break-word; }
#batchBody .bs-match td.n { color: var(--ink-dim); text-align: right; width: 1%; }
#batchBody .bs-match tr.dim td { color: var(--ink-dim); }
#batchBody .bs-match tr.err td { color: #ff8a7a; }
#batchBody .bs-warn { margin: 8px 0 0; padding: 10px 12px; border-radius: 10px; font-size: 12px; line-height: 1.45;
  color: #e8c170; border: 1px solid rgba(232, 193, 112, 0.35); background: rgba(232, 193, 112, 0.06); }
#batchBody .bs-warn.bs-bad { color: #ff9a8a; border-color: rgba(255, 120, 100, 0.45); background: rgba(255, 120, 100, 0.07); }
#batchBody .bs-warn p { margin: 0; }
#batchBody .bs-warn .bs-fixes:not(:empty) { margin-top: 8px; }
#batchBody .bs-sent { color: var(--ink); }
#batchBody .bs-logos.busy .bs-sent::before { content: ""; display: inline-block; width: 10px; height: 10px;
  margin-right: 6px; vertical-align: -1px; border: 2px solid var(--ink-dim); border-top-color: var(--ink);
  border-radius: 50%; animation: bs-spin 0.8s linear infinite; }
#batchBody .bs-logos.busy .bs-upload { opacity: 0.4; pointer-events: none; }
@keyframes bs-spin { to { transform: rotate(360deg); } }
#batchBody .bs-head { display: flex; align-items: center; }
#batchBody .bs-head .info { margin-right: 0; padding-left: 6px; }
#batchBody .bs-head .bs-label { margin-bottom: 6px; }
#batchBody .bs-logos .bs-head .bs-label { margin: 0; }

/* ---- Batch: the list as rows (batch-rows.js) ---- */
#batchBody .br { border: 1px solid var(--line-strong); border-radius: var(--radius-ctl); background: var(--field);
  padding: 6px 6px 4px; max-height: 360px; overflow: auto; }
#batchBody .br.drop { border-color: rgba(255, 255, 255, 0.45); background: rgba(255, 255, 255, 0.05); }
#batchBody .br-head, #batchBody .br-row { display: grid; gap: 6px; align-items: center; }
#batchBody .br-head { padding: 0 2px 4px; font-size: 11px; color: var(--ink-dim); }
#batchBody .br-all { display: flex; align-items: center; gap: 4px; cursor: pointer; color: var(--ink-mid); }
#batchBody .br-row { padding: 2px; }
#batchBody .br-row input[type="checkbox"], #batchBody .br-all input { width: 15px; height: 15px; margin: 0 0 0 4px;
  accent-color: #e9d8d0; cursor: pointer; }
#batchBody .br-text { min-width: 0; height: 30px; border: 1px solid transparent; border-radius: 7px; background: transparent;
  color: var(--ink); padding: 0 8px; font: inherit; font-size: 13px; }
#batchBody .br-text:hover { border-color: var(--line-strong); }
#batchBody .br-text:focus { outline: none; border-color: rgba(255, 255, 255, 0.32); background: rgba(255, 255, 255, 0.03); }
#batchBody .br-second { color: var(--ink-mid); }
#batchBody .br-chip { height: 26px; min-width: 0; border: 1px solid var(--line-strong); border-radius: 7px; background: none;
  color: var(--ink-mid); font: inherit; font-size: 11.5px; padding: 0 8px; cursor: pointer; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap; text-align: left; }
#batchBody .br-chip:hover { color: var(--ink); border-color: rgba(255, 255, 255, 0.32); }
#batchBody .br-chip.warn { color: #e8c170; border-color: rgba(232, 193, 112, 0.45); }
#batchBody .br-chip[hidden] { display: none; }
#batchBody .br-del { width: 20px; height: 20px; border: 0; background: none; color: var(--ink-faint); cursor: pointer;
  font-size: 15px; line-height: 1; opacity: 0; }
#batchBody .br-row:hover .br-del, #batchBody .br-del:focus { opacity: 1; }
#batchBody .br-add { margin: 4px 0 2px 6px; border: 0; background: none; color: var(--ink-dim); font: inherit;
  font-size: 12px; cursor: pointer; }
#batchBody .br-add:hover { color: var(--ink); }
#batchBody .bs-logobar .bs-sent:empty { display: none; }
#batchBody .bs-logobar.busy .bs-sent::before { content: ""; display: inline-block; width: 10px; height: 10px;
  margin-right: 6px; vertical-align: -1px; border: 2px solid var(--ink-dim); border-top-color: var(--ink);
  border-radius: 50%; animation: bs-spin 0.8s linear infinite; }
.popover.br-menu { min-width: 180px; padding: 6px; }
.br-menu .br-item { display: block; width: 100%; text-align: left; border: 0; background: none; color: var(--ink-mid);
  font: inherit; font-size: 12.5px; padding: 7px 10px; border-radius: 7px; cursor: pointer; }
.br-menu .br-item:hover { background: rgba(255, 255, 255, 0.06); color: var(--ink); }
.br-menu .br-item.on { color: var(--ink); }
.br-menu .br-item.on::after { content: " ✓"; }
.br-menu .br-note { margin: 6px 10px 2px; font-size: 11.5px; line-height: 1.4; color: var(--ink-dim); max-width: 26ch; }
.bv-tabbar { display: flex; align-items: flex-start; gap: 10px; }
.bv-tabbar .bv-tabs { flex: 1; }
.bv-flip { height: 32px; padding: 0 14px; border-radius: 999px; border: 1px solid var(--line-strong);
  background: none; color: var(--ink-mid); font: inherit; font-size: 12.5px; cursor: pointer; white-space: nowrap; }
.bv-flip[aria-pressed="true"] { background: rgba(255, 255, 255, 0.12); color: var(--ink); }
#batchBody .bs-face { display: flex; align-items: center; margin-top: 10px; font-size: 12.5px; color: var(--ink-mid); }
#batchBody .bs-face label { display: flex; align-items: center; gap: 8px; cursor: pointer; }
#batchBody .bs-order { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 0 0 10px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 10px; font-size: 12.5px; color: var(--ink-mid); }
#batchBody .bs-order a, #batchBody .bs-for { color: var(--ink); font-weight: 600; text-decoration: none; }
#batchBody .bs-for { display: block; margin-bottom: 4px; font-weight: 500; }
#batchBody .bs-order[hidden], #batchBody .bs-for[hidden] { display: none; }
#batchBody .bs-x { background: none; border: 0; color: var(--ink-dim); font-size: 16px; line-height: 1; cursor: pointer; padding: 2px 6px; }
#batchBody .bs-face input { width: 15px; height: 15px; margin: 0; accent-color: #e9d8d0; }
#batchBody .bs-face .info { margin-right: 0; }

/* The batch view has its own head (Download, Layout sheet) right where the
   "shared this design" bar sits; the bar is about the design, not the batch. */
body.batch-view .share-bar { display: none; }

/* A client on a shop link sees the piece and its size, not the print check's
   chips or the filament estimate - those are the shop's business. */
body.client-mode #gate, body.client-mode #factFilamentRow { display: none !important; }
/* The owner is the one who prints: the "send the link to whoever prints it"
   handoff is for a visitor designing for someone else. */
body.has-desk-nav #handoff { display: none; }
/* studio-nav.js: an owner's browser, menu not drawn yet - no visitor widgets flashing. */
body.desk-pending .auth-widget, body.desk-pending #library, body.desk-pending .titles { visibility: hidden; }


/* ---------------------------------------------------------------- panel dock
   Download / Save / More under the controls (app.js, mountDock): the next step
   is always in view. The Export block keeps the full set; "Make a batch" there is
   gone - the Batch section is the one way in. */
.panel-dock { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 8px;
  padding: 12px 18px 14px; background: var(--surface); border-top: 1px solid var(--line-strong); }
.panel-dock .btn { height: 40px; }
.panel-dock .dock-download { font-size: 14px; }
#makeBatch { display: none !important; }
@media (max-width: 900px) {
  /* Stacked, the dock rides the bottom of the screen while the panel is in view. */
  .panel-dock { position: sticky; bottom: 0; z-index: 6; border-radius: 0 0 14px 14px;
    padding-bottom: calc(12px + env(safe-area-inset-bottom)); }
  /* The piece stays in sight while its settings scroll under it: the stage's
     parts join the page grid, so the viewport can stick to the top of the screen
     across the whole panel. */
  .stage { display: contents; }
  /* (.shell > .stage > .viewport outranks desk-nav.css's "inset: auto".) */
  .shell > .stage > .viewport { position: sticky; top: 0; bottom: auto; z-index: 5; height: 40vh;
    margin-top: 0; background: var(--bg, #111); border-bottom: 1px solid var(--line-strong); }
}
@media (max-width: 900px) and (max-height: 600px) {
  .shell > .stage > .viewport { position: relative; }   /* a phone on its side: no room to pin */
}
@media (max-width: 900px) {
  /* A client on a phone: "Send to <shop>" stays at the bottom of the screen too. */
  .panel > .cm-send { position: sticky; bottom: 0; z-index: 6; gap: 4px;
    padding: 10px 16px calc(12px + env(safe-area-inset-bottom)); border-radius: 0 0 14px 14px; }
  .panel > .cm-send .cm-send-title, .panel > .cm-send .cm-send-sub { display: none; }
  .panel > .cm-send .cm-send-btn { margin-top: 0; }
}

/* auth.js: "Saved" after Save design, at the foot of the screen. */
.saved-note { position: fixed; left: 50%; bottom: 24px; transform: translateX(-50%); z-index: 120;
  padding: 10px 16px; border-radius: 12px; background: #1d1d1f; border: 1px solid var(--line-strong);
  color: var(--ink); font-size: 13px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5); max-width: calc(100vw - 32px); }
.saved-note a { color: inherit; text-decoration: underline; margin-left: 4px; }
@media (max-width: 900px) { .saved-note { bottom: 84px; } }

/* batch-studio.js: the printer is the Cost & plastic one, named with a Change link. */
#batchBody .bs-select[hidden] { display: none; }
#batchBody .bs-printer-line { display: flex; align-items: center; gap: 10px; margin: 0; font-size: 13px; color: var(--ink); }
#batchBody .bs-link { padding: 4px 2px; border: 0; background: none; color: var(--ink-mid); font: inherit;
  font-size: 12px; text-decoration: underline; cursor: pointer; }
#batchBody .bs-link:hover { color: var(--ink); }

/* The desk's full-bleed canvas runs on under the inspector (desk-nav.css: the
   viewport is inset -40px -452px -40px -40px); the batch view is a page to read,
   so it keeps to the part of the screen that can be seen. */
@media (min-width: 901px) {
  body.has-desk-nav .batch-stage { inset: 40px 452px 40px 40px; }
}

/* Keyboard: the first Tab stop jumps to the settings (index.html). */
.skip-link { position: fixed; left: 12px; top: -60px; z-index: 300; padding: 10px 14px; border-radius: 10px;
  background: #fff; color: #111; font-size: 13px; text-decoration: none; }
.skip-link:focus { top: 12px; }
#controls:focus { outline: none; }

/* Touch: the small controls get a fingertip-sized hit area around them without
   changing how they look (the ⓘ is 12 px drawn, ~36 px to touch). */
@media (pointer: coarse) {
  .info, .toggle { position: relative; }
  .info::after { content: ""; position: absolute; inset: -12px -10px; }
  .toggle::after { content: ""; position: absolute; inset: -10px -6px; }
  .view-btn { min-height: 32px; }
  input[type="checkbox"] { width: 18px; height: 18px; }
}

/* Less motion asked for: no slides, spins or shimmer. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important; scroll-behavior: auto !important; }
}
.cm-list-warn { margin: 6px 0 0; font-size: 12.5px; line-height: 1.45; color: #e8c170; }
.cm-list-warn[hidden] { display: none; }

/* A client on a phone: the shop's banner gets its own line above the wordmark
   (it sat on top of it), and the lighting controls - a studio nicety - step
   aside so the piece comes first. */
@media (max-width: 900px) {
  body.client-mode .titles { margin-top: 44px; }
  body.client-mode #lightTools { display: none; }
}

/* batch-rows.js: how many identical pieces of a row. */
#batchBody .br-copies { width: 100%; height: 32px; padding: 0 6px; border-radius: 8px; border: 1px solid var(--line-strong);
  background: var(--field); color: var(--ink); font: inherit; font-size: 13px; text-align: center; }
#batchBody .br-copies:focus { outline: none; border-color: rgba(255, 255, 255, 0.32); }
#batchBody .br-h-copies { text-align: center; }
/* A laptop: the menu and the panel leave the batch a strip; the batch view takes
   the menu's room too while it is open ("Back to the design" brings it back). */
@media (min-width: 901px) and (max-width: 1280px) {
  body.has-desk-nav.batch-view .batch-stage { position: fixed; inset: 16px 428px 16px 16px; z-index: 260; }
  body.has-desk-nav.batch-view .desk-nav { visibility: hidden; }
}
body.batch-view .panel-dock { display: none; }     /* the batch has its own Download */
