/* ---------- reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
  font-family: "Hanken Grotesk", ui-sans-serif, system-ui, -apple-system, sans-serif;
  font-size: 14px;
  line-height: 1.45;
  /* Millimetres and money line up down every column in this interface, so the
     figures are tabular everywhere rather than being switched on per rule. */
  font-variant-numeric: tabular-nums;
  color: var(--fg);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
button { font: inherit; color: inherit; }

/* ---------- theme ---------- */
/* The piece stands in a room, and the panels are paper laid on its wall. That
   is the whole colour idea, and it decides the one unusual thing here: there is
   no brand hue. Every colour a customer sees on screen belongs either to a
   decor they picked or to whether something is valid. A blue accent would
   compete with the oak and the yellow front, and imply a colour that is not for
   sale — so a selected thing is filled with ink and carries more weight
   instead.

   `--accent` is kept as a name because forty rules already reach for it; it
   simply resolves to ink now. */
:root {
  color-scheme: light dark;

  --room: #ada69c;
  --paper: #f7f5f2;
  --paper-2: #ebe8e3;
  --rule: #d6d1ca;
  --ink: #22201d;
  --ink-dim: #736e67;

  --fault: #a6342a;
  --sound: #2f6b4f;

  /* Legacy names, remapped. */
  --bg: var(--room);
  --panel: var(--paper);
  --panel-2: var(--paper-2);
  --border: var(--rule);
  --line: var(--rule);
  --fg: var(--ink);
  --fg-dim: var(--ink-dim);
  --accent: var(--ink);
  --accent-2: var(--ink-dim);
  --accent-fg: var(--paper);
  --ghost-ok: var(--sound);
  --ghost-bad: var(--fault);

  /* What the viewport outlines a selection in, so the band you are editing and
     the board highlighted in the model are plainly the same thing. Taken from
     the outline pass's own colour (render/mod.rs), converted out of the linear
     space the shader writes it in. */
  --pick: #9edaff;

  /* No drop shadows. Depth is paper against the room, not a glow under a card
     — a uniform shadow beneath every panel is the tell that turns a
     configurator into a template. */
  --shadow: none;
  --radius: 8px;
  --sidebar-w: 288px;
  --inspector-w: 320px;
}

/* Dark is the option, not the default: this is furniture for a home, looked at
   in daylight, and the decors have to read true. */
@media (prefers-color-scheme: dark) {
  :root {
    --room: #2a2723;
    --paper: #1b1917;
    --paper-2: #232120;
    --rule: #3a3633;
    --ink: #f0ece6;
    --ink-dim: #9c958c;
    --fault: #e0776b;
    --sound: #7fc4a0;
    --accent-fg: #1b1917;
  }
}

/* ---------- app shell ---------- */
/* Three real columns. The inspector track collapses to zero when nothing is
   selected, so the viewport owns exactly the space between the two panels and
   the wgpu surface resizes with it (rather than being overlapped). The track
   width is not transitioned on purpose: every width change reconfigures the
   surface and reallocates the MSAA + depth targets. */
.app {
  display: grid;
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr) 0px;
  height: 100dvh;
  width: 100vw;
  overflow: hidden;
}
.app--inspector-open {
  grid-template-columns: var(--sidebar-w) minmax(0, 1fr) var(--inspector-w);
}

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

.sidebar__header {
  padding: 14px 16px;
  border-bottom: 1px solid var(--border);
  display: flex;
  align-items: center;
  gap: 4px;
}
.sidebar__history {
  flex-shrink: 0;
  width: 26px;
  height: 26px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--panel-2);
  color: var(--fg-dim);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  transition: color 100ms, border-color 100ms;
}
.sidebar__history:hover:not(:disabled) {
  border-color: var(--accent);
  color: var(--accent);
}
.sidebar__history:disabled {
  opacity: 0.3;
  cursor: not-allowed;
}
.sidebar__title {
  margin: 0;
  flex: 1;
  font-size: 15px;
  font-weight: 600;
  color: var(--ink);
}
.sidebar__body {
  padding: 12px;
  overflow: auto;
  flex: 1 1 auto;
}

/* ---------- palette ---------- */
.sidebar__section-label {
  font-size: 12px;
  font-weight: 600;
  color: var(--fg-dim);
  padding-bottom: 6px;
}
/* One notch smaller and un-bolded: nested under .sidebar__section-label,
   grouping the palette into Elements vs Structure. */
.sidebar__subsection-label {
  font-size: 10px;
  font-weight: 500;
  color: var(--fg-dim);
  opacity: 0.75;
  padding: 4px 0;
}
.palette {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  margin-bottom: 10px;
}
.palette + .sidebar__subsection-label {
  margin-top: 8px;
}

.palette-chip {
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 14px 10px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  cursor: grab;
  user-select: none;
  transition: border-color 120ms ease, transform 120ms ease, background 120ms ease;
  touch-action: none;
}
.palette-chip:hover {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, var(--panel-2));
}
.palette-chip:active,
.palette-chip[data-dragging="true"] {
  cursor: grabbing;
  transform: scale(0.98);
  border-color: var(--accent-2);
}

.palette-chip__icon {
  width: 48px;
  height: 28px;
  border: 2px solid currentColor;
  border-radius: 2px;
  position: relative;
  overflow: hidden;
  color: var(--fg-dim);
}
.palette-chip:hover .palette-chip__icon { color: var(--accent); }

/* Each icon is the same box; the marks inside say which element it is. */
.palette-chip__icon--shelves {
  background: repeating-linear-gradient(to bottom, transparent 0 7px, currentColor 7px 9px);
}
.palette-chip__icon--drawers {
  background: repeating-linear-gradient(to bottom, currentColor 0 6px, transparent 6px 8px);
}
.palette-chip__icon--doors::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 50%;
  width: 2px;
  background: currentColor;
  transform: translateX(-50%);
}
.palette-chip__icon--rail::after {
  content: "";
  position: absolute;
  inset: 35% 10%;
  border-top: 3px solid currentColor;
  border-radius: 3px;
}
/* Dashed = a divider you are about to place, not one that exists. */
.palette-chip__icon--split-h::after {
  content: "";
  position: absolute;
  inset: 50% 0 auto 0;
  border-top: 2px dashed currentColor;
  transform: translateY(-50%);
}
.palette-chip__icon--split-v::after {
  content: "";
  position: absolute;
  inset: 0 auto 0 50%;
  border-left: 2px dashed currentColor;
  transform: translateX(-50%);
}
.palette-chip__label {
  font-size: 12px;
  font-weight: 500;
}

/* ---------- canvas viewport ---------- */
.viewport {
  position: relative;
  /* Flat. A vertical gradient behind a product is stage lighting that is not
     there, and it tints the decors it sits behind. */
  background: var(--room);
  min-width: 0;
  min-height: 0;
  height: 100%;
  overflow: hidden;
}
.viewport__canvas {
  display: block;
  width: 100%;
  height: 100%;
  outline: none;
  touch-action: none;
}

.viewport__overlay {
  position: absolute;
  inset: 0;
  pointer-events: none;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 12px;
  /* toolbar is absolutely positioned within overlay */
}
.status-pill {
  align-self: flex-start;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 4px 10px;
  font-size: 12px;
  color: var(--fg-dim);
  box-shadow: var(--shadow);
}
.status-pill[data-state="ok"] { color: var(--ghost-ok); border-color: var(--ghost-ok); }
.status-pill[data-state="bad"] { color: var(--ghost-bad); border-color: var(--ghost-bad); }

/* ---------- viewport menu (mobile only) ---------- */
.viewport__menu {
  position: absolute;
  top: 12px;
  left: 12px;
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--fg);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  pointer-events: all;
  box-shadow: var(--shadow);
}

/* Dismisses the mobile sidebar drawer. */
.scrim {
  position: fixed;
  inset: 0;
  z-index: 35;
  background: rgba(0, 0, 0, 0.45);
}
/* The real file picker, opened by the "Open plan" button beside it. Hidden
   rather than removed: it is what actually holds the OS dialog, and a
   `display: none` input still opens on `.click()`. */
.sidebar__file {
  display: none;
}

/* ---------- nav cube ---------- */
.nav-cube {
  position: absolute;
  top: 28px;
  right: 28px;
  width: 64px;
  height: 64px;
  pointer-events: all;
  z-index: 50;
}
.nav-cube__scene {
  width: 100%;
  height: 100%;
  perspective: 180px;
}
.nav-cube__cube {
  width: 64px;
  height: 64px;
  position: relative;
  transform-style: preserve-3d;
  /* transform driven by RAF loop */
}
.nav-cube__face {
  position: absolute;
  width: 64px;
  height: 64px;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 9px;
  font-weight: 700;
  /* The edge hairline is an inset shadow rather than a border, because a
     border is separate geometry outside the background box: rotated in a
     preserve-3d context it rasterizes and stretches with the face, and a 1px
     edge smears into a pale bar down the side of the cube, worse the further
     the face turns away. It also sized the faces wrong — with content-box the
     bordered face is 66px against a translateZ that assumes 64, so every face
     overhung its neighbours. An inset shadow paints inside the background box
     and changes neither the geometry nor the size. */
  box-shadow: inset 0 0 0 1px rgba(255,255,255,0.18);
  cursor: pointer;
  user-select: none;
  transition: filter 80ms;
  backface-visibility: visible;
  /* Neutral. The faces used to be blue, green and orange — the only saturated
     chrome left on screen once the accent went, and it sat directly above a
     piece of furniture whose own colours are what the customer is choosing
     between. A view-picker does not need to be the brightest thing in the room.

     One shade for all six. They used to come in a near pair and a far pair, to
     say which way the cube was turned, but the pairing was fixed per face
     rather than read off the current rotation: spin the cube and a far-shaded
     face comes to the front, so the nearest face was the palest and the most
     see-through. The rotation already says which way it is turned. */
  background: rgba(34,32,29,0.86);
  color: #fff;
}
.nav-cube__face:hover { filter: brightness(1.35); }
/* Nothing but geometry below — the shared look lives in the rule above. */
.nav-cube__face--front  { transform: rotateY(0deg)    translateZ(32px); }
.nav-cube__face--back   { transform: rotateY(180deg)  translateZ(32px); }
.nav-cube__face--right  { transform: rotateY(90deg)   translateZ(32px); }
.nav-cube__face--left   { transform: rotateY(-90deg)  translateZ(32px); }
.nav-cube__face--top    { transform: rotateX(90deg)   translateZ(32px); }
.nav-cube__face--bottom { transform: rotateX(-90deg)  translateZ(32px); }

/* ---------- drag chip (follows the cursor) ---------- */
.drag-ghost {
  position: fixed;
  pointer-events: none;
  background: var(--panel);
  border: 1px solid var(--accent);
  border-radius: 6px;
  padding: 6px 10px;
  font-size: 12px;
  color: var(--fg);
  box-shadow: var(--shadow);
  transform: translate(-50%, -50%);
  z-index: 9999;
  opacity: 0.95;
  white-space: nowrap;
  max-width: 60vw;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Matches the red cell ghost in the viewport: the drop is allowed, but it
   produces a design that cannot be exported. */
.drag-ghost--bad {
  border-color: var(--ghost-bad);
  color: var(--ghost-bad);
}

/* ---------- mobile ---------- */
/* Below this width neither panel can afford a column: the sidebar becomes a
   left slide-over and the inspector a bottom sheet, both over a full-bleed
   canvas. Camera control is touch-driven (see ui/canvas.rs `Gesture`). */
@media (max-width: 860px) {
  .app,
  .app--inspector-open {
    grid-template-columns: minmax(0, 1fr);
  }

  .viewport__menu { display: flex; }

  .sidebar {
    position: fixed;
    inset: 0 auto 0 0;
    width: min(320px, 85vw);
    z-index: 40;
    border-right: 1px solid var(--border);
    box-shadow: var(--shadow);
    transform: translateX(-100%);
    transition: transform 180ms ease;
  }
  .app--sidebar-open .sidebar { transform: none; }

  .panel {
    position: fixed;
    inset: auto 0 0 0;
    height: 45dvh;
    max-height: 85dvh;
    z-index: 45;
    border-left: none;
    border-top: 1px solid var(--border);
    border-radius: 14px 14px 0 0;
    box-shadow: 0 -4px 24px rgba(0,0,0,0.35);
    animation: sheet-in 180ms ease;
    transition: height 160ms ease;
  }
  .panel--expanded { height: 85dvh; }
  /* While the grip is held the height is written inline every pointermove;
     transitioning it would make the sheet lag behind the finger. */
  .panel--dragging { transition: none; }
  .panel__grip { display: block; }

  /* The pop-out has no sidebar to sit beside, so it becomes a sheet of its
     own, over the panel that opened it. */
  .pop {
    position: fixed;
    inset: auto 8px 8px;
    top: auto !important;
    width: auto;
    max-height: 70dvh;
    z-index: 50;
  }

  /* Keep the cube clear of the sheet and give it a thumb-sized hit area. */
  .nav-cube { top: 16px; right: 16px; }

  /* Comfortable touch targets, and 16px inputs so iOS does not zoom on focus. */
  input,
  select { font-size: 16px; }
  .ghostbtn,
  .addbtn,
  .export-btn { min-height: 44px; }
  .band { min-height: 48px; }
}

@keyframes sheet-in {
  from { transform: translateY(100%); }
  to { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .panel,
  .pop { animation: none; }
  .sidebar { transition: none; }
}

/* ---------- cost estimate ---------- */
/* Board cost, under the carcass editor. The total reads first and largest;
   the per-material lines and the caveat are supporting detail. */
.cost {
  padding-bottom: 12px;
}
.cost__totals {
  padding-bottom: 6px;
}
.cost__total {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
}
/* What you actually pay leads; the area figure sits under it as context. */
.cost__total--lead .cost__total-value {
  font-size: 18px;
}
.cost__total-value--minor {
  font-size: 12px;
  font-weight: 500;
  color: var(--fg-dim);
}
.cost__total-label {
  font-size: 12px;
  color: var(--fg-dim);
}
.cost__total-value {
  font-weight: 600;
  font-variant-numeric: tabular-nums;
}
.cost__lines {
  list-style: none;
  margin: 0;
  padding: 0;
}
/* Name takes the slack, quantity and cost keep their own columns so the
   figures line up down the list however long the decor names get. */
.cost__line {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 11px;
  padding: 2px 0;
}
.cost__line-name {
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.cost__line-qty {
  flex-shrink: 0;
  color: var(--fg-dim);
}
.cost__line-cost {
  flex-shrink: 0;
  font-variant-numeric: tabular-nums;
}
.cost__caveat {
  margin: 8px 0 0;
  font-size: 10px;
  line-height: 1.4;
  color: var(--fg-dim);
  opacity: 0.8;
}


/* ---------- the order rail ---------- */
/* The action that completes the job. It had no rules at all until now and came
   out as a bare browser button under the two blue ones — the only unstyled
   thing on screen was the one that earns the money.
   Full width because it is not one option among several. */
.sidebar__export {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding-top: 12px;
  margin-top: 4px;
  border-top: 1px solid var(--rule);
}
.export-btn {
  width: 100%;
  padding: 11px 16px;
  border: none;
  border-radius: var(--radius);
  background: var(--ink);
  color: var(--paper);
  font: inherit;
  font-size: 15px;
  font-weight: 600;
  cursor: pointer;
  transition: opacity 120ms ease;
}
.export-btn:hover:not(:disabled) {
  opacity: 0.88;
}
.export-btn:disabled {
  background: var(--paper-2);
  color: var(--ink-dim);
  cursor: not-allowed;
}
/* Why you cannot order yet. Reasons, not errors: the design is unfinished, and
   saying which part is the useful thing. */
.export-blockers {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 12px;
  line-height: 1.4;
  color: var(--fault);
}
.export-blockers li {
  padding-left: 14px;
  text-indent: -14px;
}
.export-blockers li::before {
  content: "\2014\00a0";
}


/* ---------- price ---------- */
/* What you pay, said once and large. It was four numbers and a paragraph of
   nesting caveat; three of the numbers were about sheets of raw board, which is
   how the piece gets made rather than what anybody is buying. */
.price {
  padding: 4px 0 12px;
}
.price__amount {
  display: flex;
  align-items: baseline;
  gap: 3px;
  font-size: 34px;
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.01em;
}
.price__currency {
  font-size: 20px;
  font-weight: 500;
  color: var(--ink-dim);
}
.price__note {
  margin: 2px 0 0;
  font-size: 12px;
  color: var(--ink-dim);
}

/* ---------- the lede ---------- */
/* One sentence saying how the thing works, where six palette chips and three
   headings used to be. */
.sidebar__lede {
  margin: 0 0 4px;
  font-size: 13px;
  line-height: 1.45;
  color: var(--ink-dim);
}

/* ---------- secondary actions ---------- */
.sidebar__secondary {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid var(--rule);
  border-radius: var(--radius);
  background: none;
  color: var(--ink-dim);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: border-color 100ms ease, color 100ms ease;
}
.sidebar__secondary:hover {
  border-color: var(--ink);
  color: var(--ink);
}

/* ---------- workshop (debug builds) ---------- */
/* Collapsed by default: it is reference, not something to read. */
.workshop {
  border-top: 1px solid var(--rule);
  padding-top: 8px;
}
.workshop__summary {
  font-size: 12px;
  font-weight: 600;
  color: var(--ink-dim);
  cursor: pointer;
  list-style: none;
}
.workshop__summary::-webkit-details-marker {
  display: none;
}
.workshop__summary::before {
  content: "+ ";
}
.workshop[open] .workshop__summary::before {
  content: "\2212\00a0";
}
.sidebar__dev {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
/* The bundle is the one of the three you reach for, and the only one whose
   name does not fit beside another. */
.sidebar__dev .sidebar__secondary:first-child {
  flex-basis: 100%;
}
.sidebar__dev .sidebar__secondary:not(:first-child) {
  flex: 1;
}
.sidebar__dev .sidebar__secondary {
  font-size: 12px;
  padding: 6px 8px;
}


/* ---------- the foot of the rail ---------- */
/* Price and the order button, pinned. What something costs and how to buy it
   are the two things that must never be scrolled away from — and pinning them
   also stops the column reading as half-empty, which it did once the palette
   left. */
.sidebar__foot {
  flex: 0 0 auto;
  padding: 12px 12px 14px;
  border-top: 1px solid var(--rule);
  background: var(--paper);
}
.sidebar__foot .price {
  padding-top: 0;
}

/* ---------- a drawer's box ---------- */
/* The height of the box behind the front, which the front's own height caps.
   At the top of its range it is not a number at all but "as tall as this front
   allows", which is what the word beside the field says. */
.boxrow { display: flex; align-items: center; gap: 8px; }
.boxrow__label { flex: 1; font-size: 13px; font-weight: 600; color: var(--fg); }
.boxrow__auto { font-size: 11px; color: var(--fg-dim); }
.boxrow__num {
  width: 64px;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: 4px 6px;
  font: inherit;
  font-size: 14px;
  color: var(--fg);
  text-align: right;
  font-variant-numeric: tabular-nums;
}
.boxrow__num:focus { outline: none; border-color: var(--accent); }
.boxrow__num:disabled { opacity: 0.4; }
.boxrow__unit { font-size: 11px; color: var(--fg-dim); }
.boxslider { width: 100%; margin: 4px 0 0; accent-color: var(--accent); }
.boxslider:disabled { opacity: 0.4; }
.boxends {
  display: flex;
  justify-content: space-between;
  font-size: 11px;
  color: var(--fg-dim);
}

/* ═══════════════════════════════════════════════════════════════════════════
   The context panel
   ═══════════════════════════════════════════════════════════════════════════
   One panel per kind of thing selected, built from a handful of controls that
   look the same wherever they appear: a choice is always a connected row of
   options, a measurement is always a label, a number and a slider, and a
   catalogue part is always a row that opens a pop-out. */

.panel {
  background: var(--panel);
  border-left: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  min-height: 0;
  min-width: 0;
  overflow: hidden;
  font-variant-numeric: tabular-nums;
}
/* Bottom-sheet grab handle. Only meaningful on mobile, where the panel is a
   sheet rather than a column. */
.panel__grip {
  display: none;
  flex: 0 0 auto;
  align-self: center;
  width: 40px;
  height: 4px;
  margin: 8px 0 2px;
  border-radius: 999px;
  background: var(--border);
  cursor: grab;
  touch-action: none;
}
.panel__head {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
  height: 56px;
  padding: 0 8px 0 16px;
  border-bottom: 1px solid var(--border);
}
.panel__title {
  margin: 0;
  flex: 1;
  min-width: 0;
  font-size: 16px;
  font-weight: 500;
  color: var(--fg);
}
.panel__body {
  flex: 1;
  min-height: 0;
  overflow: auto;
  display: flex;
  flex-direction: column;
}

/* ---------- buttons ---------- */
/* An icon on its own, for the ones whose meaning is the picture: close,
   delete. Square, so a row of them lines up. */
.iconbtn {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: var(--fg-dim);
  cursor: pointer;
  transition: color 100ms, background 100ms;
}
.iconbtn:hover:not(:disabled) { color: var(--fg); background: var(--panel-2); }
.iconbtn:disabled { opacity: 0.35; cursor: default; }
/* The trash goes red on approach, and only there: it is the one button in the
   header that takes something away. */
.panel__head .iconbtn:first-of-type:hover:not(:disabled) { color: var(--ghost-bad); }

/* Outlined, not filled: these are available, not urged. */
.ghostbtn {
  display: flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
  height: 32px;
  padding: 0 10px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: transparent;
  color: var(--fg);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
  transition: border-color 100ms, background 100ms, color 100ms;
}
.ghostbtn:hover:not(:disabled) { border-color: var(--accent); }
.ghostbtn:disabled { opacity: 0.4; cursor: default; }
/* Pressed: the model is showing it open. Filled, like a chosen segment, so
   "on" means one thing throughout the panel. */
.ghostbtn.is-on { background: var(--accent); border-color: var(--accent); color: var(--accent-fg); }
.ghostbtn--bare { border-color: transparent; padding: 0 4px; color: var(--fg-dim); }
.ghostbtn--bare:hover:not(:disabled) { border-color: transparent; color: var(--ghost-bad); }

.icon { flex-shrink: 0; }

/* ---------- sections ---------- */
/* One rule between sections and nothing under a heading: two lines within a
   few pixels of each other read as a box, and the panel was full of boxes. */
.sec {
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 16px;
}
.sec + .sec { border-top: 1px solid var(--border); }
.sec__title {
  margin: 0;
  font-size: 15px;
  font-weight: 500;
  color: var(--fg);
}
.sec__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 8px;
}
.sec__titles { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.sec__sub { font-size: 13px; color: var(--fg-dim); }

/* A control under its own name, for the ones too wide to share a line. */
.labelled { display: flex; flex-direction: column; gap: 6px; }
.labelled__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
}
.labelled__label { font-size: 13px; color: var(--fg-dim); }
/* What is chosen, or a fact about the slot — "Matte stainless", "HDF, 3 mm". */
.labelled__aside { font-size: 13px; color: var(--fg); text-align: right; }

/* A control on the same line as its name. */
.line {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.line__label { font-size: 13px; color: var(--fg-dim); }
.line__control { display: flex; align-items: center; gap: 6px; }
/* Nothing to choose: one value, stated. Deliberately not a disabled control —
   a greyed slider still looks clickable. */
.line__only { font-size: 14px; color: var(--fg); }

.stackrow { display: flex; flex-direction: column; gap: 6px; }
/* A fact worth keeping but not worth reading twice. */
.caption { margin: 0; font-size: 12px; color: var(--fg-dim); line-height: 1.4; }
/* Something that just happened, or what fits while you are typing. */
.says { margin: 0; font-size: 13px; color: var(--fg); line-height: 1.4; }
/* Something that is wrong now and will stay wrong until it is fixed. */
.alarm {
  margin: 0;
  padding: 8px 10px;
  border: 1px solid var(--ghost-bad);
  border-radius: 8px;
  background: color-mix(in srgb, var(--ghost-bad) 12%, transparent);
  color: var(--ghost-bad);
  font-size: 13px;
  line-height: 1.4;
}

/* ---------- a choice ---------- */
/* Two or three options as one connected control: the chosen one filled, the
   rest not. A radio group, not buttons that happen to sit together. */
.seg {
  display: grid;
  gap: 3px;
  padding: 3px;
  background: var(--panel-2);
  border: 1px solid var(--border);
  border-radius: 10px;
}
.seg__opt {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-width: 0;
  height: 34px;
  padding: 0 6px;
  border: 0;
  border-radius: 7px;
  background: transparent;
  color: var(--fg-dim);
  font: inherit;
  font-size: 13px;
  white-space: nowrap;
  cursor: pointer;
  transition: background 100ms, color 100ms;
}
.seg__opt:hover:not(:disabled):not(.is-on) { color: var(--fg); }
.seg__opt.is-on { background: var(--accent); color: var(--accent-fg); }
.seg__opt:disabled { opacity: 0.35; cursor: default; }
.seg__opt:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* ---------- a measurement ---------- */
/* One pattern for every dimension in the app: the name on the left, the number
   on the right, the slider under both. The slider carries the limits — it
   cannot be dragged past them — so no caption has to repeat what they are. */
.dim { display: flex; flex-direction: column; gap: 6px; }
.dim__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.dim__label { font-size: 13px; color: var(--fg-dim); }
.dim__value { display: flex; align-items: center; gap: 6px; }
/* A word in front of the number, when the number is an outcome rather than a
   setting — a box height that is following the front it sits behind. */
.dim__lead { font-size: 12px; color: var(--fg-dim); }
.dim__num {
  width: 72px;
  box-sizing: border-box;
  height: 32px;
  padding: 0 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel-2);
  color: var(--fg);
  font: inherit;
  font-size: 15px;
  text-align: right;
}
.dim__num:focus { outline: none; border-color: var(--accent); }
.dim__num:disabled { opacity: 0.4; }
.dim__unit { width: 22px; font-size: 13px; color: var(--fg-dim); }
.dim__range { width: 100%; margin: 0; accent-color: var(--accent); }
.dim__range:disabled { opacity: 0.4; }
.dim__ends {
  display: flex;
  justify-content: space-between;
  font-size: 12px;
  color: var(--fg-dim);
}
.dim__note { margin: 0; font-size: 12px; color: var(--fg-dim); line-height: 1.4; }

/* ---------- what is chosen from a catalogue ---------- */
/* The grid used to live here, which made a scrolling area inside a scrolling
   panel. The row says what is on it now; choosing happens in the pop-out. */
.pickrow {
  display: flex;
  align-items: center;
  gap: 12px;
  width: 100%;
  padding: 8px 12px 8px 8px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel-2);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: border-color 100ms;
}
.pickrow:hover { border-color: var(--accent); }
.pickrow__thumb {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 40px;
  height: 40px;
  border-radius: 6px;
  overflow: hidden;
  background: var(--paper-2);
}
/* A part is photographed against white and is wider than it is tall. */
.pickrow__thumb--wide { width: 52px; background: #f3f2ee; }
.pickrow__thumb img { width: 100%; height: 100%; object-fit: contain; display: block; }
.pickrow__text { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.pickrow__name {
  font-size: 14px;
  font-weight: 500;
  color: var(--fg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pickrow__sub { font-size: 12px; color: var(--fg-dim); }
.pickrow__action { flex-shrink: 0; font-size: 13px; color: var(--fg-dim); }
.pickrow:hover .pickrow__action { color: var(--fg); }

/* A decor as a colour, or the photograph of one. */
.chip {
  display: block;
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
}
.chip img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Inside a segment, beside the word. */
.chip--sm { width: 14px; height: 14px; border-radius: 3px; flex-shrink: 0; }

/* ---------- colours ---------- */
/* A dot needs no name for the colour it stands for, which is what lets the
   buckets be chosen for the eye rather than for the dictionary. */
.dots { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.dot {
  width: 24px;
  height: 24px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: 50%;
  background-size: cover;
  cursor: pointer;
  transition: transform 100ms, box-shadow 100ms;
}
.dot:hover { transform: scale(1.08); }
.dot.is-on {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--panel), 0 0 0 4px var(--accent);
}
.dots--big .dot { width: 32px; height: 32px; }
/* The only colour it comes in: shown, because the axis is always named, but
   not offered as a choice. */
.dot.is-only { cursor: default; opacity: 0.85; }
.dot.is-only:hover { transform: none; }

/* ═══════════════════════════════════════════════════════════════════════════
   The stack
   ═══════════════════════════════════════════════════════════════════════════
   Drawn to scale, in the cabinet's own orientation, and edited on the drawing:
   the number that sizes a drawer is *in* the drawer. A column of fields beside
   a picture of the same stack asks you to match one row to one band. */

.strip { display: flex; }
.strip--v { flex-direction: row; align-items: stretch; height: 280px; }
.strip--h { flex-direction: column; }
.strip__bands {
  flex: 1;
  display: flex;
  min-width: 0;
  padding: 6px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel-2);
}
.strip--v .strip__bands { flex-direction: column; }
.strip--h .strip__bands { flex-direction: row; height: 120px; }

.band {
  display: flex;
  align-items: center;
  gap: 8px;
  min-height: 0;
  min-width: 0;
  padding: 0 8px 0 10px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel);
  overflow: hidden;
  cursor: pointer;
}
/* A row of columns has no width to spare, so the same parts stack. */
.strip--h .band { flex-direction: column; justify-content: center; gap: 4px; padding: 6px 4px; }
.strip--h .band__note { display: none; }
/* Hatched: this one takes what is left rather than stating a size, so its
   number is an outcome. The dashed field and pill say the same thing up close.*/
.band.is-fill {
  background: repeating-linear-gradient(
    135deg, var(--panel) 0 6px, var(--panel-2) 6px 7px
  );
}
.band.is-fill .band__value,
.band.is-fill .band__mode { border-style: dashed; background: transparent; color: var(--fg-dim); }
/* Over its limits. Named by the banner under the stack; this is so you can see
   which one it means without reading. */
.band.is-bad { border-color: var(--ghost-bad); background: color-mix(in srgb, var(--ghost-bad) 14%, var(--panel)); }
.band.is-bad .band__value { color: var(--ghost-bad); }
/* The one being edited, in the colour the model outlines it with. */
.band.is-on { border-color: var(--pick); box-shadow: inset 0 0 0 1px var(--pick); }

.band__num { width: 10px; flex-shrink: 0; font-size: 12px; color: var(--fg-dim); }
/* Text rather than a number field: the value may be typed with a unit after
   it — "40%", "210mm" — which is how a drawer's sizing is changed without
   going to the menu for it. */
.band__value {
  width: 64px;
  box-sizing: border-box;
  flex-shrink: 0;
  height: 30px;
  padding: 0 8px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--panel-2);
  color: var(--fg);
  font: inherit;
  font-size: 15px;
  text-align: right;
}
.band__value:focus { outline: none; border-color: var(--accent); }
.strip--h .band__value { width: 100%; font-size: 13px; padding: 0 4px; }
.band__unit { flex-shrink: 0; font-size: 13px; color: var(--fg-dim); }
/* What a percentage works out to. The percentage is the instruction; this is
   the thing actually being decided. */
.band__note {
  flex: 1;
  min-width: 0;
  font-size: 13px;
  color: var(--fg-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.band__mode {
  margin-left: auto;
  flex-shrink: 0;
  max-width: 100%;
  height: 28px;
  padding: 0 6px 0 12px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--panel-2);
  color: var(--fg);
  font: inherit;
  font-size: 12px;
  cursor: pointer;
}
.strip--h .band__mode { margin-left: 0; }

/* The divider between two bands, as its own element: a border cannot be
   grabbed. Wider than it looks — the visible line is 3px but the hit area is
   the whole element, which is what makes it catchable with a finger. */
.band__grip {
  flex: 0 0 10px;
  position: relative;
  touch-action: none;
}
.strip--v .band__grip { cursor: row-resize; }
.strip--h .band__grip { cursor: col-resize; }
.band__grip::after {
  content: "";
  position: absolute;
  background: var(--border);
  border-radius: 2px;
}
.strip--v .band__grip::after {
  left: 42%; right: 42%; top: 50%; height: 3px; transform: translateY(-50%);
}
.strip--h .band__grip::after {
  top: 42%; bottom: 42%; left: 50%; width: 3px; transform: translateX(-50%);
}
.band__grip:hover::after { background: var(--accent); }
/* While dragging, the whole strip takes the cursor and stops selecting text —
   a drag that starts on the grip keeps moving over the bands. */
.strip__bands.is-dragging { user-select: none; }
.strip__bands.is-dragging .band__grip::after { background: var(--accent); }

.addbtn {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  width: 100%;
  height: 40px;
  border: 1px dashed var(--border);
  border-radius: 8px;
  background: transparent;
  color: var(--fg-dim);
  font: inherit;
  cursor: pointer;
  transition: border-color 100ms, color 100ms;
}
.addbtn:hover:not(:disabled) { border-color: var(--accent); color: var(--fg); }
.addbtn:disabled { opacity: 0.4; cursor: default; }

/* Whatever the picked entry has beyond a size. Raised off the panel, because
   it is about one band rather than about the stack. */
.card {
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 12px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel-2);
}
.card__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}
.card__title { margin: 0; font-size: 14px; font-weight: 400; color: var(--fg-dim); }
.card__title b { font-weight: 500; color: var(--fg); margin-right: 4px; }

/* ═══════════════════════════════════════════════════════════════════════════
   Where a door's handle goes
   ═══════════════════════════════════════════════════════════════════════════
   The door at its real proportions, with the hinges drawn on the side they are
   actually on, and six places a handle can sit. Every spot shows a faint handle
   in the direction it would take, so the drawing is always a preview. */

.doorview {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 14px;
  margin-bottom: 8px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel-2);
}
.doorview__face {
  display: flex;
  gap: 4px;
  width: 100%;
  max-width: 256px;
}
/* A door-shaped rectangle, the same one whatever the cabinet is. Drawing the
   real proportions made a wide, short door a letterbox with six spots piled
   into it, and the size caption above already says what the real one is. */
.doorview__leaf {
  position: relative;
  flex: 1;
  min-width: 0;
  aspect-ratio: 2 / 3;
  max-height: 200px;
  border: 1px solid var(--border);
  border-radius: 3px;
  background: var(--panel);
}
/* On the outside edge, where the barrel would be. */
.doorview__hinge {
  position: absolute;
  left: -3px;
  width: 4px;
  height: 14px;
  border-radius: 1px;
  background: var(--fg-dim);
}
.doorview__hinge.is-right { left: auto; right: -3px; }

/* The hit target: a thumb-sized square over the handle it picks, and nothing
   to look at until you approach it. */
.spot {
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: 8px;
  background: transparent;
  transform: translate(-50%, -50%);
  cursor: pointer;
}
.spot:hover { background: color-mix(in srgb, var(--fg) 10%, transparent); }
.spot.is-on { background: color-mix(in srgb, var(--fg) 14%, transparent); }
/* The handle itself, drawn at its real size against the board and placed by
   the same function that places it on the cabinet. */
/* A handle, at a size that reads rather than at the one it is. Which way it
   lies is the thing this has to show. */
.spot__bar {
  display: block;
  width: 24px;
  height: 4px;
  border-radius: 2px;
  background: var(--border);
}
.spot__bar--up { width: 4px; height: 24px; }
.spot.is-on .spot__bar { background: var(--fg); }

/* ---------- a rail's finish ---------- */
/* The tube itself, in the colour it renders, rather than three words for three
   metals. */
.finishes { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
.finish {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 10px;
  padding: 14px 10px 10px;
  border: 1px solid var(--border);
  border-radius: 10px;
  background: var(--panel-2);
  color: var(--fg-dim);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.finish.is-on { border-color: var(--accent); color: var(--fg); }
/* Oval, like the tube: 30 × 15 mm in section. */
.finish__tube { display: block; height: 10px; border-radius: 5px; }
.finish__name { text-align: left; }

/* ═══════════════════════════════════════════════════════════════════════════
   The pop-outs
   ═══════════════════════════════════════════════════════════════════════════
   Beside the panel, over the edge of the model — not on top of it, and with no
   dimmed backdrop: choosing a handle and then nudging its size is one job, so
   the panel stays live while this is open. */

.pop {
  position: fixed;
  right: calc(var(--inspector-w) + 10px);
  width: 360px;
  max-height: min(620px, calc(100dvh - 24px));
  z-index: 46;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--panel);
  box-shadow: 0 8px 32px rgba(0, 0, 0, 0.28);
  overflow: hidden;
  animation: pop-in 120ms ease;
}
@keyframes pop-in {
  from { opacity: 0; transform: translateX(6px); }
  to { opacity: 1; transform: none; }
}
.pop__head {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
  height: 56px;
  padding: 0 8px 0 16px;
  border-bottom: 1px solid var(--border);
}
.pop__titles { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.pop__title { margin: 0; font-size: 15px; font-weight: 500; color: var(--fg); }
/* What is on it now, so the pop-out says what it is about to replace. */
.pop__sub {
  font-size: 12px;
  color: var(--fg-dim);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pop__filters {
  display: flex;
  flex-direction: column;
  gap: 12px;
  flex-shrink: 0;
  padding: 16px;
}
/* The two dropdowns side by side. Search, when it is there at all, is a
   debug-only line above them and gets the full width. */
.pop__row { display: flex; gap: 8px; }
.pop__select {
  flex: 1;
  min-width: 0;
  height: 36px;
  padding: 0 8px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel-2);
  color: var(--fg);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}
.pop__select--wide { width: 100%; }
.pop__find { position: relative; display: flex; align-items: center; }
.pop__find .icon { position: absolute; left: 12px; color: var(--fg-dim); pointer-events: none; }
.pop__find-input {
  width: 100%;
  box-sizing: border-box;
  height: 36px;
  padding: 0 12px 0 36px;
  border: 1px solid var(--border);
  border-radius: 8px;
  background: var(--panel-2);
  color: var(--fg);
  font: inherit;
}
.pop__find-input:focus { outline: none; border-color: var(--accent); }

/* One scroll area, and it is this one. */
.pop__grid {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: grid;
  padding: 4px 16px 16px;
}
.pop__grid--decors { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 14px 10px; }
.pop__grid--parts { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 12px; }

/* A decor is chosen by look, so the name is a caption under the swatch rather
   than a tooltip: four columns is wide enough to tell two oaks apart. */
.decor {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.decor__swatch {
  display: block;
  width: 100%;
  height: 52px;
  box-sizing: border-box;
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow: hidden;
}
.decor__img { width: 100%; height: 100%; object-fit: cover; display: block; }
.decor__name { font-size: 12px; line-height: 1.3; color: var(--fg-dim); }
.decor.is-on .decor__swatch { outline: 2px solid var(--accent); outline-offset: 2px; }
.decor.is-on .decor__name { color: var(--fg); }

/* A part is chosen by its photograph, with the range of sizes it is made in
   underneath: the sizes are what the list collapsed, so it says so. */
.part {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.part:disabled { opacity: 0.35; cursor: default; }
.part__shot {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 84px;
  border-radius: 8px;
  background: #f3f2ee;
  overflow: hidden;
}
.part__img { max-width: 88%; max-height: 88%; object-fit: contain; display: block; }
.part__name {
  font-size: 13px;
  font-weight: 500;
  color: var(--fg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.part__sub { font-size: 12px; color: var(--fg-dim); }
.part.is-on .part__shot { outline: 2px solid var(--accent); outline-offset: 2px; }

/* Spinners off: they are 9px of hit area on a number you type or drag, and
   they push the digits left every time the pointer passes over. */
.dim__num::-webkit-outer-spin-button,
.dim__num::-webkit-inner-spin-button {
  appearance: none;
  margin: 0;
}
.dim__num { appearance: textfield; }
