*, *::before, *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

:root {
  /* Everything below is dark, and without saying so the browser keeps painting
     light-mode chrome over it: scrollbars, select popups, colour swatches,
     number spinners and the file button all come up white otherwise. */
  color-scheme: dark;

  --bg-page: #1d1d1d;
  --bg-panel: #303030;
  --bg-panel-hover: #3d3d3d;
  --bg-input: #232323;
  --border: #232323;
  --border-light: #4a4a4a;
  --text: #e6e6e6;
  --text-muted: #9a9a9a;
  --accent: #4772b3;
  --accent-hover: #5482c8;
  --active: #e87d0d;
  --active-hover: #f58a1f;
  --danger: #c04a4a;
  --danger-hover: #d75a5a;
  --focus-glow: #4772b3;
  --viewport-bg: #1d1d1d;
  --radius: 3px;
  --toolbar-h: 44px;
  --statusbar-h: 24px;
  --outliner-w: 220px;
  --attributes-w: 260px;
}

html, body {
  height: 100%;
  background: var(--bg-page);
  color: var(--text);
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  font-size: 13px;
  line-height: 1.5;
}

a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-hover); }

/* Scrollbars, everywhere at once: the outliner and attributes panels, the cut
   editor's body, the tile grid and the tile form, and the document itself on
   the pages that are not height-clamped.
   `color-scheme: dark` above already gets them out of light mode; this is the
   part that matches them to the palette. Deliberately no `scrollbar-color`
   alongside it — Chrome gives the standard property precedence and switches
   ::-webkit-scrollbar customisation off entirely, so setting both would throw
   these rules away. Firefox keeps the native dark bar from color-scheme. */
::-webkit-scrollbar {
  width: 10px;
  height: 10px;
}

/* Transparent, not a colour: --bg-input and --border are the same #232323, and
   the surfaces that scroll already sit on it, so a painted track would only
   show up as a seam. */
::-webkit-scrollbar-track,
::-webkit-scrollbar-corner {
  background: transparent;
}

::-webkit-scrollbar-thumb {
  background: var(--border-light);
  border-radius: 5px;
  /* Inset through the border box, so the thumb reads narrower than the gutter
     it slides in rather than filling it edge to edge. */
  border: 2px solid transparent;
  background-clip: padding-box;
}

::-webkit-scrollbar-thumb:hover {
  background: var(--text-muted);
  background-clip: padding-box;
}

/* ─── Auth pages ─── */
.auth-page {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
}

.auth-form {
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 40px;
  width: 360px;
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.auth-form h1 {
  font-size: 24px;
  font-weight: 700;
  color: var(--accent);
  text-align: center;
}

.auth-form h2 {
  font-size: 16px;
  font-weight: 500;
  color: var(--text-muted);
  text-align: center;
}

.auth-form label {
  display: flex;
  flex-direction: column;
  gap: 4px;
  font-size: 13px;
  color: var(--text-muted);
}

.auth-form input {
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  padding: 10px 12px;
  font-size: 14px;
  outline: none;
  transition: border-color 0.15s;
}

.auth-form input:focus {
  border-color: var(--focus-glow);
  box-shadow: 0 0 0 2px rgba(0, 180, 216, 0.15);
}

.auth-form button {
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: var(--radius);
  padding: 10px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s;
}

.auth-form button:hover { background: var(--accent-hover); }

.auth-error {
  color: var(--danger);
  font-size: 13px;
  text-align: center;
}

.auth-link {
  text-align: center;
  font-size: 13px;
  color: var(--text-muted);
}

/* ─── Scenes page ─── */
.scenes-page {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.scenes-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 16px 24px;
  background: var(--bg-panel);
  border-bottom: 1px solid var(--border);
}

.scenes-header h1 {
  font-size: 20px;
  color: var(--accent);
}

.scenes-main {
  flex: 1;
  padding: 24px;
  max-width: 800px;
  margin: 0 auto;
  width: 100%;
}

.scenes-toolbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 16px;
}

.scenes-toolbar h2 {
  font-size: 18px;
  font-weight: 600;
}

.btn-primary {
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: var(--radius);
  padding: 8px 16px;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s;
}

.btn-primary:hover { background: var(--accent-hover); }
.btn-primary:disabled:hover { background: var(--accent); }

.btn-danger {
  background: transparent;
  color: var(--danger);
  border: 1px solid var(--danger);
  border-radius: var(--radius);
  padding: 8px 16px;
  font-size: 13px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.15s;
}

.btn-danger:hover {
  background: var(--danger);
  color: #fff;
}

.btn-link {
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 13px;
  padding: 4px 8px;
}

.btn-link:hover { color: var(--text); }

.btn-icon {
  background: none;
  border: none;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 16px;
  padding: 4px 8px;
  border-radius: var(--radius);
}

.btn-icon:hover {
  background: var(--bg-panel-hover);
  color: var(--danger);
}

.scene-card {
  display: flex;
  align-items: center;
  justify-content: space-between;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px 16px;
  margin-bottom: 8px;
  transition: border-color 0.15s;
}

.scene-card:hover { border-color: var(--accent); }

.scene-card-link {
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
  color: var(--text);
}

.scene-card-name {
  font-size: 15px;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.scene-card-stats {
  font-size: 12px;
  color: var(--text-muted);
}

.scene-card-link:hover .scene-card-name { color: var(--accent); }

/* ─── The admin page ───
   The same page furniture as the scene list — header, toolbar, a column of cards
   — since it answers the same kind of question about a different noun. The row is
   a scene card with a stacked stat block instead of a single line. */
.admin-hint {
  font-size: 12px;
  color: var(--text-muted);
}

.admin-user {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  background: var(--bg-panel);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 12px 16px;
  margin-bottom: 8px;
}

/* A superuser is the one row worth picking out of a list of thirteen. */
.admin-user.is-admin {
  border-color: var(--accent);
}

.admin-user-main {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-width: 0;
}

.admin-user-head {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 2px;
}

.admin-user-email {
  font-size: 14px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.admin-user-stats {
  font-size: 12px;
  color: var(--text-muted);
}

.admin-tag {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
  padding: 1px 5px;
}

.admin-tag-super {
  color: var(--accent);
  border-color: var(--accent);
}

.admin-user-actions {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-shrink: 0;
}

.scene-card-actions {
  display: flex;
  align-items: center;
  flex-shrink: 0;
}

.btn-rename-scene:hover { color: var(--accent); }

/* The rename form replaces the name+stats block while it is open. */
.scene-card.renaming .scene-card-link { display: none; }

.scene-rename-form { flex: 1; min-width: 0; }

.scene-rename-input {
  width: 100%;
  background: var(--bg-input);
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
  color: var(--text);
  font-family: inherit;
  font-size: 15px;
  font-weight: 500;
  padding: 4px 8px;
}

.scene-rename-input:focus {
  outline: none;
  border-color: var(--focus-glow);
}

/* ─── Editor layout ─── */
.editor {
  display: flex;
  flex-direction: column;
  height: 100vh;
  overflow: hidden;
}

.toolbar {
  display: flex;
  align-items: center;
  gap: 10px;
  height: var(--toolbar-h);
  padding: 0 12px;
  background: var(--bg-panel);
  border-bottom: 1px solid var(--border);
  flex-shrink: 0;
}

.toolbar-title {
  font-size: 15px;
  font-weight: 700;
  color: var(--text);
  margin-right: 8px;
}

/* Reads as the toolbar title until hovered/focused, then as an editable field.
   Sits at the right-hand end of the row now, so it takes the toolbar's own gap
   for spacing rather than carrying a margin of its own. */
.toolbar-title-input {
  width: 180px;
  text-align: right;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius);
  color: var(--text);
  font-family: inherit;
  font-size: 15px;
  font-weight: 700;
  padding: 3px 6px;
  transition: background 0.15s, border-color 0.15s;
}

.toolbar-title-input:hover {
  background: var(--bg-input);
  border-color: var(--border-light);
}

.toolbar-title-input:focus {
  outline: none;
  background: var(--bg-input);
  border-color: var(--focus-glow);
}

.toolbar-actions {
  display: flex;
  gap: 8px;
  align-items: center;
  flex: 1;
}

/* Still an <a>, so middle-click and right-click open it the way a link should,
   but wearing .btn-icon-tool so it reads as part of the tool row. */
.toolbar-back {
  flex-shrink: 0;
}

.toolbar-separator {
  width: 1px;
  height: 24px;
  background: var(--border);
  margin: 0 4px;
}

.btn-icon-tool {
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  width: 32px;
  height: 28px;
  padding: 3px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all 0.15s;
}

.btn-icon-tool:hover {
  background: var(--bg-panel-hover);
}

.btn-icon-tool.active {
  background: var(--active);
  border-color: var(--active-hover);
}

.btn-icon-tool.active:hover {
  background: var(--active-hover);
}

.btn-icon-tool img {
  width: 16px;
  height: 16px;
  display: block;
  filter: brightness(0) invert(1);
  opacity: 0.8;
  transition: opacity 0.15s;
}

.btn-icon-tool:hover img {
  opacity: 1;
}

.btn-icon-tool.active img {
  opacity: 1;
}

.btn-icon-tool:disabled {
  opacity: 0.3;
  cursor: default;
}

.btn-icon-tool:disabled:hover {
  background: transparent;
}

.btn-icon-tool:disabled img {
  opacity: 0.4;
}

/* The app's own mark rather than a tool glyph, so it loses the button chrome
   and stands as just the image — a box around a logo makes it read as one more
   tool. It still has to answer the pointer, which it does by coming up to full
   strength and lifting slightly instead of filling a box behind itself. */
#btn-tiles {
  border-color: transparent;
  background: transparent;
  width: auto;
  height: auto;
  padding: 2px 4px;
}

#btn-tiles:hover {
  background: transparent;
}

/* Every other glyph in the row is a single dark fill that the filter above
   turns white; this one is the real logo, and flattening it to a silhouette
   would throw the point of it away. Being the only colour in the row is what
   marks it as the important button. */
#btn-tiles img {
  filter: none;
  opacity: 0.85;
  width: 22px;
  height: 22px;
  transition: opacity 0.15s, transform 0.15s, filter 0.15s;
}

#btn-tiles:hover img {
  opacity: 1;
  transform: scale(1.12);
  filter: brightness(1.15);
}

/* Pressed: settle back down, so the click has somewhere to land. */
#btn-tiles:active img {
  transform: scale(1.02);
}

.editor-body {
  display: flex;
  flex: 1;
  overflow: hidden;
}

.panel {
  background: var(--bg-panel);
  border-right: 1px solid var(--border);
  display: flex;
  flex-direction: column;
  overflow-y: auto;
  flex-shrink: 0;
}

.panel-title-row {
  display: flex;
  align-items: center;
  border-bottom: 1px solid var(--border);
}

.panel-title {
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--text-muted);
  padding: 12px 16px;
  border-bottom: 1px solid var(--border);
}

.panel-title-row .panel-title {
  border-bottom: none;
}

/* How many surfaces an edit in this panel will reach. Left empty — and so with
   no padding to give it away — whenever that is just the one on display. */
.panel-count {
  font-size: 10px;
  font-weight: 600;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  margin-right: 12px;
}

.panel-count:not(:empty) {
  background: var(--bg-input);
  border: 1px solid var(--border-light);
  border-radius: 10px;
  padding: 2px 8px;
}

.btn-danger-outliner {
  background: transparent;
  color: var(--danger);
  border: 1px solid var(--danger);
  border-radius: var(--radius);
  padding: 3px 8px;
  margin-right: 12px;
  font-size: 11px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.15s;
}

.btn-danger-outliner:hover {
  background: var(--danger);
  color: #fff;
}

.btn-danger-outliner:disabled {
  opacity: 0.3;
  cursor: default;
}

.btn-danger-outliner:disabled:hover {
  background: transparent;
  color: var(--danger);
}

.outliner-panel {
  width: var(--outliner-w);
}

.attributes-panel {
  width: var(--attributes-w);
  border-right: none;
  border-left: 1px solid var(--border);
}

.viewport {
  flex: 1;
  background: var(--viewport-bg);
  position: relative;
  overflow: hidden;
}

/* ─── Transform HUD ───
   Floating readout that tracks the gizmo while dragging (translate/rotate/
   scale delta). Hidden by default; shown/positioned from hud.ts. */
.transform-hud {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 5;
  padding: 3px 8px;
  background: rgba(24, 24, 24, 0.92);
  border: 1px solid var(--border-light);
  border-radius: 4px;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--text);
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transform: translateY(-4px);
  transition: opacity 0.08s ease;
}

.transform-hud.visible {
  opacity: 1;
}

/* ─── Dimension labels ───
   The measurements on the viewport's drafting layer. Pooled divs, positioned in
   px from dimensions.ts by projecting each dimension line's midpoint. The opaque
   background is the same knockout drawLabel() paints on the cut editor's canvas:
   a number has to stay readable over the grid and over its own line. */
.dim-label {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 3;
  transform: translate(-50%, -50%);
  padding: 0 3px;
  background: var(--viewport-bg);
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: 10px;
  line-height: 13px;
  font-variant-numeric: tabular-nums;
  color: rgba(230, 230, 230, 0.8);
  white-space: nowrap;
  pointer-events: none;
}

/* The overall-extents tier, matching COLOR_DIM_FAINT in blueprint.ts. */
.dim-label.faint {
  color: rgba(230, 230, 230, 0.45);
}

/* ─── Marquee (box) selection ───
   Rubber-band rectangle drawn while left-dragging in the Select tool. Positioned
   and sized from viewport.ts; hidden unless the .visible class is set. */
.marquee-box {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 4;
  border: 1px solid var(--accent);
  background: rgba(71, 114, 179, 0.15);
  pointer-events: none;
  display: none;
}

.marquee-box.visible {
  display: block;
}

/* ─── Status bar ─── */
.statusbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: var(--statusbar-h);
  padding: 0 12px;
  background: var(--bg-panel);
  border-top: 1px solid var(--border);
  font-size: 11px;
  color: var(--text-muted);
  flex-shrink: 0;
  gap: 12px;
}

.statusbar-coords {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.statusbar-message {
  flex: 1;
  text-align: right;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.statusbar-message .axis-disabled {
  color: var(--text-muted);
}

/* ─── Outliner ─── */
.outliner-tree {
  list-style: none;
  padding: 4px 0;
}

.outliner-item {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 5px 12px;
  cursor: pointer;
  transition: background 0.1s;
  user-select: none;
}

.outliner-item:hover { background: var(--bg-panel-hover); }

.outliner-item.selected {
  background: rgba(71, 114, 179, 0.25);
  border-left: 3px solid var(--accent);
  padding-left: 9px;
}

/* The active (primary) surface within a multi-selection reads a touch brighter. */
.outliner-item.selected.active {
  background: rgba(71, 114, 179, 0.4);
}

.outliner-icon {
  font-size: 14px;
  color: var(--accent);
}

.outliner-label {
  font-size: 13px;
}

.panel-empty {
  color: var(--text-muted);
  font-size: 13px;
  padding: 24px 16px;
  text-align: center;
}

/* ─── Attributes form ─── */
.attr-form {
  padding: 10px 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.attr-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
}

.attr-row-3 {
  grid-template-columns: repeat(3, 1fr);
}

.attr-field {
  display: flex;
  flex-direction: column;
  gap: 3px;
  font-size: 11px;
  font-weight: 500;
  color: var(--text-muted);
}

.attr-field-axis {
  text-align: center;
}

.attr-field input, .attr-field select {
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: calc(var(--radius) - 2px);
  color: var(--text);
  padding: 5px 7px;
  font-size: 12px;
  outline: none;
  transition: border-color 0.15s, box-shadow 0.15s;
  width: 100%;
}

.attr-field-axis input {
  text-align: center;
  padding: 5px 4px;
}

.attr-field input:hover, .attr-field select:hover {
  border-color: var(--text-muted);
}

.attr-field input:focus, .attr-field select:focus {
  border-color: var(--focus-glow);
  box-shadow: 0 0 0 2px rgba(0, 180, 216, 0.15);
}

/* Checkbox + caption on one line, unlike the stacked default field. */
.attr-field-check {
  flex-direction: row;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text);
  cursor: pointer;
}

.attr-field-check input[type="checkbox"] {
  width: auto;
  margin: 0;
  accent-color: var(--accent);
  cursor: pointer;
}

/* Label left, value right on one line — the same flip .attr-field-check does
   for checkboxes. A colour needs a chip, not a full-width box. */
.attr-field-inline {
  flex-direction: row;
  align-items: center;
  gap: 8px;
}

.attr-field-inline .attr-swatch-hex {
  margin-left: auto;
  font-size: 11px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  text-transform: uppercase;
}

.attr-field input[type="color"] {
  width: 44px;
  height: 22px;
  flex: 0 0 auto;
  padding: 0;
  cursor: pointer;
  overflow: hidden;
}

/* Without these the UA draws its own inset border inside the box, so the chip
   reads as a bordered widget rather than as the colour itself. */
.attr-field input[type="color"]::-webkit-color-swatch-wrapper {
  padding: 0;
}

.attr-field input[type="color"]::-webkit-color-swatch {
  border: none;
  border-radius: 1px;
}

.attr-field input[type="color"]::-moz-color-swatch {
  border: none;
  border-radius: 1px;
}

.attr-fieldset {
  border: 1px solid var(--border);
  border-radius: calc(var(--radius) - 1px);
  padding: 6px 8px 8px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* The panel's two actions, side by side and at the panel's own scale. They used
   to be one full-width .btn-primary at 13px/8px-16px, against fields at
   12px/5px-7px — it read as a foreign element dropped into the column. */
.attr-actions {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
  margin-top: 4px;
}

.attr-action {
  background: transparent;
  border: 1px solid var(--border-light);
  border-radius: calc(var(--radius) - 2px);
  color: var(--text);
  font: inherit;
  font-size: 11px;
  padding: 6px 4px;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}

.attr-action:hover {
  background: var(--bg-panel-hover);
}

.attr-action:disabled {
  opacity: 0.35;
  cursor: default;
}

.attr-action:disabled:hover {
  background: transparent;
}

/* With more than one surface selected, every field here writes to all of them —
   it always has. The tint is what stops that being a silent surprise. */
.attr-form.multi .attr-field {
  color: var(--active);
}

.attr-fieldset legend {
  font-size: 10px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.5px;
  color: var(--text-muted);
  padding: 0 4px;
}

/* ─── Room dialog ─── */
.room-dialog {
  width: 300px;
  padding: 0;
  /* The `* { margin: 0 }` reset kills the UA's `margin: auto`, which is what
     centers a modal dialog in its inset: 0 box. */
  margin: auto;
  background: var(--bg-panel);
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
  color: var(--text);
}

.room-dialog::backdrop {
  background: rgba(0, 0, 0, 0.5);
}

.room-dialog-title {
  margin: 0;
}

.room-dialog-body {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px;
}

.room-dialog-actions {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 8px;
  margin-top: 2px;
}

/* ─── Cut editor dialog ─── */
.cut-dialog {
  /* Nearly the whole window: this is a drawing surface, not a form. The canvas
     below flexes into whatever the chrome leaves, so no scrollbar appears. */
  width: min(1600px, calc(100vw - 32px));
  height: calc(100vh - 32px);
  flex-direction: column;
  overflow: hidden;
  padding: 0;
  /* Same reason as .room-dialog: the margin reset would pin it to the corner. */
  margin: auto;
  background: var(--bg-panel);
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
  color: var(--text);
}

/* Only when open: a closed <dialog> is hidden by the UA's `display: none`, and a
   plain `display: flex` here would override it and pin the editor to the page. */
.cut-dialog[open] {
  display: flex;
}

.cut-dialog::backdrop {
  background: rgba(0, 0, 0, 0.5);
}

.cut-dialog-title {
  margin: 0;
  padding: 10px 12px 0;
}

.cut-dialog-body {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px;
  /* min-height: 0 lets the canvas shrink; the overflow is the last resort on a
     window too short even for the canvas's minimum. */
  flex: 1;
  min-height: 0;
  overflow: auto;
}

.cut-tools {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
}

.cut-tool {
  background: transparent;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  color: var(--text);
  font-size: 11px;
  padding: 5px 9px;
  cursor: pointer;
  transition: all 0.15s;
}

/* Matches .btn-danger-outliner:disabled. A button that does nothing has to look
   like it — the built-in tile library greys several of these out at once, and
   without this they sat there at full contrast inviting the click. */
.cut-tool:disabled,
.btn-primary:disabled {
  opacity: 0.3;
  cursor: default;
}

.cut-tool:disabled:hover {
  background: transparent;
  border-color: var(--border);
}

.cut-tool:hover {
  background: var(--bg-panel-hover);
}

.cut-tool.active {
  background: var(--active);
  border-color: var(--active-hover);
}

/* Pushes the history/delete buttons away from the drawing tools. */
.cut-tools-gap {
  flex: 1;
}

.cut-canvas {
  width: 100%;
  /* Takes the height left over after the toolbar, fields and buttons, down to a
     floor that still leaves room for the drafting margins outside the face. */
  flex: 1 1 0;
  min-height: 260px;
  display: block;
  background: var(--viewport-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  /* The editor drives the pointer itself; touch scrolling would fight it. */
  touch-action: none;
}

/* Shares the footer row with the fields and buttons, taking whatever width they
   leave. min-width:0 lets it wrap rather than shove them off the right edge. */
.cut-hint {
  flex: 1;
  min-width: 0;
  font-size: 11px;
  color: var(--text-muted);
  min-height: 14px;
}

.cut-dialog-actions {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 8px;
  margin-top: 2px;
}

/* ─── Selected cut's X/Y/W/H ───
   A compact strip in the dialog's footer, left of Cancel/Apply. It stays a
   fieldset purely so `disabled` still covers all four inputs at once when
   nothing is selected — the border and legend are dropped. */
.cut-fields {
  border: 0;
  padding: 0;
  margin: 0 6px 0 0;
}

.cut-fields:disabled {
  opacity: 0.4;
}

/* The flex row lives on a child: fieldsets have their own layout quirks. */
.cut-fields-row {
  display: flex;
  align-items: center;
  gap: 5px;
}

.cut-field {
  display: flex;
  align-items: center;
  gap: 4px;
  font-size: 11px;
  font-weight: 500;
  color: var(--text-muted);
}

.cut-field input {
  width: 66px;
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: calc(var(--radius) - 2px);
  color: var(--text);
  padding: 4px 6px;
  font-size: 12px;
  text-align: center;
  font-variant-numeric: tabular-nums;
  outline: none;
  transition: border-color 0.15s, box-shadow 0.15s;
}

.cut-field input:hover {
  border-color: var(--text-muted);
}

.cut-field input:focus {
  border-color: var(--focus-glow);
  box-shadow: 0 0 0 2px rgba(0, 180, 216, 0.15);
}

.cut-fields-unit {
  font-size: 11px;
  color: var(--text-muted);
}

/* Moved to the attributes block, where these two buttons actually live. */

/* ─── Tile library ───
   Three columns: the repeat preview on the left taking the slack, the selected
   tile's form in the middle, and the set picker over its swatch grid on the
   right. Same dialog shell as the cut editor. */
.tile-dialog {
  width: min(1200px, calc(100vw - 32px));
  /* The whole window height, like .cut-dialog: the preview is a viewport you
     pan and zoom now, not a thumbnail, and it flexes into whatever is left. */
  height: calc(100vh - 32px);
  flex-direction: column;
  overflow: hidden;
  padding: 0;
  /* Same reason as .room-dialog: the margin reset would pin it to the corner. */
  margin: auto;
  background: var(--bg-panel);
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
  color: var(--text);
}

/* Only when open — a plain `display: flex` would defeat the UA's `display: none`
   and pin the dialog to the page. Same trap as .cut-dialog. */
.tile-dialog[open] {
  display: flex;
}

.tile-dialog::backdrop {
  background: rgba(0, 0, 0, 0.5);
}

.tile-dialog-title {
  margin: 0;
  padding: 10px 12px 0;
}

.tile-dialog-body {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px;
  flex: 1;
  min-height: 0;
}

/* Sits in the list column now, which is too narrow for the picker and its three
   buttons side by side — so the picker takes the first row on its own and the
   buttons wrap onto the second. */
.tile-sets {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 6px;
}

/* .cut-hint carries `flex: 1` so it can take the slack in the cut editor's
   footer row. Here it is a row in a column, where growing would hand it all the
   spare height and squash the panes above it. */
.tile-dialog-body .cut-hint {
  flex: 0 0 auto;
}

.tile-set-picker {
  flex: 1 1 100%;
}

/* Why the form beside this is greyed out, and the way round it. Sits under the
   set picker rather than beside the form, because the set is what makes it
   read-only and that is the control you change to get out of it. */
.tile-readonly {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.tile-readonly .cut-hint {
  flex: 1;
  margin: 0;
}

.tile-readonly button {
  flex: 0 0 auto;
  white-space: nowrap;
}

/* The rename box stands exactly where the picker was, so the row does not jump. */
.tile-set-picker input[type="text"] {
  background: var(--bg-input);
  border: 1px solid var(--focus-glow);
  border-radius: calc(var(--radius) - 2px);
  color: var(--text);
  padding: 5px 7px;
  font-size: 12px;
  outline: none;
  width: 100%;
}

/* The three panes split the height the chrome leaves; min-height:0 is what lets
   the grid scroll instead of stretching the dialog. */
.tile-main {
  display: flex;
  gap: 12px;
  flex: 1;
  min-height: 0;
}

/* 360, not less: .tile-grid is `repeat(auto-fill, minmax(104px, 1fr))` with an
   8px gap, so three swatches need 328px of content box. At 320px — once the
   grid's own 8px padding and 1px border come off — only two would fit, and the
   column would quietly go two-across. */
.tile-grid-pane {
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex: 0 0 360px;
  min-width: 0;
}

.tile-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  align-content: start;
  gap: 8px;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 8px;
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.tile-add {
  align-self: flex-start;
}

.tile-swatch {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 5px;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s;
}

.tile-swatch:hover {
  background: var(--bg-panel-hover);
}

.tile-swatch.selected {
  background: var(--bg-panel-hover);
  border-color: var(--active);
}

/* A fixed-height row so the labels under every swatch line up, whatever shape
   the tile inside it is. The face itself is sized in tilecard.ts — shared with
   the Lay Tiles picker — because keeping a plank's proportions while fitting it
   into the column needs both axes scaled together and CSS can only clamp one of
   them at a time. */
.tile-swatch-face {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 74px;
}

.tile-swatch-shape {
  display: block;
  max-width: 100%;
  border: 1px solid var(--border);
  border-radius: calc(var(--radius) - 2px);
  background-position: center;
  /* Every tile is a solid sitting proud of its joint, so every swatch gets the
     drop. The chamfer is a per-tile property and rides on .bevelled below. */
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.4);
}

/* The chamfer the preview canvas draws, as the two insets — same light from the
   upper left, so a swatch and the preview below it read as the same tile. Hard
   inset edges, because a flat chamfer meets the face along a hard line. */
.tile-swatch-shape.bevelled {
  box-shadow:
    inset 1px 1px 0 rgba(255, 255, 255, 0.3),
    inset -1px -1px 0 rgba(0, 0, 0, 0.3),
    0 1px 2px rgba(0, 0, 0, 0.4);
}

/* A bullnose instead: the insets blur into the face rather than stopping at it,
   and the silhouette loses its corners, matching the canvas clip. */
.tile-swatch-shape.bevelled.rounded {
  border-radius: 5px;
  box-shadow:
    inset 2px 2px 3px rgba(255, 255, 255, 0.32),
    inset -2px -2px 3px rgba(0, 0, 0, 0.32),
    0 1px 2px rgba(0, 0, 0, 0.4);
}

.tile-swatch-label {
  font-size: 11px;
  line-height: 1.25;
  /* Two lines, then ellipsis: catalogue names run long. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.tile-swatch-size {
  font-size: 10px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* The left column: the canvas takes the slack, the caption rides under it. The
   canvas has to live out here rather than inside .tile-form, because nothing
   inside an `overflow-y: auto` box can flex to the available height. */
.tile-preview-col {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 0;
}

/* Positioned only so the reset-view button can sit in its corner. */
.tile-preview-pane {
  position: relative;
  display: flex;
  flex: 1 1 0;
  min-height: 260px;
}

/* A fieldset only so `disabled` greys the form out when no tile is selected —
   same trick as .cut-fields. The preview is deliberately not inside it: a
   dimmed-out preview is harder to read, and it is blank anyway with no tile. */
.tile-form {
  /* Its own column now, so it simply fills the height and scrolls when the
     fields outrun it — no cap needed to stop it crowding the preview. */
  flex: 0 0 300px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 0;
  overflow-y: auto;
  margin: 0;
  padding: 10px;
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.tile-form:disabled {
  opacity: 0.4;
}

.tile-preview {
  width: 100%;
  /* Takes whatever height the caption and fields leave, down to the pane's
     floor — the same deal .cut-canvas gets. */
  flex: 1 1 0;
  display: block;
  background: var(--viewport-bg);
  border: 1px solid var(--border);
  border-radius: calc(var(--radius) - 1px);
  /* The preview drives the pointer itself; touch scrolling would fight it. */
  touch-action: none;
}

/* Reset view. Absent until the view has actually been moved — there is nothing
   to go back to before that — then faint, so it never competes with the tiles,
   and full strength once the pointer is over the preview. */
.tile-preview-home {
  position: absolute;
  right: 8px;
  bottom: 8px;
  background: var(--bg-panel);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.15s;
}

.tile-preview-pane.moved .tile-preview-home {
  opacity: 0.35;
  pointer-events: auto;
}

.tile-preview-pane.moved:hover .tile-preview-home,
.tile-preview-home:focus-visible {
  opacity: 1;
}

.tile-preview-caption {
  font-size: 11px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  min-height: 14px;
  margin-top: -4px;
}

.tile-form-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 2px;
}

.tile-form-gap {
  flex: 1;
}

/* ─── Lay Tiles dialog ───
   The same shell as the cut editor — a full-window drawing surface — with a
   fixed rail down the right for the zones and their tile mixes. The canvas takes
   everything the rail and the chrome leave. */
.lay-dialog {
  width: min(1600px, calc(100vw - 32px));
  height: calc(100vh - 32px);
  flex-direction: column;
  overflow: hidden;
  padding: 0;
  /* Same reason as .room-dialog: the margin reset would pin it to the corner. */
  margin: auto;
  background: var(--bg-panel);
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
  color: var(--text);
}

/* Only when open — a plain `display: flex` would defeat the UA's `display: none`
   and pin the dialog to the page. Same trap as .cut-dialog. */
.lay-dialog[open] {
  display: flex;
}

.lay-dialog::backdrop {
  background: rgba(0, 0, 0, 0.5);
}

.lay-dialog-title {
  margin: 0;
  padding: 10px 12px 0;
}

.lay-dialog-body {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px;
  flex: 1;
  min-height: 0;
}

/* min-height: 0 so the canvas can shrink inside the column rather than pushing
   the footer off the bottom of the dialog. */
.lay-main {
  display: flex;
  gap: 10px;
  flex: 1;
  min-height: 0;
}

.lay-canvas {
  flex: 1 1 0;
  min-width: 0;
  min-height: 260px;
  display: block;
  background: var(--viewport-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  /* The editor drives the pointer itself; touch scrolling would fight it. */
  touch-action: none;
}

.lay-rail {
  width: 260px;
  flex: 0 0 260px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  overflow-y: auto;
}

.lay-section {
  display: flex;
  flex-direction: column;
  gap: 6px;
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
}

/* The zone form is a fieldset only so `disabled` covers every control at once
   when no zone is selected; the border and padding are dropped. */
.lay-zone-form[disabled] {
  opacity: 0.5;
}

.lay-section-head {
  display: flex;
  align-items: center;
  gap: 6px;
}

.lay-section-title {
  flex: 1;
  margin: 0;
  font-size: 11px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

.lay-mini {
  padding: 2px 6px;
  font-size: 11px;
}

.lay-zones {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.lay-zone {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 4px 6px;
  font: inherit;
  font-size: 12px;
  text-align: left;
  color: var(--text);
  background: var(--bg-input);
  border: 1px solid transparent;
  border-radius: var(--radius);
  cursor: pointer;
}

.lay-zone:hover {
  border-color: var(--border-light);
}

.lay-zone.active {
  border-color: var(--accent);
}

.lay-zone-swatches {
  display: flex;
  gap: 2px;
  margin-left: auto;
}

/* The tile's own colour under its face image, so a tile whose image has not
   decoded yet still reads as the right colour rather than as a hole.
   `cover` here, unlike .tile-swatch-shape's `100% 100%`, and deliberately: this
   is a 14px chip answering "what colour and texture", not a format — stretching
   a 2:1 wood photo into a square would distort the grain at a size where no
   proportion reads anyway. The picker's thumbnails are a format view and go
   through tileShape, so they stretch with the library grid. */
.lay-swatch {
  width: 14px;
  height: 14px;
  flex: 0 0 14px;
  border-radius: 2px;
  border: 1px solid var(--border);
  background-size: cover;
  background-position: center;
}

.lay-mix {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.lay-mix-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 3px 6px;
  font-size: 12px;
  background: var(--bg-input);
  border: 1px solid transparent;
  border-radius: var(--radius);
  cursor: pointer;
}

/* Which tile the swap tool paints with — the same orange the editor uses for
   "the thing you are about to act on" everywhere else. */
.lay-mix-row.active {
  border-color: var(--active);
}

.lay-mix-name {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.lay-mix-weight {
  width: 44px;
  flex: 0 0 44px;
  font-variant-numeric: tabular-nums;
}

.lay-mix-remove {
  background: transparent;
  border: 0;
  color: var(--text-muted);
  cursor: pointer;
  font-size: 14px;
  line-height: 1;
  padding: 0 2px;
}

.lay-mix-remove:hover {
  color: var(--danger);
}

.lay-warn {
  color: var(--active);
}

/* ─── The tile picker ───
   A thumbnail grid rather than a dropdown, because choosing a tile is a visual
   decision and a list of catalogue names is not one.

   Its own dialog rather than a panel in the Lay rail: the library is dozens of
   tiles, and that grid in a 260px column had to be capped into a scroller of its
   own, which pushed Start from, Band and Rectangle somewhere you had to go
   looking for them. Sized as a picker, not a workspace — wide enough for five or
   six columns, and no taller than the window. */
.pick-dialog {
  width: min(760px, calc(100vw - 32px));
  max-height: calc(100vh - 64px);
  flex-direction: column;
  overflow: hidden;
  padding: 0;
  /* Same reason as .room-dialog: the margin reset would pin it to the corner. */
  margin: auto;
  background: var(--bg-panel);
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
  color: var(--text);
}

/* Only when open — a plain `display: flex` would defeat the UA's `display: none`
   on a closed dialog. */
.pick-dialog[open] {
  display: flex;
}

/* Darker than the other dialogs': this one opens on top of Lay Tiles, which has
   already spent one backdrop, and at 0.5 the two stack into near-black. */
.pick-dialog::backdrop {
  background: rgba(0, 0, 0, 0.35);
}

.pick-dialog-title {
  margin: 0;
  padding: 12px 12px 0;
}

/* .cut-dialog-actions carries no padding of its own — in the cut and lay dialogs
   it sits inside a padded body and takes theirs. Here it is a direct child of a
   dialog with `padding: 0`, so the hint ran into the left edge and Done into the
   right. The rule above it separates the footer from the grid the way the two
   panes of the other dialogs are separated. */
.pick-dialog .cut-dialog-actions {
  gap: 10px;
  padding: 10px 12px 12px;
  border-top: 1px solid var(--border);
}

/* Fixed chrome, one scrolling pane — the shape the cut editor and the tile
   library both use. The body does not scroll; the grid does. Scrolling the whole
   body instead and pinning the controls with `position: sticky` left a strip of
   the body's own top padding above them, and rows slid up through it in full
   view of the dialog title. A sticky bar only ever masks its own box. */
.pick-dialog-body {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  flex: 1;
  min-height: 0;
}

/* Three .attr-fields, each stacking its label over its control, exactly as the
   attributes panel and the Tile Library's set row do. flex-end keeps the controls
   themselves on one line even if a label wraps on a narrow window.

   The flex sizes below are on the labels, not the controls: .attr-field is a
   column with the default `stretch`, so each input and select fills the width its
   label was given. */
.pick-controls {
  display: flex;
  align-items: flex-end;
  gap: 6px;
}

/* One height for all three. Left to themselves a select and a text input differ
   by a pixel or two — same padding, same border, different intrinsic content box
   — which nobody notices until they are sitting in a row together. */
.pick-controls input,
.pick-controls select {
  height: 28px;
}

/* Wide enough for a set name without crowding the search box. */
.pick-set {
  flex: 0 0 200px;
  min-width: 0;
}

/* The one that takes the slack — it is the control you type into. */
.pick-search {
  flex: 1;
  min-width: 0;
}

.pick-format {
  flex: 0 0 160px;
  min-width: 0;
}

.pick-note {
  margin: 0;
  font-size: 11px;
  color: var(--text-muted);
}

/* The only thing in the dialog that scrolls. min-height: 0 so it can shrink
   inside the column rather than pushing the footer out through the bottom. */
.pick-grid {
  display: grid;
  /* minmax rather than a column count, so the grid reflows on a narrow window
     instead of clipping. */
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  align-content: start;
  gap: 4px;
  padding: 6px;
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  /* So bottoming out the grid does not then scroll the page behind the dialog. */
  overscroll-behavior: contain;
  background: var(--bg-input);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

.pick-tile {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 3px;
  position: relative;
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius);
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background 0.15s, border-color 0.15s, opacity 0.15s;
}

.pick-tile:hover {
  background: var(--bg-panel-hover);
}

/* Already in the mix. The accent blue, not the orange `--active` — that is
   reserved for .lay-mix-row.active, which is the one entry the Swap tool paints
   with, and the two must not read as the same state. */
.pick-tile.in-mix {
  border-color: var(--accent);
}

.pick-tile.in-mix::after {
  content: "✓";
  position: absolute;
  top: 1px;
  right: 3px;
  font-size: 10px;
  line-height: 1;
  color: var(--accent);
}

/* What the next click will do, said before the click. A tick that turns into a
   cross under the pointer is the only thing telling you the grid is a toggle at
   all — without it, "click again to remove" is a sentence in the footer that
   nobody reads twice. */
.pick-tile.in-mix:hover::after {
  content: "✕";
  color: var(--danger);
}

/* A different format from the one setting the mix's lattice. Dimmed rather than
   hidden: the constraint stays legible, but clicking the first tile into an
   empty mix does not make fifty thumbnails vanish under the pointer.

   Only just dimmed, though. A mix led by a 300×600 puts 54 of the 56 tiles in
   here, and at the 0.45 this started on the whole grid read as disabled rather
   than as de-emphasised. The job is to let the handful that do share the lattice
   step forward, not to grey out the library. */
.pick-tile.off-format {
  opacity: 0.72;
}

.pick-tile.off-format:hover {
  opacity: 1;
}

/* Follows PICK_BOX_PX in tilepicker.ts, plus a little air: the face is measured
   there, in both axes, so a plank keeps its proportions instead of being capped
   by CSS into the same box as a square. */
.pick-face {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 66px;
}

.pick-label {
  font-size: 11px;
  line-height: 1.2;
  /* Two lines, then ellipsis: catalogue names run long. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.pick-meta {
  font-size: 10px;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

.pick-empty {
  grid-column: 1 / -1;
}

/* Sits in the footer between the hint and the buttons: the running count of what
   the wall costs in tiles, which is the question the dialog exists to answer. */
.lay-readout {
  font-size: 11px;
  color: var(--text);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ─── Estimate dialog ───
   A document rather than a workspace: a column of tables that scrolls, with the
   estimator's own figures pinned above it and the total in the footer. */
.est-dialog {
  width: min(1000px, calc(100vw - 32px));
  max-height: calc(100vh - 32px);
  flex-direction: column;
  overflow: hidden;
  padding: 0;
  /* Same reason as .room-dialog: the margin reset would pin it to the corner. */
  margin: auto;
  background: var(--bg-panel);
  border: 1px solid var(--border-light);
  border-radius: var(--radius);
  color: var(--text);
}

/* Only when open — a plain `display: flex` would defeat the UA's `display: none`
   and pin the dialog to the page. Same trap as .cut-dialog. */
.est-dialog[open] {
  display: flex;
}

.est-dialog::backdrop {
  background: rgba(0, 0, 0, 0.5);
}

.est-dialog-title {
  margin: 0;
  padding: 10px 12px 0;
}

.est-dialog-body {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 12px;
  min-height: 0;
}

.est-settings {
  display: flex;
  flex-direction: column;
  gap: 4px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 8px;
  margin: 0;
}

/* The tables are the part that grows, so they take the scroll rather than the
   dialog — the settings and the total stay put while the list moves. */
.est-body {
  display: flex;
  flex-direction: column;
  gap: 12px;
  overflow-y: auto;
  min-height: 0;
}

.est-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}

.est-table th {
  text-align: right;
  font-weight: 600;
  font-size: 11px;
  color: var(--text-muted);
  padding: 4px 6px;
  border-bottom: 1px solid var(--border-light);
  white-space: nowrap;
}

.est-table td {
  text-align: right;
  padding: 3px 6px;
  border-bottom: 1px solid var(--border);
}

/* The first column is a name, not a number. */
.est-table th:first-child,
.est-table td:first-child {
  text-align: left;
}

.est-table tfoot td {
  font-weight: 600;
  border-bottom: 0;
  border-top: 1px solid var(--border-light);
}

.est-unpriced {
  display: inline-block;
  padding: 0 5px;
  font-size: 10px;
  border-radius: 8px;
  color: var(--active);
  border: 1px solid var(--active);
  white-space: nowrap;
}

.est-details summary {
  font-size: 11px;
  color: var(--text-muted);
  cursor: pointer;
  padding: 2px 0;
}

.est-note {
  color: var(--active);
}

.est-total {
  font-size: 15px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The printable sheet: a plain white copy of the estimate, filled in just
   before printing and thrown away after.

   Printing the dialog itself does not work. It is opened with showModal(), so it
   lives in the browser's top layer under a ::backdrop that covers the page, and
   it is nested inside .editor rather than sitting at the top of the body — so
   any rule broad enough to hide the editor hides the dialog with it. A separate
   sheet sidesteps both: nothing in the top layer, nothing to except out. */
.est-print {
  display: none;
}

@media print {
  /* The app is dark; paper is not. Browsers drop background colours by default,
     but "Background graphics" in the print dialog puts them back — and a quote
     printed on a black page wastes a cartridge. */
  html,
  body {
    background: #fff !important;
  }

  /* Hidden, not display:none — the sheet is nested, and its ancestors have to
     stay laid out for it to have anywhere to be. visibility is inherited, so a
     blanket hide here can still be undone on the sheet's own subtree below. */
  body * {
    visibility: hidden !important;
  }

  #estimate-print,
  #estimate-print * {
    visibility: visible !important;
  }

  #estimate-print {
    display: block !important;
    position: absolute;
    left: 0;
    top: 0;
    width: 100%;
    padding: 0;
    background: #fff;
    color: #000;
    font: 12px system-ui, sans-serif;
  }

  .est-print-head {
    display: flex;
    align-items: baseline;
    gap: 10px;
    margin-bottom: 10px;
    border-bottom: 1px solid #000;
    padding-bottom: 6px;
  }

  .est-print-title {
    font-size: 16px;
    font-weight: 600;
    flex: 1;
  }

  .est-print-total {
    font-size: 15px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
  }

  /* On paper the estimate is one document, so the collapsible per-surface
     breakdown is opened rather than left for someone to click. */
  #estimate-print details {
    display: block;
  }

  #estimate-print summary {
    font-weight: 600;
    margin: 12px 0 4px;
    list-style: none;
  }

  #estimate-print .est-table {
    width: 100%;
    border-collapse: collapse;
    font-size: 11px;
    font-variant-numeric: tabular-nums;
    margin-bottom: 14px;
    /* A table split across a page break loses its header; these are short. */
    break-inside: avoid;
  }

  #estimate-print .est-table th {
    text-align: right;
    font-size: 10px;
    color: #444;
    padding: 3px 5px;
    border-bottom: 1px solid #000;
    white-space: nowrap;
  }

  #estimate-print .est-table td {
    text-align: right;
    padding: 3px 5px;
    border-bottom: 1px solid #ccc;
  }

  #estimate-print .est-table th:first-child,
  #estimate-print .est-table td:first-child {
    text-align: left;
  }

  #estimate-print .est-table tfoot td {
    font-weight: 600;
    border-bottom: 0;
    border-top: 1px solid #000;
  }

  #estimate-print .est-unpriced {
    border: 1px solid #666;
    color: #444;
    border-radius: 8px;
    padding: 0 4px;
    font-size: 9px;
  }

  #estimate-print .est-note {
    color: #444;
    font-size: 10px;
  }

  @page {
    margin: 14mm;
  }
}
