/**
 * shell.css
 * The shared page-shell stylesheet for every generator app — the theme-neutral UI shell, form
 * controls, loading/toast feedback, the debug cache-dump table, AND the folded-in ColorPicker
 * popover (the old Libraries/color-picker.css `.cp-*` rules). It replaces the ~150 lines of
 * dark-theme shell + control CSS each app used to duplicate inline.
 *
 * Cascade contract (identical to mobile-layout.css):
 *   - Linked AFTER each app's tiny inline <style> (so this file's shell/control rules win the
 *     duplicated ones during migration), and BEFORE Libraries/mobile-layout.css (so the mobile
 *     layer's small-screen overrides still win over these).
 *   - Mostly theme-neutral: --bg --sidebar --surface --border --text --muted --danger --sidebar-w
 *     come from a var(--x) the app defines in its own :root — a Node test (Libraries/shell.test.js)
 *     asserts every one of THOSE this file uses is defined by every app that links it.
 *   - --accent / --accent-dim / --accent-wash are the ONE exception: defined HERE, not per-app. Every
 *     generator used to pick its own accent (gold vs blue), the only palette value that ever actually
 *     varied — apps must NOT redeclare these three (shell.test.js's strict no-redeclare check covers
 *     this the same way it covers shared shell/control rules).
 *   - Typography is parameterised: --font-ui (body/controls) and --font-display (the h1) with the
 *     original Word-Easel fonts as the fallback, so an app that wants a different face overrides the
 *     var instead of forking a rule.
 *
 * The @media block near the bottom (ColorPicker's mobile bottom-sheet) MUST stay
 * character-identical to MobileUI.MEDIA_QUERY / the @media rule in mobile-layout.css — a Node test
 * enforces the match, or the sheet layout shows on the wrong screens.
 */

/* ── Shared accent (the one palette value every generator used to redeclare separately) ── */
:root {
  --accent: #5aa9e6;
  --accent-dim: #3a6ea5;
  --accent-wash: rgba(90, 169, 230, 0.05);
  /* The green of a highlighted dimension (the red is the shared --danger) — see Libraries/dim-diagram.js. */
  --callout-green: #4cc38a;
}

/* ── Global reset ── */
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html,
body {
  height: 100%;
  overflow: hidden;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui, 'DM Mono', monospace);
  font-size: 13px;
}

/* ── Page shell ── */
.app {
  display: flex;
  height: 100vh;
}

.sidebar {
  width: var(--sidebar-w);
  min-width: var(--sidebar-w);
  background: var(--sidebar);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

.sidebar-header {
  padding: 20px 20px 14px;
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}

.sidebar-header h1 {
  font-family: var(--font-display, 'Fraunces', serif);
  font-size: 20px;
  font-weight: 700;
  color: var(--accent);
  letter-spacing: 0.02em;
}

.sidebar-header p {
  color: var(--muted);
  font-size: 11px;
  margin-top: 3px;
}

.back-link {
  display: inline-block;
  font-size: 11px;
  color: var(--muted);
  text-decoration: none;
  margin-bottom: 8px;
  transition: color 0.15s;
}

.back-link:hover {
  color: var(--accent);
}

.sidebar-body {
  flex: 1;
  overflow-y: auto;
  padding: 16px;
}

.sidebar-body::-webkit-scrollbar {
  width: 5px;
}

.sidebar-body::-webkit-scrollbar-track {
  background: transparent;
}

.sidebar-body::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 3px;
}

/* ── Section labels + collapsible sections ── */
.section-label {
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent);
  margin: 18px 0 10px;
}

.section-label:first-child {
  margin-top: 0;
}

.collapsible-section {
  border: 1px solid var(--border);
  border-radius: 7px;
  margin-bottom: 10px;
  background: var(--surface);
}

.collapsible-section summary {
  list-style: none;
  cursor: pointer;
  padding: 10px 12px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--accent);
  user-select: none;
}

.collapsible-section summary::-webkit-details-marker {
  display: none;
}

.collapsible-section summary::after {
  content: '›';
  font-size: 16px;
  font-weight: 400;
  color: var(--muted);
  transform: rotate(90deg);
  transition: transform 0.15s;
  display: inline-block;
  line-height: 1;
}

.collapsible-section[open] summary::after {
  transform: rotate(270deg);
}

.collapsible-section summary:hover {
  color: var(--text);
}

.collapsible-section .section-content {
  padding: 2px 12px 12px;
  border-top: 1px solid var(--border);
}

.collapsible-section .section-content > .ctrl:first-child,
.collapsible-section .section-content > .check-row:first-child {
  margin-top: 12px;
}

/* ── Sidebar option search (Shell.optionSearch.attach) ──
   A plain block, right after the Configuration section (see Shell.optionSearch.attach) — NOT
   sticky/floating, so it scrolls with the rest of the sidebar instead of having content pass
   underneath it. */
.sidebar-search {
  margin: 0 0 14px;
}

.sidebar-search input {
  width: 100%;
  box-sizing: border-box;
  padding: 7px 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  font-family: inherit;
  font-size: 12px;
}

.sidebar-search input:focus-visible {
  outline: none;
  border-color: var(--accent);
}

/* The "Diagrams" show/hide toggle (Shell.dimDiagramToggle) shares the search row on pages that have
   dimension diagrams. aria-pressed carries the state; pressed = diagrams shown. */
.sidebar-search.has-dim-toggle { display: flex; gap: 6px; align-items: stretch; }
.sidebar-search.has-dim-toggle input { flex: 1 1 auto; min-width: 0; }
.dim-diagram-toggle {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 0 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--muted);
  font-family: inherit;
  font-size: 12px;
  cursor: pointer;
}
.dim-diagram-toggle[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); background: var(--accent-wash); }
.dim-diagram-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.dim-diagram-toggle-icon { font-size: 14px; line-height: 1; }
.dim-diagrams-off .dim-figure { display: none; }

/* ── Sidebar language selector (Shell.languageSelector.attach) ──
   A plain block, right after the option-search box (or the Configuration section if search isn't
   attached) — same non-sticky placement rationale as .sidebar-search. Only rendered at all once a
   second locale is registered, so this never appears as a single, useless option. */
.sidebar-language {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 14px;
}

.sidebar-language label {
  color: var(--muted);
  font-size: 11px;
  white-space: nowrap;
}

.sidebar-language select {
  flex: 1;
  box-sizing: border-box;
  padding: 6px 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  font-family: inherit;
  font-size: 12px;
}

.sidebar-language select:focus-visible {
  outline: none;
  border-color: var(--accent);
}

/* A search query is active and this SECTION, ROW (inside an otherwise-matching, open section), or
   bare top-level unit (e.g. Print Settings Template) has no match — hidden outright at whatever
   granularity it applies to, so the user never scrolls past something they can't act on. A
   non-matching ROW used to dim in place instead (opacity 0.35) to preserve the section's
   surrounding context, but even at that opacity a muted-gray label against this near-black theme
   read as barely different from full brightness, and sat next to a full-brightness match it looked
   brighter than it was (simultaneous contrast) — confirmed by pixel-sampling the rendered page, not
   just by eye. Hiding removes the ambiguity; see Shell.optionSearch.attach's doc comment. */
.search-hidden {
  display: none;
}

/* ── Controls ── */
.ctrl {
  margin-bottom: 12px;
}

.ctrl label {
  display: block;
  color: var(--muted);
  font-size: 11px;
  margin-bottom: 5px;
}

.ctrl input[type="text"],
.ctrl input[type="number"],
.ctrl select,
.ctrl textarea {
  width: 100%;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  font-family: inherit;
  font-size: 13px;
  padding: 7px 10px;
  outline: none;
  transition: border-color 0.15s;
}

.ctrl textarea {
  resize: vertical;
  /* No min-height here — the element's `rows` attribute / an app override sizes it (e.g. RackBlank
     keeps a compact 2.4em). */
}

.ctrl input[type="text"]:focus,
.ctrl input[type="number"]:focus,
.ctrl select:focus,
.ctrl textarea:focus {
  border-color: var(--accent);
}

.ctrl select option {
  background: var(--surface);
}

.range-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.range-row input[type="range"] {
  flex: 1;
  -webkit-appearance: none;
  height: 4px;
  border-radius: 2px;
  background: var(--border);
  outline: none;
  cursor: pointer;
}

.range-row input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--accent);
  cursor: pointer;
  transition: transform 0.1s;
}

.range-row input[type="range"]::-webkit-slider-thumb:hover {
  transform: scale(1.2);
}

.range-row input[type="number"] {
  width: 58px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  font-family: inherit;
  font-size: 13px;
  padding: 5px 6px;
  outline: none;
  text-align: right;
  -moz-appearance: textfield;
}

.range-row input[type="number"]::-webkit-inner-spin-button,
.range-row input[type="number"]::-webkit-outer-spin-button {
  -webkit-appearance: none;
}

.range-row input[type="number"]:focus {
  border-color: var(--accent);
}

.range-row input:disabled,
.ctrl.disabled {
  opacity: 0.4;
}

/* Inline "auto" reset next to a derived range/number pair (e.g. a leg width/length that's normally
   computed from other settings until the user overrides it directly). */
.auto-row {
  display: flex;
  align-items: center;
  gap: 8px;
}

.auto-row .range-row {
  flex: 1;
}

.btn-auto {
  flex-shrink: 0;
  background: none;
  border: 1px solid var(--border);
  color: var(--muted);
  border-radius: 4px;
  cursor: pointer;
  padding: 4px 8px;
  font-size: 11px;
  line-height: 1;
  transition: color 0.15s, border-color 0.15s;
}

.btn-auto:hover {
  color: var(--text);
  border-color: var(--accent-dim);
}

.btn-auto.active {
  color: var(--accent);
  border-color: var(--accent);
}

.check-row {
  display: flex;
  align-items: center;
  gap: 9px;
  cursor: pointer;
  user-select: none;
  padding: 4px 0;
}

.check-row input[type="checkbox"] {
  width: 16px;
  height: 16px;
  accent-color: var(--accent);
  cursor: pointer;
  flex-shrink: 0;
}

.check-row .check-text {
  font-size: 12px;
  color: var(--text);
}

/* A hint sits AFTER its control, which has already spent its own margin-bottom getting clear of the
   hint. Without a bottom margin of its own the hint ends up nearer the NEXT control's label than the
   one it describes, and reads as belonging to the wrong control — obvious once most controls have a
   hint. The 12px matches .ctrl's own margin-bottom so the rhythm is unchanged. */
.check-hint {
  color: var(--muted);
  font-size: 10px;
  margin: 2px 0 12px 25px;
}

/* A description paragraph that describes a whole SECTION rather than one option — e.g. the Export
   blurb sitting under a bare .section-label, not a .ctrl/.check-row — so it isn't indented like a
   checkbox's sub-note. Same muted/small treatment as .check-hint; Shell.optionSearch's groupUnits
   recognizes both classes as searchable description text (see architecture/localization.md). */
.section-note {
  color: var(--muted);
  font-size: 10px;
  margin: 4px 0 8px;
}

/* ── Toggle switch (an alternative to .check-row for a prominent on/off) — moved here from
   CoasterGenerator, which was the only app using it; its checked-state colour was hardcoded to a
   fixed blue instead of var(--accent), the one control on the page that didn't follow the app's own
   accent. Fixed as part of the move. ── */
.toggle-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
.collapsible-section .section-content > .toggle-row:first-child { margin-top: 12px; }
.toggle-row-text { flex: 1; }
.toggle-row-text .toggle-title { color: var(--text); font-size: 12px; font-weight: 500; display: block; }
.toggle-row-text .toggle-desc { color: var(--muted); font-size: 10px; display: block; margin-top: 2px; }
.toggle-label { display: inline-flex; align-items: center; cursor: pointer; flex-shrink: 0; }
.toggle-input { display: none; }
.toggle-track { width: 38px; height: 21px; background: var(--border); border-radius: 11px; position: relative; transition: background 0.2s; }
.toggle-track::after { content: ''; position: absolute; top: 3px; left: 3px; width: 15px; height: 15px; border-radius: 50%; background: var(--muted); transition: transform 0.2s, background 0.2s; }
.toggle-input:checked + .toggle-track { background: var(--accent); }
.toggle-input:checked + .toggle-track::after { transform: translateX(17px); background: #fff; }

/* ── Upload / drop zones ── */
.upload-zone {
  border: 1.5px dashed var(--border);
  border-radius: 8px;
  padding: 14px;
  text-align: center;
  cursor: pointer;
  transition: border-color 0.15s, background 0.15s;
  position: relative;
}

.upload-zone:hover,
.upload-zone.drag-over {
  border-color: var(--accent);
  background: var(--accent-wash);
}

.upload-zone input[type="file"] {
  position: absolute;
  inset: 0;
  opacity: 0;
  cursor: pointer;
  width: 100%;
  height: 100%;
}

.upload-zone .upload-icon {
  font-size: 22px;
  color: var(--muted);
  margin-bottom: 4px;
}

.upload-zone .upload-text {
  color: var(--muted);
  font-size: 11px;
}

.upload-zone .upload-name {
  color: var(--accent);
  font-size: 11px;
  margin-top: 4px;
  display: none;
  word-break: break-all;   /* long SVG/font filenames wrap instead of overflowing the zone */
}

.upload-zone.has-file .upload-name {
  display: block;
}

.upload-zone.has-file .upload-text {
  display: none;
}

.upload-zone.upload-zone-sm {
  padding: 10px;
}

/* Shell.attachSvgLibraryPicker's "choose from library" trigger — inserted right after an SVG
   upload-zone. Always visible (unlike each app's own .btn-clear, which stays hidden until a file is
   loaded — this offers a SOURCE, it doesn't clear one), styled once here so no app needs its own CSS
   for it. See Libraries/svg-library-picker-ui.js for the popover itself. */
.shell-svglib-trigger {
  display: block;
  background: none;
  border: none;
  color: var(--accent);
  font-size: 11px;
  font-family: inherit;
  cursor: pointer;
  padding: 0;
  margin-top: 6px;
  text-align: left;
}

.shell-svglib-trigger:hover {
  text-decoration: underline;
}

/* ── Buttons ── */
/* .btn's own `display` (below) ties the UA stylesheet's [hidden]{display:none} on specificity and
   wins on source order, so a `hidden` .btn-classed button (e.g. #btn-warnings) would otherwise still
   render. */
.btn[hidden] {
  display: none;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  border: none;
  border-radius: 7px;
  font-family: inherit;
  font-size: 13px;
  cursor: pointer;
  padding: 9px 16px;
  transition: opacity 0.15s, transform 0.1s;
  user-select: none;
}

.btn:active {
  transform: scale(0.97);
}

.btn-primary {
  background: var(--accent);
  color: var(--bg);
  font-weight: 500;
  width: 100%;
}

.btn-primary:hover {
  opacity: 0.88;
}

.btn-secondary {
  background: var(--surface);
  color: var(--accent);
  border: 1px solid var(--accent);
  font-weight: 500;
  width: 100%;
}

.btn-secondary:hover {
  background: var(--accent);
  color: var(--bg);
}

/* .cfg-file-grid is the Configuration panel's Save/Load file pair (Shell.settings.attach). Same
   two-up layout as the export buttons, deliberately a DIFFERENT class — see the note there. */
.export-btn-grid,
.cfg-file-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
}

/* Single-column variant (e.g. RackBlank's three stacked full-width export buttons). The compound
   selector out-specifies the base rule, so it wins regardless of stylesheet load order. */
.export-btn-grid.one-col {
  grid-template-columns: 1fr;
}

.export-btn-grid .btn,
.cfg-file-grid .btn {
  width: 100%;
}

.btn-ghost {
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  padding: 7px 14px;
  font-size: 12px;
}

.btn-ghost:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.btn-sm {
  font-size: 11px;
  padding: 4px 9px;
}

/* ── Configuration panel (Shell.settings.attach — save/load settings to a file and to named
   browser configs). Rendered once by the shell into a <details> section; consumed by every
   generator, so it lives here, not per-app. ── */

/* Named-save row: the standard .ctrl input beside its Save button. */
.cfg-save-row {
  display: flex;
  gap: 6px;
}
.cfg-save-row input[type="text"] {   /* look inherited from .ctrl input[type=text] */
  flex: 1;
  min-width: 0;
}
.cfg-save-row .btn {                  /* beats .btn-secondary { width: 100% } */
  flex-shrink: 0;
  width: auto;
}

/* This app's saved configs — same surface/border rhythm as MyPreferences' .tpl-item. */
.cfg-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
  margin-bottom: 12px;
}
.cfg-list:empty {
  display: none;
}
.cfg-item {
  display: flex;
  align-items: center;
  gap: 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 5px 6px 5px 10px;
}
.cfg-item .cfg-name {
  flex: 1;
  min-width: 0;
  font-size: 12px;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cfg-item .cfg-load {
  flex-shrink: 0;
}
.cfg-del {
  flex-shrink: 0;
  background: none;
  border: none;
  color: var(--muted);
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  padding: 4px 6px;
  border-radius: 4px;
}
.cfg-del:hover {
  color: var(--danger);
}
.cfg-empty {
  color: var(--muted);
  font-size: 11px;
  font-style: italic;
  margin-bottom: 12px;
}
.cfg-empty[hidden] {
  display: none;
}
.cfg-file-label {
  color: var(--muted);
  font-size: 10px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  margin: 0 0 6px;
}

/* Drop a .json onto the OPEN Configuration section to load it — no dedicated dashed zone. */
.collapsible-section.drag-over {
  border-color: var(--accent);
  background: var(--accent-wash);
}

/* Segmented control (e.g. alignment / mode toggles) */
.align-group {
  display: flex;
  gap: 0;
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
}

.align-btn {
  flex: 1;
  background: var(--surface);
  /* NOT var(--muted) (#6b7080 on --surface #1e2128 measures ~3.3:1 — below WCAG AA's 4.5:1 floor for
     12px text, caught by ui-design-critic). This is a clickable control (the Finished/Build
     Plate/Debug view switch), not de-emphasized hint text, so it gets its own brighter idle colour
     (~5:1) instead of dimming the shared --muted used for genuinely secondary text elsewhere. */
  color: #8a90a0;
  border: none;
  font-family: inherit;
  font-size: 12px;
  padding: 7px 10px;
  cursor: pointer;
  transition: background 0.15s, color 0.15s;
}

.align-btn + .align-btn {
  border-left: 1px solid var(--border);
}

/* Plate navigator (Shell.plates) — floating chip bar over the viewport, shown only in Build Plate
   view when the app registered >1 plate group. Reuses the .align-group segmented vocabulary; sits
   below popovers/drawer (z 60+) but above the canvas. */
.plate-nav {
  position: absolute;
  top: 12px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 20;
  background: var(--surface);
}
.plate-nav[hidden] { display: none; }

.align-btn:hover {
  color: var(--text);
}

.align-btn.active {
  background: var(--accent);
  color: var(--bg);
  font-weight: 500;
}

.divider {
  border: none;
  border-top: 1px solid var(--border);
  margin: 16px 0;
}

/* ── Legacy colour row (native <input type=color>) — kept as a fallback for the ColorPicker
   popover, which relocates + hides this input into its Palette pane. ── */
.color-row {
  display: flex;
  align-items: center;
  gap: 10px;
}

.color-row input[type="color"] {
  -webkit-appearance: none;
  appearance: none;
  width: 48px;
  height: 30px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--surface);
  cursor: pointer;
  padding: 3px;
  flex-shrink: 0;
  transition: border-color 0.15s;
}

.color-row input[type="color"]:hover {
  border-color: var(--accent);
}

.color-row input[type="color"]::-webkit-color-swatch-wrapper {
  padding: 0;
}

.color-row input[type="color"]::-webkit-color-swatch {
  border: none;
  border-radius: 3px;
}

.color-hex {
  color: var(--muted);
  font-size: 11px;
}

/* ── Drag-to-reorder rows (Libraries/drag-reorder.js) ──
   The handle (⠿) any reorderable list row carries. Pointer Events drive the drag, so
   touch-action:none keeps a touch drag from scrolling the sidebar; the handle is focusable so
   ArrowUp/ArrowDown reorder without a pointer. Shared here because more than one generator now
   ships a reorderable list (WordEasel tiles, CurveGenerator notes). */
.tile-drag-handle {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: 1px solid var(--border);
  color: var(--muted);
  border-radius: 4px;
  cursor: grab;
  padding: 4px 6px;
  font-size: 13px;
  line-height: 1;
  touch-action: none;
  user-select: none;
  transition: color 0.15s, border-color 0.15s;
}

.tile-drag-handle:hover {
  color: var(--text);
  border-color: var(--accent-dim);
}

.tile-drag-handle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

/* The row lifted while dragging, and a global grabbing cursor for the whole gesture. */
.dr-row.dragging {
  opacity: 0.55;
  outline: 1px dashed var(--accent);
  outline-offset: 2px;
}

body.dr-dragging,
body.dr-dragging * {
  cursor: grabbing !important;
}

/* ── "+ Add …" row trigger — a dashed, full-width button that appends a row/tile/unit to a list.
   Five generators (WordEasel, CableTag, LaptopStand, DrawerUnits, DrawerLabels) had independently
   authored byte-identical copies of this before it was promoted here — the same drift risk
   `.tile-drag-handle` above was promoted to avoid. `.add-btn-grid` is for the two-button case
   (WordEasel's "+ Add word tile" / "+ Add SVG tile", CableTag's tag/pair pair); a single button
   just wants `width: 100%` from `.add-btn` itself. */
.add-btn {
  width: 100%;
  padding: 7px;
  background: none;
  border: 1px dashed var(--border);
  color: var(--muted);
  border-radius: 5px;
  cursor: pointer;
  font-family: inherit;
  font-size: 11px;
  transition: border-color 0.15s, color 0.15s;
}

.add-btn:hover {
  border-color: var(--accent-dim);
  color: var(--accent);
}

.add-btn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.add-btn-grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
}

/* ── Viewport + toolbar ── */
.viewport-wrap {
  flex: 1;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--bg);
}

#viewport {
  flex: 1;
  display: block;
  position: relative;
  overflow: hidden;
}

#viewport canvas {
  display: block;
  width: 100% !important;
  height: 100% !important;
}

.toolbar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 8px;
  padding: 10px 16px;
  border-top: 1px solid var(--border);
  background: var(--sidebar);
  flex-shrink: 0;
}

.toolbar .dims-info {
  flex: 1;
  color: var(--muted);
  font-size: 11px;
}

/* Printability-warnings toggle (Shell.toolbar) — a --danger-toned variant of .btn-ghost so it reads
   as an alert among the neutral toolbar buttons; hidden by default, shown by setWarnings() only when
   there's something to report. */
.btn-warning {
  background: var(--surface);
  color: var(--danger);
  border: 1px solid var(--danger);
}

.btn-warning:hover {
  background: rgba(224, 90, 90, 0.12);
}

/* The warnings popover reuses .cp-popover/.cp-head/.cp-title/.cp-close (below) for its chrome —
   this only styles its own content, a simple scrollable list. */
.toolbar-warn-list {
  list-style: none;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.toolbar-warn-list li {
  font-size: 12px;
  color: var(--text);
  line-height: 1.4;
  padding-left: 18px;
  position: relative;
}

.toolbar-warn-list li::before {
  content: '⚠';
  position: absolute;
  left: 0;
  color: var(--danger);
}

/* ── Loading overlay + toast ── */
/* Background is NEAR-opaque, not merely dimmed: a slow/OCC-heavy rebuild (e.g. a coaster holder's
   revolve) is visibly mid-geometry underneath while this is up, and at the old 0.65 alpha a bright
   in-progress mesh against the dark scene background still showed a discernible, distorted
   silhouette through the overlay — reported as "the holder renders as a broken spiral shape" when
   it was actually just an incomplete build, still visible through a loading mask meant to hide it.
   0.97 reads as fully solid without literally being opacity:1 (keeps the transition from looking
   like a hard cut). */
#loading-overlay {
  position: absolute;
  inset: 0;
  background: rgba(14, 15, 17, 0.97);
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.2s;
  z-index: 10;
}

#loading-overlay.visible {
  opacity: 1;
  pointer-events: all;
}

.spinner {
  width: 32px;
  height: 32px;
  border: 3px solid var(--border);
  border-top-color: var(--accent);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
}

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

#toast {
  position: fixed;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%) translateY(20px);
  background: #2a1e1e;
  border: 1px solid var(--danger);
  color: var(--danger);
  padding: 10px 18px;
  border-radius: 8px;
  font-size: 12px;
  opacity: 0;
  transition: opacity 0.2s, transform 0.2s;
  z-index: 100;
  pointer-events: none;
  max-width: 400px;
  text-align: center;
}

#toast.visible {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}

/* Success variant (folds in MyPreferences' green "saved" toast). */
#toast.ok {
  background: #17251b;
  border-color: #4bbf73;
  color: #7fd6a0;
}

/* ── Debug cache dump table ── */
.debug-stats {
  margin-top: 10px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 4px;
  display: none;
  overflow-x: auto;
}

.debug-stats.visible {
  display: block;
}

.cache-table {
  border-collapse: collapse;
  width: 100%;
  font-size: 10px;
  color: var(--muted);
}

.cache-table th,
.cache-table td {
  padding: 4px 6px;
  text-align: left;
  border-bottom: 1px solid var(--border);
  vertical-align: top;
  white-space: nowrap;
}

.cache-table th {
  color: var(--accent);
  font-weight: 600;
  position: sticky;
  top: 0;
  background: var(--surface);
}

.cache-table td.cache-table-name {
  white-space: normal;
  min-width: 220px;
  color: var(--text);
}

.cache-table tbody tr:last-child td {
  border-bottom: none;
}
/* NOTE: `.debug-total` is NOT defined here — RackBlank's is a bespoke red "grand total" warning
   banner (its own colours + a .visible toggle), not a generic shell control, so it stays app-side. */

/* ════════════════════════════════════════════════════════════════════════════════════════════════
   ColorPicker popover — folded in from the retired Libraries/color-picker.css. Collapses each colour
   row to a swatch trigger; clicking opens ONE popover (Palette / My Filaments). Appended to <body>,
   so it themes from the app's :root palette vars. z-index 70/65 stays ABOVE the mobile drawer (60)
   and its scrim (50) — anything lower renders BEHIND the drawer that spawned it.
   ════════════════════════════════════════════════════════════════════════════════════════════════ */

/* ── Resting trigger (one per Colors row) ── */
.cp-trigger {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  box-sizing: border-box;
  min-height: 40px;
  padding: 6px 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  font-family: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color 0.15s;
}
.cp-trigger:hover { border-color: var(--accent); }
.cp-trigger:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.cp-swatch {
  flex-shrink: 0;
  width: 34px;
  height: 22px;
  border-radius: 4px;
  border: 1px solid rgba(255, 255, 255, 0.16);
}
/* AA contrast fix on the trigger's hex read-out — the app's own .color-hex renders at --muted
   (~3.26:1 on --surface); on this new control we render it at --text. */
.cp-trigger .color-hex { color: var(--text); font-size: 11px; }
.cp-chip {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--muted);
  font-size: 11px;
}
.cp-chip[hidden] { display: none; }
.cp-chevron { flex-shrink: 0; margin-left: auto; color: var(--muted); font-size: 12px; }

/* ── Backdrop: full-viewport click-catcher. Transparent on desktop (must not dim the 3D preview
   the user is choosing against); dimmed as a sheet on mobile. z-index 65 sits ABOVE the mobile
   drawer (60) and its scrim (50) — see mobile-layout.css. ── */
.cp-backdrop { position: fixed; inset: 0; z-index: 65; background: transparent; }
.cp-backdrop[hidden] { display: none; }

/* ── Popover ── */
.cp-popover {
  position: fixed;
  z-index: 70;                 /* above the drawer + backdrop */
  width: 300px;                /* narrower than --sidebar-w so it doesn't read as a bigger surface */
  box-sizing: border-box;
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  overflow: hidden;            /* the popover itself never scrolls — only its swatch grid does (below),
                                  so the header/tabs/search stay pinned and nothing clips off-screen */
  background: var(--sidebar);
  border: 1px solid var(--border);
  border-radius: 10px;
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.5);
  color: var(--text);
  font-family: inherit;
}
.cp-popover[hidden] { display: none; }

.cp-head { display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
.cp-title { font-size: 12px; font-weight: 500; color: var(--text); }
.cp-close {
  background: none;
  border: none;
  color: var(--muted);
  font-size: 15px;
  line-height: 1;
  padding: 2px 6px;
  border-radius: 5px;
  cursor: pointer;
}
.cp-close:hover { color: var(--text); }
.cp-close:focus-visible { outline: 2px solid var(--accent); }

/* Mode tabs — inactive text at --text (NOT --muted) to clear WCAG AA. */
.cp-tabs {
  display: inline-flex;
  align-self: flex-start;
  flex-shrink: 0;
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
}
.cp-tab {
  background: var(--surface);
  color: var(--text);
  border: none;
  font-family: inherit;
  font-size: 11px;
  padding: 6px 12px;
  cursor: pointer;
}
.cp-tab + .cp-tab { border-left: 1px solid var(--border); }
.cp-tab-active { background: var(--accent); color: #111; font-weight: 500; }
.cp-tab:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

.cp-pane[hidden] { display: none; }
/* Both panes are a CONSTANT height, so switching Palette↔My Filaments never resizes the popover —
   and because the popover never changes size, it never moves (position is set once, at open). */
.cp-pane { height: 244px; display: flex; flex-direction: column; min-height: 0; }
.cp-pane-fil { gap: 0; }
.cp-pane-palette { gap: 10px; }

/* Palette pane: a THEMED HSV picker — saturation/brightness box + hue slider + hex field — matching
   the app's dark theme (no native OS colour dialog). */
.cp-sv {
  position: relative;
  width: 100%;
  flex: 1 1 auto;
  min-height: 120px;
  border: 1px solid var(--border);
  border-radius: 8px;
  cursor: crosshair;
  touch-action: none;               /* the SV box owns drag gestures, not the page */
}
.cp-sv:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.cp-sv-handle {
  position: absolute;
  width: 12px; height: 12px;
  margin: -6px 0 0 -6px;
  border-radius: 50%;
  border: 2px solid #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55);
  pointer-events: none;
}
.cp-hue {
  position: relative;
  width: 100%;
  height: 14px;
  flex-shrink: 0;
  border: 1px solid var(--border);
  border-radius: 7px;
  cursor: pointer;
  touch-action: none;
  background: linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
}
.cp-hue:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.cp-hue-handle {
  position: absolute;
  top: 50%;
  width: 12px; height: 12px;
  margin: -6px 0 0 -6px;
  border-radius: 50%;
  border: 2px solid #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55);
  pointer-events: none;
}
.cp-hexrow { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
.cp-hexhash { color: var(--muted); font-size: 12px; }
.cp-hex {
  flex: 1;
  min-width: 0;
  padding: 6px 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  font-family: inherit;
  font-size: 12px;
  text-transform: lowercase;
}
.cp-hex:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* Filaments pane */
.cp-search {
  width: 100%;
  box-sizing: border-box;
  flex-shrink: 0;
  margin-bottom: 8px;
  padding: 6px 8px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text);
  font-family: inherit;
  font-size: 12px;
}
.cp-filters { display: flex; flex-shrink: 0; gap: 6px; margin-bottom: 8px; }
.cp-filters select {
  flex: 1;
  min-width: 0;
  padding: 5px 6px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 5px;
  color: var(--text);
  font-family: inherit;
  font-size: 11px;
}
.cp-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(34px, 1fr));
  gap: 6px;
  flex: 1 1 auto;             /* fills the pane's fixed height and scrolls — the ONLY scroll region */
  min-height: 0;
  align-content: start;      /* pack rows at the top (no stretched gaps when there are few swatches) */
  overflow-y: auto;
  /* Padding gives the selection/focus ring + hover-scale room INSIDE the scroll viewport, so a
     top-row swatch's highlight is never clipped against the search box above it. */
  padding: 4px;
}
.cp-sw {
  position: relative;
  width: 100%;
  padding-bottom: 100%;        /* square */
  border-radius: 6px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  cursor: pointer;
  transition: transform 0.08s, box-shadow 0.08s;
}
.cp-sw:hover { transform: scale(1.08); }
/* INSET ring (not an outer box-shadow) so the selected swatch's highlight stays within its own box and
   can never overlap/clip the search box or a neighbouring swatch. Shape cue, not colour-only. */
.cp-sw.sel { box-shadow: inset 0 0 0 2px var(--accent); border-color: var(--accent); }
.cp-sw:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.cp-empty { color: var(--text); font-size: 12px; line-height: 1.5; }
.cp-empty a { color: var(--accent); }
.cp-count { margin-top: 8px; color: var(--muted); font-size: 10px; }
.cp-selected { margin-top: 4px; min-height: 14px; color: var(--text); font-size: 11px; }

/* ── Mobile: bottom sheet + 44px touch targets. The @media list MUST match MobileUI.MEDIA_QUERY. ── */
@media (max-width: 768px), (max-height: 500px) and (pointer: coarse) {
  .cp-backdrop { background: rgba(0, 0, 0, 0.5); }
  .cp-popover.cp-sheet {
    left: 0;
    right: 0;
    bottom: 0;
    top: auto;
    width: auto;
    max-height: 70vh;
    border-radius: 14px 14px 0 0;
  }
  .cp-trigger { min-height: 44px; }
  .cp-tab { min-height: 44px; padding: 0 16px; }
  .cp-close { min-width: 44px; min-height: 44px; }
  .cp-grid { grid-template-columns: repeat(auto-fill, minmax(44px, 1fr)); }
  /* 16px so iOS Safari doesn't auto-zoom the page when these focus (same rule mobile-layout.css uses). */
  .cp-search, .cp-filters select, .cp-hex { font-size: 16px; }
  /* Configuration panel: 44px touch targets for the per-config Load / delete controls. */
  .cfg-item { padding: 8px 8px 8px 12px; }
  .cfg-item .cfg-load { min-height: 44px; }
  .cfg-del { min-width: 44px; min-height: 44px; }
}

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

/* ── Debug view: stage picker (Shell.debugView.layoutGrid builds it) ───────────────────────────
   Top-right of the viewport, clear of the edge/face readout which lives bottom-left. One copy here
   so no app carries its own. Hidden outside the Debug view by Shell.debugView itself. */
.debug-stage-picker { position: absolute; top: 12px; right: 12px; z-index: 20; }
.debug-stage-picker[hidden] { display: none; }
.debug-stage-picker select {
  display: block; max-width: min(360px, 40vw); padding: 6px 10px; border-radius: 8px;
  background: rgba(10, 12, 16, .88); color: #e3e6eb; border: 1px solid #2a2f37;
  font: 12px/1.4 ui-monospace, Menlo, Consolas, monospace;
}

/* ── Dimension diagram (Libraries/dim-diagram.js) ──────────────────────────────────────────────
   A live drawing of the part with a clickable pill per dimension, at the top of a section. Colors are
   all tokens (light/dark and the mobile layer need nothing extra); the pills are the real focusable
   buttons, the dimension lines are decorative and never take pointer events. Window Shelf's Window
   Grip is the reference. `hidden-callout` = the callout's control is off, so it is not drawn. */
.dim-figure { margin: 12px 0 8px; }
.dim-figure[hidden] { display: none; }
.dim-svg { display: block; width: 100%; height: auto; overflow: visible; }
.dim-svg text { font-family: var(--font-ui, 'DM Mono', monospace); user-select: none; -webkit-user-select: none; }
.dg-body { fill: var(--surface); stroke: var(--muted); stroke-width: 1.2; stroke-linejoin: round; }
.dg-accent { fill: var(--accent-dim); stroke: var(--muted); stroke-width: 1; stroke-linejoin: round; }
.dg-edge { fill: none; stroke: var(--muted); stroke-width: 1.2; stroke-linejoin: round; }
.dg-soft { fill: var(--accent-dim); fill-opacity: 0.35; stroke: var(--muted); stroke-width: 1; stroke-linejoin: round; }
.dg-frame { fill: none; stroke: var(--border); stroke-width: 1; }
.dg-ghost { fill: none; stroke: var(--muted); stroke-width: 1; stroke-dasharray: 4 3; stroke-linejoin: round; opacity: 0.85; }
.dg-center { fill: var(--accent); }
.dg-dim { color: var(--muted); pointer-events: none; }
.dg-dim[data-color="red"], .dg-pill[data-color="red"] { color: var(--danger); }
.dg-dim[data-color="green"], .dg-pill[data-color="green"] { color: var(--callout-green); }
.dg-dim line, .dg-dim polyline, .dg-dim path { fill: none; stroke: currentColor; stroke-width: 1; }
.dg-dim .dg-ext { stroke-dasharray: 2 2; opacity: 0.7; }
.dg-dim .dg-arrow { fill: currentColor; stroke: none; }
.dg-dim.active line, .dg-dim.active polyline, .dg-dim.active path { stroke-width: 1.8; }
.dg-pill { color: var(--muted); cursor: pointer; outline: none; }
.dg-pill .dg-pill-bg { fill: var(--surface); stroke: currentColor; stroke-width: 1; }
.dg-pill .dg-name { fill: var(--muted); font-size: 10.5px; letter-spacing: 0.02em; }
.dg-pill .dg-val { fill: var(--text); font-size: 13px; font-weight: 500; }
.dg-pill[data-color="red"] .dg-val, .dg-pill[data-color="green"] .dg-val { fill: currentColor; }
.dg-pill:hover .dg-pill-bg, .dg-pill.active .dg-pill-bg { stroke: var(--accent); stroke-width: 2; fill: var(--accent-wash); }
.dg-pill[data-color="red"]:hover .dg-pill-bg, .dg-pill[data-color="red"].active .dg-pill-bg,
.dg-pill[data-color="green"]:hover .dg-pill-bg, .dg-pill[data-color="green"].active .dg-pill-bg { stroke: currentColor; stroke-width: 2.4; }
.dg-pill:focus-visible .dg-pill-bg { stroke: var(--accent); stroke-width: 2.6; }
.dg-pill[hidden-callout], .dg-dim[hidden-callout] { display: none; }
/* The brief highlight the control gets after its callout is clicked. */
@keyframes ctrl-flash { 0% { box-shadow: 0 0 0 2px var(--accent); background: var(--accent-wash); } 100% { box-shadow: 0 0 0 2px transparent; background: transparent; } }
.ctrl.callout-flash { border-radius: 6px; animation: ctrl-flash 1.4s ease-out 1; }

/* Print Settings Template section (Shell.templates.mount renders it; hand-written copies use the
   same classes). */
.section-label-qualifier { font-weight: 400; opacity: .6; }
.template-saved-select { width: 100%; margin-top: 8px; }
.template-info-row { display: flex; align-items: center; justify-content: space-between; margin-top: 6px; }
.slicer-badge { font-size: 11px; font-weight: 600; border-radius: 4px; padding: 2px 7px; background: var(--accent); color: var(--bg); }
.template-slicer-picker { margin-top: 6px; font-size: 12px; color: var(--muted); }
.template-slicer-picker label { display: inline-flex; align-items: center; gap: 4px; margin-right: 10px; cursor: pointer; }
