/* site/labels/styles.css: the label maker, on BrickBatch's own design system.
   Tokens (--ground, --ink, --accent, --cream, --muted, --card…) and the whole
   .btn mechanism come from /front/button.css, which is linked ahead of this
   file. NOTHING here re-styles .btn: a button on this page is a plain .btn
   with the system's own state classes, exactly as on the front page. */

:root {
  color-scheme: light;
  /* The front page's own grid line; button.css falls back to --faint, which
     is a much darker tan and reads as a texture rather than a structure. */
  --grid-line: #d8d0bf;
  /* The rise settings are the unit system's tokens, mapped onto button.css's
     scale on #lm; see "ON THE GRID" at the end of this file. */
  /* NO --radius override. button.css sets it to 0 and the whole design is
     hard-cornered; redefining it here rounded the system's own buttons. */
  --card: rgba(118, 94, 61, 0.06);
  --card-border: rgba(118, 94, 61, 0.22);
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: 'Lexend', system-ui, sans-serif;
  font-size: 14px;
  background: var(--ground);
  color: var(--ink);
}

/* ---- THE SITE HEADER ------------------------------------------------
   These rules are PORTED from front.css, not imported. front.css carries
   the landing page's whole layout with it (a flex-column body, its hero
   grid placement, its own --v-rest), and linking it here would fight this
   page for control of body and main. What the header actually needs is the
   block below: --hdr-row-h and the .slot-* sizing, all of it a pure
   function of --gp, which chrome.js sets from the same staircase
   front/layout.js uses. Keep the two in step.

   Not ported: layout.js's measured nav shrinking and its grid-mode
   absolute positioning. This header wraps instead of collapsing, which is
   the right behaviour for a tool page that has its own bar underneath. */
:root { --hdr-row-h: calc(3 * var(--gp) + var(--sw)); }

#site-header {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--gp);
  padding: calc(var(--gp) * .6) var(--gp);
}
.row-flush { display: flex; }
.lockup-row, .nav-row { height: var(--hdr-row-h); }
.lockup-row .slot-mark { width: var(--hdr-row-h); height: var(--hdr-row-h); }
.lockup-row .slot-word-lg { width: calc(13 * var(--gp) + var(--sw)); height: var(--hdr-row-h); }
.nav-row .slot-word { width: calc(6 * var(--gp) + var(--sw)); height: var(--hdr-row-h); }
/* A flush row overlaps its neighbours by exactly one stroke, which is how
   the design system shares a seam between two buttons. */
.row-flush > .btn + .btn { margin-left: calc(var(--sw) * -1); }
.nav-row { margin-left: calc(var(--sw) * -1); }

/* Matching button.css's own three-deep `.btn > .face > .fill` padding rule,
   so this wins the tie by source order rather than losing on specificity. */
.slot-word-lg > .face > .fill { padding: calc(var(--gp) * .22); }
/* The mark's .lab needs an explicit height or the image's max-height:100%
   resolves against an indefinite containing block and silently does
   nothing; front.css's own note on this is worth reading. */
.slot-mark > .face > .fill > .lab { height: 100%; }
.slot-mark > .face > .fill {
  padding-block: calc((var(--hdr-row-h) - var(--sw) * 2) * .12);
  padding-inline: calc((var(--hdr-row-h) - var(--sw) * 2) * .10);
}
.logo-img { display: block; max-width: 100%; max-height: 100%; object-fit: contain; }
a.btn { text-decoration: none; color: inherit; }

/* ---- THE TOOL BAR ---------------------------------------------------
   The application's own controls, at the application's own density. A
   dense tool does not use landing-page button sizes, and the site header
   above is what carries the site's identity. */
.tool-bar {
  display: flex;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
  padding: 10px 20px;
  background: var(--cream);
  border-top: var(--sw) solid var(--stroke);
  border-bottom: var(--sw) solid var(--stroke);
}

/* The panels sit on the grid, so they need their own ground to stay legible
   over it. main itself stays transparent. */
main > section { position: relative; }
h1 { font-size: 18px; margin: 0; font-weight: 600; letter-spacing: -0.01em; }
h2 { font-size: 15px; font-weight: 600; margin: 0 0 10px; }
h3 { font-size: 13px; font-weight: 600; margin: 18px 0 8px; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; }

#panel-nav { display: flex; gap: 8px; flex-wrap: wrap; }

main { padding: 20px; max-width: 1400px; background: transparent; }

/* ---- form controls -------------------------------------------------- */
#lm :is(input[type="search"], input[type="text"], input[type="number"], select, textarea) {
  font-family: inherit;
  font-size: 14px;
  padding: 7px 9px;
  border: 1px solid var(--card-border);
  background: #fff;
  color: var(--ink);
}
#lm input[type="search"] { width: 100%; }
label { font-size: 13px; }

.field { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--muted); }
/* A .field is a stacked label-over-control pair, which is wrong for a
   checkbox: the box ends up on its own line above its own text. */
.field.check, label.check { flex-direction: row; align-items: center; gap: 7px; font-size: 13px; color: var(--ink); }
.field.check input, label.check input { margin: 0; }
.field input, .field select { color: var(--ink); }

.toolbar {
  display: flex;
  gap: 10px;
  align-items: center;
  flex-wrap: wrap;
  margin-bottom: 14px;
}
.toolbar .spacer { flex: 1 1 auto; }

.panel-card {
  background: var(--cream);
  border: 1px solid var(--card-border);
  padding: 14px 16px;
}

.hint { color: var(--muted); font-size: 12px; margin: 6px 0 0; }
.empty { color: var(--muted); font-size: 13px; padding: 24px 4px; }

/* ---- catalogue: the card grid ---------------------------------------- */
.catalog-layout { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 20px; align-items: start; }
@media (max-width: 1000px) { .catalog-layout { grid-template-columns: minmax(0, 1fr); } }

.breadcrumb { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; font-size: 13px; color: var(--muted); }
.breadcrumb strong { color: var(--ink); font-weight: 600; }

.piece-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(124px, 1fr));
  gap: 12px;
}

/* A piece card is a real button, so hover and press come from the system.
   What it must NOT do is take .is-selected, which repaints the WHOLE card
   accent: the art is meant to sit on the page's own ground, and only the
   caption strip that holds the text changes colour. */
/* front.css defines .is-cream, but front.css is the FRONT PAGE's stylesheet
   and is not linked here, so the class did nothing and every card fell back
   to button.css's default fill, which is the accent. The two tokens are set
   directly instead, which is all .is-cream ever was. */
.piece-card {
  text-align: left;
  --fill-rest: var(--cream);
  --fg-rest: var(--ink);
  /* Rise: the TILE role (see "ON THE GRID"): a grid of 150 lifted cards is
     noise, so tiles rest flat by default and rise on hover. */
}
.piece-card > .face > .fill {
  display: flex;
  flex-direction: column;
  gap: 0;
  padding: 0;
}
/* button.css sets white-space:nowrap on .fill for single-line labels, and
   sizes .lab to its content. A card's label is a block of wrapped text, so
   both have to be undone HERE (never in button.css) or a long part name runs
   straight out through the side of its card. */
.piece-card > .face > .fill > .lab {
  display: flex; flex-direction: column; gap: 0;
  width: 100%; min-width: 0; flex: 1 1 auto;
  white-space: normal;
}
.piece-card > .face > .fill { white-space: normal; }

.piece-card .thumb {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: contain;
  /* No background. The render is a transparent PNG and it belongs on the
     card's own ground, not in a white box floating inside it. */
  display: block;
  padding: 8px;
}
.piece-card .thumb-missing {
  width: 100%; aspect-ratio: 4 / 3;
  display: flex; align-items: center; justify-content: center;
  font-size: 11px; color: var(--muted); text-align: center; padding: 4px;
}

/* THE CAPTION STRIP: the only part of a card that changes colour. */
.piece-card .pc-caption {
  display: flex; flex-direction: column; gap: 2px;
  position: relative;
  /* Pinned to the bottom of the button's fill box. Without this the fill's own
     background shows as a strip below the caption, a pale line under the
     accent whenever a card is selected. */
  margin-top: auto;
  width: 100%; min-width: 0;
  padding: 7px 9px;
  border-top: var(--sw) solid var(--stroke);
  background: var(--cream);
  transition: background var(--dur) var(--ease);
}
.piece-card.is-picked .pc-caption { background: var(--accent); }
/* In SOME OTHER group. Still selectable (the same piece may belong to
   several groups on purpose) but visibly spoken for. */
.piece-card.is-elsewhere .pc-caption { background: var(--panel-2); color: var(--muted); }
.piece-card.is-elsewhere.is-picked .pc-caption { background: var(--accent); color: var(--ink); }
/* Armed for removal: the first of two clicks inside an open group. */
.piece-card.is-armed .pc-caption { background: var(--danger); color: var(--cream); }
.piece-card.is-armed .pc-id { color: var(--cream); }

.piece-card .pc-name {
  width: 100%; min-width: 0; white-space: normal;
  font-size: 11.5px; line-height: 1.25; font-weight: 500;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.piece-card .pc-id { font-size: 10.5px; color: var(--muted); font-variant-numeric: tabular-nums; }
.piece-card.is-picked .pc-id { color: var(--ink); }
.piece-card .pc-meta { display: flex; justify-content: space-between; align-items: center; gap: 6px; width: 100%; min-width: 0; }
.piece-card .pc-meta > * { overflow: hidden; text-overflow: ellipsis; }

/* ---- groups ---------------------------------------------------------- */
.group-list { list-style: none; margin: 0 0 12px; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.group-row {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 10px;
  background: #fff;
  border: 1px solid var(--card-border);
}
.group-row.is-open { border-color: var(--accent); box-shadow: inset 0 0 0 1px var(--accent); }
.group-row.is-hidden { opacity: .5; }
.group-row .gr-main { flex: 1 1 auto; min-width: 0; cursor: pointer; }
.group-row .gr-name { font-weight: 600; font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.group-row .gr-sub { font-size: 11px; color: var(--muted); }
.group-row .gr-dots { display: flex; gap: 2px; margin-top: 4px; flex-wrap: wrap; }
.group-row .gr-dot { width: 10px; height: 10px; border: 1px solid rgba(0,0,0,.18); }
.new-group-row { display: flex; gap: 8px; margin-bottom: 12px; }
.new-group-row input { flex: 1 1 auto; min-width: 0; }

/* ---- colour picker -------------------------------------------------
   Square .btn.sq swatches, one grid square each, sharing their seams, the
   colour carried by --fill-rest so it IS the button's fill, and selection
   expressed as the PRESSED state (--bb-tile-pressed: latched down until
   another colour is chosen) rather than a border or a tick. Geometry
   carries the selection. The name is read out above the row on hover,
   because 274 swatches cannot each carry a legible label. */
.swatch-row {
  display: flex;
  flex-wrap: wrap;
  margin-bottom: 4px;
}
/* Size, seams and the selected (PRESSED) state: see "ON THE GRID" below. */
/* A transparent colour is marked by its edge, never by its fill; the fill
   is the colour and must stay honest. */
.swatch-row .btn.is-trans > .face > .fill { opacity: .55; }

.swatch-caption {
  min-height: 1.35em;
  font-size: 12px;
  font-weight: 600;
  color: var(--ink);
  margin: 2px 0 6px;
}
.swatch-caption .sc-hint { font-weight: 400; color: var(--muted); }
.swatch-section-label { font-size: 11px; color: var(--muted); margin: 12px 0 6px; }

/* ---- sheet ----------------------------------------------------------- */
.sheet-layout { display: grid; grid-template-columns: minmax(0, 420px) minmax(0, 1fr); gap: 20px; align-items: start; }
@media (max-width: 900px) { .sheet-layout { grid-template-columns: minmax(0, 1fr); } }
.sheet-fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin: 12px 0; }
.sheet-fields .full { grid-column: 1 / -1; }
.sheet-preview { position: relative; background: #fff; border: 1px solid var(--card-border); overflow: hidden; }
.sheet-preview .cell { position: absolute; border: 1px dashed rgba(118, 94, 61, .55); box-sizing: border-box; }
/* ---- sliders --------------------------------------------------------
   Ported from front.css's .slider rather than imported: front.css carries
   the front PAGE's layout with it, and this file only needs the control.
   Same mechanism: one stroke weight, hover and press change GEOMETRY only
   (thumb scale), never colour, exactly like every button. */
.slider {
  appearance: none;
  -webkit-appearance: none;
  display: block;
  width: 100%;
  height: calc(var(--sw) * 3);
  background: var(--panel-2);
  box-shadow: inset 0 0 0 var(--sw) var(--stroke);
  outline: none;
  margin: 4px 0 0;
  cursor: pointer;
}
.slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 14px; height: 22px;
  background: var(--accent);
  box-shadow: inset 0 0 0 var(--sw) var(--stroke);
  margin-top: calc((var(--sw) * 3 - 22px) / 2);
  cursor: pointer;
  transition: transform .12s var(--ease);
}
.slider::-moz-range-thumb {
  width: 14px; height: 22px;
  background: var(--accent);
  box-shadow: inset 0 0 0 var(--sw) var(--stroke);
  border: 0; border-radius: 0;
  cursor: pointer;
  transition: transform .12s var(--ease);
}
.slider::-moz-range-track { height: calc(var(--sw) * 3); background: transparent; border: 0; }
.slider:hover::-webkit-slider-thumb, .slider:focus-visible::-webkit-slider-thumb { transform: scaleY(1.15); }
.slider:hover::-moz-range-thumb, .slider:focus-visible::-moz-range-thumb { transform: scaleY(1.15); }
.slider:active::-webkit-slider-thumb { transform: scaleY(.8); }
.slider:active::-moz-range-thumb { transform: scaleY(.8); }
@media (prefers-reduced-motion: reduce) {
  .slider::-webkit-slider-thumb, .slider::-moz-range-thumb { transition: none; }
}

.sheet-warning:not(:empty) {
  margin-top: 10px; padding: 8px 10px;
  background: #fff3cd; border: 1px solid #e0c67a; color: #6b4e00; font-size: 13px;
}

/* ---- the label itself, shared by template preview and review grid -----
   One set of rules, so what the editor shows and what the review grid shows
   cannot drift apart. Size always comes from labelbox.js, never from here. */
.label-box {
  position: relative;
  background: #fff;
  border: 1px solid var(--card-border);
  /* NOT overflow: hidden. A field with bleed on must be seen running past the
     label, as it will print; the default mask comes from .lf-mask below, one
     per masked field, which is the PDF's per-field clip path in CSS. */
  overflow: visible;
}
/* The label's own rectangle (inset: 0 is the same padding box the fields'
   percentages resolve against), clipping the one field inside it. */
.label-box .lf-mask { position: absolute; inset: 0; overflow: hidden; }
.label-box .lf-img { position: absolute; object-fit: contain; }
.label-box .lf-missing {
  display: flex; align-items: center; justify-content: center;
  color: var(--muted); font-size: 10px;
}
/* line-height MUST match compose.js's LINE_HEIGHT: the PDF steps its
   baselines by that number, and a preview that spaces lines differently from
   the print is not a preview. */
.label-box .lf-txt { position: absolute; line-height: 1.15; overflow: hidden; }
/* Each wrapped line is its own element, broken by measure.js rather than by
   the browser, so the preview and the PDF cannot disagree about where a line
   ends. white-space: pre stops the browser re-breaking one. */
.label-box .lf-line { white-space: pre; }
/* A colour swatch (compose.js's swatch rect): the piece colour, outlined in
   ink. border-box puts the outline inside the box, where pdf.js pulls its
   stroke; the width is set inline from the print's own stroke. */
.label-box .lf-swatch { position: absolute; box-sizing: border-box; border: 1px solid var(--ink); background: transparent; }

/* ---- template editor ------------------------------------------------- */
.template-layout { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 380px); gap: 24px; align-items: start; }
@media (max-width: 1000px) { .template-layout { grid-template-columns: minmax(0, 1fr); } }

.stage { display: flex; flex-direction: column; gap: 10px; align-items: flex-start; }
.stage-canvas { position: relative; }

/* The editable overlay sits ON the live preview, so a field is dragged
   against the real content it will print rather than against a hatched box. */
.field-overlay { position: absolute; inset: 0; }
/* An UNSELECTED field is outlined but never filled. The preview's whole job
   is to show what will print, and a wash of accent colour over every field
   quietly recolours the label the user is judging. Only the field being
   edited is tinted. */
.field-handle {
  position: absolute;
  border: 1px solid rgba(32, 26, 18, .30);
  background: transparent;
  cursor: move;
  touch-action: none;
}
.field-handle.is-selected { border: 2px solid var(--ink); background: rgba(254, 215, 0, .18); }
.field-handle .grip {
  position: absolute; width: 10px; height: 10px;
  background: var(--accent); border: 1px solid var(--ink);
  touch-action: none;
}
.field-handle .grip.se { right: -5px; bottom: -5px; cursor: nwse-resize; }
.field-handle .grip.sw { left: -5px; bottom: -5px; cursor: nesw-resize; }
.field-handle .grip.ne { right: -5px; top: -5px; cursor: nesw-resize; }
.field-handle .grip.nw { left: -5px; top: -5px; cursor: nwse-resize; }
.field-handle .fh-tag {
  position: absolute; top: -16px; left: 0;
  font-size: 9px; color: var(--muted); white-space: nowrap; pointer-events: none;
}

/* ---- the field editor (app.js mountFieldEditor) ---------------------
   The SAME sections for every field, in the same places: a text-only section
   is disabled for an image field, never removed, so the editor is one height
   whatever is selected and nothing around it jumps. */
.field-editor { display: flex; flex-direction: column; gap: 10px; }
/* "Add field": one segmented row of icon buttons (app.js addFieldRow), the
   same on the Template panel and in the dialog. It wraps rather than
   overflowing a narrow column, so EVERY button (not just the second onward)
   steps back one stroke on both axes, and the row pads that back, so a button
   that wraps to the start of a line then shares its seams like the rest. */
.add-field { display: flex; flex-direction: column; gap: 5px; margin-bottom: 12px; }
.fe-head { display: flex; align-items: baseline; gap: 8px; min-height: 1.4em; }
.fe-title { font-weight: 700; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.fe-hint { font-size: 12px; color: var(--muted); }
.field-editor fieldset { border: 0; margin: 0; padding: 0; min-width: 0; display: flex; flex-direction: column; gap: 12px; }
/* Disabled reads as "not for this field": present, dimmed, inert. */
.field-editor fieldset:disabled { opacity: .38; }
.field-editor fieldset:disabled .btn { pointer-events: none; }
.fe-sec { display: flex; flex-direction: column; gap: 5px; }
.fe-cap { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
.fe-geo { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; }
.fe-geo input { width: 100%; min-width: 0; }
.fe-tools { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.fe-state { font-size: 12px; color: var(--muted); }
.fe-foot { margin-top: 2px; }

/* Segmented rows: small square buttons sharing their seams, like the swatch
   row. Flat at rest (a dense row of risen buttons is noise, and each one's
   footprint would sit on its neighbour), rising on hover as ever. Selected is
   the yellow fill (setSelected in app.js), never a stroke. */
.seg { display: flex; align-items: stretch; }
/* (flat at rest: they are TILES; see "ON THE GRID") */
.fe-size input[type="number"] {
  width: 4.2em; text-align: center; position: relative; z-index: 1;
  min-height: 0; height: calc(var(--u) * 1.3);
}
.fe-aa { font-size: 15px; line-height: 1; }

/* Flat at rest like the segmented rows; the chosen colour is PRESSED in
   (see "ON THE GRID"), since a swatch's fill IS its colour and cannot turn
   yellow. */
.fe-swatches { margin: 0; padding-top: var(--sw); }
.fe-swatches .btn.sq { width: calc(var(--u) * 1.3); height: calc(var(--u) * 1.3); }
.fe-swatches .ms { font-size: 18px; }
.fe-custom-wrap { position: relative; display: inline-flex; margin-left: calc(var(--sw) * -1); margin-top: calc(var(--sw) * -1); }
.fe-custom-wrap .btn.sq { margin: 0; }
/* The real colour input: invisible, under its button, so the picker opens
   where the button is. */
.fe-custom-wrap input[type="color"] {
  position: absolute; inset: 0; width: 100%; height: 100%;
  opacity: 0; pointer-events: none; border: 0; padding: 0;
}

/* ---- review ---------------------------------------------------------- */
.review-grid { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-start; }
.review-tile { display: flex; flex-direction: column; gap: 4px; cursor: pointer; }
/* NOT content-visibility: auto. It looks like the right tool for a few hundred
   heavy tiles, but an element inside a content-visibility:auto subtree never
   reports an IntersectionObserver intersection, which is exactly the signal
   app.js uses to decide when to bake that tile's art. The two cancel out and
   no image ever loads. Lazy baking is the mechanism that actually matters
   here (it avoids the WORK, not just the paint), so it wins. */
.review-tile .label-box { outline: 2px solid transparent; outline-offset: 2px; }
.review-tile.is-selected .label-box { outline-color: var(--ink); }
.review-tile.is-hidden { opacity: .35; }
.review-tile .rt-caption { font-size: 10px; color: var(--muted); max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.page-break { width: 100%; border-top: 1px dashed var(--card-border); margin: 8px 0 2px; font-size: 11px; color: var(--muted); padding-top: 6px; }
/* v2: dragging tiles into a custom order (app.js startTileDrag). The bar is
   where the dragged tile(s) will land; positioned against the grid. */
.review-grid { position: relative; }
.review-tile.is-drag-source { opacity: .35; }
.drop-bar { position: absolute; width: var(--L, 3px); background: var(--ink); pointer-events: none; z-index: 2; }
body.is-tile-dragging, body.is-tile-dragging * { cursor: grabbing !important; user-select: none; }

/* ---- the per-label editor -------------------------------------------
   Double-clicking a review tile opens the template editor for THAT label
   alone, so one piece can be laid out differently from the sheet without
   touching the shared template. */
#label-dialog {
  border: var(--sw) solid var(--stroke);
  background: var(--cream);
  color: var(--ink);
  padding: 0;
  max-width: min(940px, 92vw);
  width: 100%;
}
/* A FIXED height, whatever is selected or typed: the head and the footer
   hold still and only the side column scrolls, so Save and Cancel never move
   out from under the pointer. [open] only: a closed dialog must stay
   display: none. */
#label-dialog[open] {
  display: flex; flex-direction: column;
  height: min(88vh, 780px);
  overflow: hidden;
}
#label-dialog::backdrop { background: rgba(32, 26, 18, .45); }
.dlg-head {
  display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
  padding: 12px 16px;
  border-bottom: 1px solid var(--card-border);
}
.dlg-head h2 { margin: 0; }
.dlg-head .dlg-sub { font-size: 12px; color: var(--muted); }
.dlg-head, .dlg-foot { flex: none; }
.dlg-body {
  flex: 1 1 auto; min-height: 0;
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 320px); grid-template-rows: minmax(0, 1fr);
  gap: 20px; padding: 0 0 0 16px;
}
.dlg-body > .stage { padding-top: 16px; min-height: 0; overflow-x: hidden; overflow-y: auto; }
.dlg-side { overflow-y: auto; min-height: 0; padding: 16px 16px 16px 2px; }
/* Room round the preview for a field that BLEEDS to be seen doing it: the
   label is drawn inset by this much (app.js DIALOG_BLEED_ROOM), so what runs
   past its edge is visible instead of cut off by the dialog. */
.dlg-body .stage-canvas { margin: 28px; }
@media (max-width: 860px) {
  /* Stacked, the two columns scroll as one; the footer still holds still. */
  .dlg-body { grid-template-columns: minmax(0, 1fr); grid-template-rows: none; overflow-y: auto; padding-right: 16px; }
  .dlg-body > .stage, .dlg-side { overflow: visible; min-height: auto; }
  /* min-height: auto matters: at 0 the rows were squeezed into the body's
     height and the preview painted over the name controls below it. */
  .dlg-body { align-content: start; }
}
.dlg-foot {
  display: flex; gap: 8px; align-items: center;
  padding: 12px 16px;
  border-top: 1px solid var(--card-border);
}
.dlg-foot .spacer { flex: 1 1 auto; }
/* Never wrap a footer label: a button that grows a second line changes the
   footer's height, and the footer is what must hold still. */
.dlg-foot .btn .lab { white-space: nowrap; }
/* A phone-narrow dialog cannot hold five buttons in a row at all. Wrapping
   there is the lesser evil: the rows are fixed once the dialog has a width,
   because no footer label changes its own width (see #dlg-reset below). */
@media (max-width: 860px) { .dlg-foot { flex-wrap: wrap; } }

/* v2: "Add a colour of this piece" */
.colour-add { display: flex; flex-direction: column; align-items: flex-start; gap: 6px; margin: 4px 0 12px; }
.colour-add .btn .lab { display: inline-flex; align-items: center; gap: 6px; }
.colour-add .ms { font-size: 18px; }
#dlg-colour-picker .hint { margin: 0; }

/* ---- two buttons that change their own label ------------------------
   A button that resizes when its text changes drags the whole row with it.
   Both of these swap their label in place ("Reset to sheet template" ->
   "Sure?", "Hide selected" -> "Show selected"), so they are given the width
   of their WIDEST state and keep it. */
#dlg-reset > .face > .fill { min-width: 15em; }
#review-hide-selected > .face > .fill { min-width: 8em; }

/* ---- icons ------------------------------------------------------------
   Material Symbols Rounded (index.html loads the subset), the library's own
   settings: weight 500, outlined at rest, FILLED only for the selected state.
   Sized for the small square button, where the label's ordinary text size
   would make a glyph tiny. */
.ms {
  font-family: 'Material Symbols Rounded';
  font-weight: normal; font-style: normal;
  font-size: 20px; line-height: 1;
  letter-spacing: normal; text-transform: none; white-space: nowrap; direction: ltr;
  display: inline-block; flex: none;
  font-variation-settings: 'FILL' 0, 'wght' 500, 'GRAD' 0, 'opsz' 24;
  -webkit-font-smoothing: antialiased;
}
.btn.is-selected .ms { font-variation-settings: 'FILL' 1, 'wght' 500, 'GRAD' 0, 'opsz' 24; }

/* v2: name options: ‹ [name] › with the option readout under it */
.name-field { gap: 6px; }
.name-row { display: flex; align-items: stretch; gap: 0; }
.name-row input { flex: 1 1 auto; min-width: 0; margin: 0 calc(var(--L) * -1); position: relative; z-index: 1; }
.name-row .btn .lab { font-size: 22px; font-weight: 700; line-height: 1; }   /* the ‹ › arrows, readable */
.name-option { font-family: var(--font-mono); font-size: 11px; color: var(--muted); letter-spacing: .03em; }
.name-option b { color: var(--ink); }
.name-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }

/* ===================================================================== *
   ON THE GRID (2026-09-26): the grid, header and footer are the unit
   system's (site/units/, mounted by unit-frame.js), and so is every box in
   the tool. The rules, as site/units/unit.css states them:

     S = square (the grid pitch), L = line (the gap between squares).
     Grid lines sit on n·S from the origin (top-centre of the page).
     A box that snaps: position n·S − L/2, size n·S + L, so its edges
     cover the lines. Strokes are L, in ink.

   How that is done INSIDE a normal-flow page, without positioning every
   element by hand:
     · The flow runs on the lines. #lm starts on a line (unit-frame.js puts
       its top at n·S − L/2; its L/2 padding moves the flow onto the line).
     · A BOX (bordered control, panel, button) is n·S + L with a margin of
       −L/2 all round, so it takes up exactly n·S of flow and its edges land
       half a line either side of it, on the lines.
     · Inside a bordered box, padding k·S − L/2 puts the flow back on a line.
     · Gaps are whole squares; boxes that share a seam have no gap at all
       (their −L/2 margins overlap them by exactly one line).
     · Text takes whole squares of height (line-height S, or S/2 for small
       text in a block at least S tall); app.js mountGridSnap rounds up
       whatever content decides (a button's label, a wrapped hint, a label
       preview's physical proportions; previews are real-size renders, so
       only their FRAMES are on the grid).
   Everything is in var(--S)/var(--L), so it follows the tokens.
 * ===================================================================== */
body { background: var(--gap); }                 /* the grid canvas paints the squares */
.unit-stage { z-index: 2; }                      /* header menus above the tool */

#lm {
  /* rise: the unit system's ROLE tokens (tokens.css), on button.css's own
     scale. --offset is the travel of one v, so v means S/10 here exactly as
     it does on a unit. A .btn is a BUTTON; piece cards and the rows whose
     buttons share their seams are TILES (below). */
  --sw: var(--L);
  --offset: calc(var(--S) / 10);
  --v-rest: var(--bb-button-rest);
  --v-hover: var(--bb-button-hover);
  --v-press: var(--bb-button-press);
  --v-pressed: var(--bb-button-pressed);
  --dur: var(--bb-dur);
  --dur-press: var(--bb-dur-press);
  --ease: var(--bb-ease);
  /* the lengths every rule below is built from */
  --half: calc(var(--L) / -2);                   /* a box's margin */
  --pad: calc(var(--S) - var(--L) / 2);          /* padding (inside a border) that lands the flow on a line */
  --spad: calc(var(--S) + var(--L) / 2);         /* the same inside an inset stroke */
  --box1: calc(var(--S) + var(--L));             /* a one-square box */

  position: relative; z-index: 1;
  display: flex; flex-direction: column;
  /* a whole number of squares wide, edges on the grid lines, centred on the origin */
  width: calc(2 * var(--lm-H, 20) * var(--S) + var(--L));
  margin-left: calc(var(--ox) - var(--lm-H, 20) * var(--S) - var(--L) / 2);
  padding: calc(var(--L) / 2);
}

/* ---- boxes: n·S + L, taking n·S of flow ------------------------------ */
/* :where() so these two stay at an id's weight and any more specific rule
   below (a seamed row, a corner segment) can override them */
#lm :where(.btn, input:not([type="checkbox"]):not([type="color"]), select, textarea,
        .tool-bar, .panel-card, .group-row, .sheet-warning, .stage-frame, .sheet-frame,
        .review-tile > .label-box, .page-break, .fe-custom-wrap) {
  margin: var(--half);
}
/* A container's stroke is drawn INSIDE its box (a hard inset line, no
   blur), not as a border: the browser rounds a border down to whole device
   pixels (3px is 2.67px at 150% zoom), and in a column of panels that error
   piles up until the lower ones are off the grid. Padding (--pad, below)
   counts the stroke in. */
#lm :where(.tool-bar, .panel-card, .group-row, .sheet-warning, .stage-frame, .sheet-frame) {
  border: 0;
  border-radius: 0;
  box-shadow: inset 0 0 0 var(--L) var(--ink);
  background: var(--panel);
}

/* ---- text: whole squares ---------------------------------------------- */
#lm :is(h1, h2, h3) { margin: 0; line-height: var(--S); min-height: var(--S); }
h1 { font-size: calc(var(--S) * .7); font-weight: 800; letter-spacing: -.01em; }
h2 { font-size: calc(var(--S) * .55); font-weight: 800; }
h3 { font-family: var(--font-mono); font-size: 11px; letter-spacing: .09em; }
#lm h3 { margin-top: var(--S); }
/* a text block: half-square lines, a quarter square of padding top and
   bottom, so one line fills one square; mountGridSnap rounds a wrapped block
   up to whole squares */
#lm :is(.hint, .empty, .swatch-caption, .swatch-section-label, .name-option) {
  display: block;
  margin: 0;
  padding: calc(var(--S) / 4) 0;
  min-height: var(--S);
  line-height: calc(var(--S) / 2);
}
#lm :is(.hint, .empty)[hidden] { display: none; }
/* an empty status line (e.g. "Added in Red" before anything is added) takes
   no room; it stays in the page so it is still announced when it fills */
#lm .hint:empty { min-height: 0; padding: 0; }
/* button.css's display:inline-block outranks the browser's [hidden] rule,
   so a hidden .btn (e.g. "Apply to all colours" with only one colour) still
   showed and took up a place in its row */
#lm .btn[hidden] { display: none; }
#lm :is(.breadcrumb, .fe-head) {
  display: flex; align-items: center; gap: calc(var(--S) / 3);
  margin: 0; min-height: var(--S); line-height: calc(var(--S) / 2);
}
/* a field is a label line over its control: two squares */
#lm .field { gap: 0; line-height: var(--S); }
#lm .field.check, #lm label.check { min-height: var(--S); gap: calc(var(--S) / 3); line-height: 1.2; }

/* ---- controls --------------------------------------------------------- */
#lm :is(input[type="search"], input[type="text"], input[type="number"], select, textarea) {
  border: var(--L) solid var(--ink);
  background: var(--panel);
  border-radius: 0;
  height: var(--box1);
  min-height: 0;
  padding: 0 calc(var(--S) / 4);
  font-size: calc(var(--S) * .45);
}
#lm .field textarea {
  height: calc(2 * var(--S) + var(--L));
  padding: calc(var(--S) / 4 - var(--L));
  line-height: calc(var(--S) / 2);
  resize: vertical;
}
/* FOCUS: never a ring. Keyboard focus follows the unit system's focus
   tokens (--bb-focus-button / --bb-focus-field, copied onto <html
   data-focus-*> by site/units/tokens.js), the same treatments unit.css gives
   a unit: rise like hover / yellow / white / ink face, or none. With no
   attribute (no JS) the defaults apply: a button rises, a field turns white.
   (unit.css already removes every outline on the page.) */
:root:is(:not([data-focus-button]), [data-focus-button="rise"]) #lm .btn:not(:active):focus-visible { --v: var(--v-hover); }
:root[data-focus-button="yellow"] #lm .btn:focus-visible { --fill: var(--yellow); --fg: var(--on-yellow); }
:root[data-focus-button="white"]  #lm .btn:focus-visible { --fill: var(--white); }
:root[data-focus-button="ink"]    #lm .btn:focus-visible { --fill: var(--ink); --fg: var(--panel); }
:root:is(:not([data-focus-field]), [data-focus-field="white"]) #lm :is(input, select, textarea):not([type="checkbox"], [type="color"], [type="range"]):focus-visible { background: var(--white); }
:root[data-focus-field="yellow"] #lm :is(input, select, textarea):not([type="checkbox"], [type="color"], [type="range"]):focus-visible { background: var(--yellow); color: var(--on-yellow); }
:root[data-focus-field="ink"] #lm :is(input, select, textarea):not([type="checkbox"], [type="color"], [type="range"]):focus-visible { background: var(--ink); color: var(--panel); }

/* .btn (from /front/button.css): one square tall, width from its label,
   rounded up to whole squares by mountGridSnap. */
#lm .btn { height: var(--box1); vertical-align: top; }
#lm .btn > .face > .fill { font-size: calc(var(--S) * .43); padding: 0 calc(var(--S) * .4); }
#lm .btn.xs > .face > .fill { font-size: calc(var(--S) * .4); padding: 0 calc(var(--S) * .3); }
#lm .btn.sq { width: var(--box1); }
#lm .btn.sq > .face > .fill { padding: 0; }
/* On a press the label re-centres in what is left of the fill (half the
   travel), as a unit's content does when its face is pressed in; the full
   travel pushed a one-square button's label out through its bottom edge. */
#lm .btn > .face > .fill > .lab {
  transform: translate(calc(var(--push) * var(--rise-x) * -.5), calc(var(--push) * var(--rise-y) * -.5));
}

/* PRESSED: latched down until something else is chosen: the selected
   colour, the open panel tab, a toggled alignment, font or bleed. It stays
   down under the pointer too; it is a state, not a hover. */
#lm .btn:is(.is-selected, .is-swatch-selected, [aria-pressed="true"]) {
  --v-rest: var(--v-pressed);
  --v-hover: var(--v-pressed);
  --v-press: var(--v-pressed);
}

/* TILES: the tile role's tokens: piece cards and every row whose buttons
   share their seams (panel tabs, segmented rows, swatch rows, a group's
   corner segment, the ‹ name › row), where risen neighbours would stand on
   each other's footprints. :where() keeps this under the PRESSED rule
   above, so a chosen tile still latches down (at the tile's own depth). */
#lm :where(.piece-card, #panel-nav > .btn, .seg > .btn, .fe-swatches .btn, .swatch-row .btn, .gr-actions > .btn, .name-row > .btn) {
  --v-rest: var(--bb-tile-rest);
  --v-hover: var(--bb-tile-hover);
  --v-press: var(--bb-tile-press);
  --v-pressed: var(--bb-tile-pressed);
}

/* sliders: a one-square box with the track drawn across its middle */
#lm .slider {
  width: calc(100% + var(--L));
  height: var(--box1);
  margin: var(--half);
  background: transparent;
  box-shadow: none;
}
#lm .slider::-webkit-slider-runnable-track {
  height: calc(var(--L) * 3);
  background: var(--panel-2);
  box-shadow: inset 0 0 0 var(--L) var(--ink);
}
#lm .slider::-webkit-slider-thumb { margin-top: calc((var(--L) * 3 - 22px) / 2); }
#lm .slider::-moz-range-track { height: calc(var(--L) * 3); background: var(--panel-2); box-shadow: inset 0 0 0 var(--L) var(--ink); }

/* rows of controls: whole-square gaps, bottoms aligned (a field is two
   squares, a button one; both end on a line) */
#lm .toolbar { gap: var(--S); margin: 0; align-items: flex-end; }
#lm .toolbar > .field { flex: none; }
#lm .toolbar select { width: calc(6 * var(--S) + var(--L)); }
#lm .toolbar .slider { width: calc(5 * var(--S) + var(--L)); }

/* ---- the tool bar ------------------------------------------------------ */
#lm .tool-bar { padding: var(--spad); gap: var(--S); }
#lm .tool-bar h1 { flex: none; width: calc(6 * var(--S)); white-space: nowrap; }
/* the panel tabs share their seams; the open one is pressed in */
#panel-nav { gap: 0; }

/* ---- panels ------------------------------------------------------------ */
main { padding: var(--S) 0 0; max-width: none; }
main > section { display: flex; flex-direction: column; gap: var(--S); }
main > section[hidden] { display: none; }
#lm .panel-card { padding: var(--spad); display: flex; flex-direction: column; }

/* ---- Catalog & Groups -------------------------------------------------- */
.catalog-layout { grid-template-columns: minmax(0, 1fr) calc(12 * var(--S)); gap: var(--S); }
.catalog-layout > div:first-child { display: flex; flex-direction: column; gap: var(--S); }
#lm #part-search { width: calc(12 * var(--S) + var(--L)); flex: none; }

/* cards: 4 × 5 squares; art in the top three, caption in the bottom two,
   the caption's rule on a line */
.piece-grid {
  grid-template-columns: repeat(auto-fill, calc(4 * var(--S)));
  grid-auto-rows: calc(5 * var(--S));
  gap: var(--S);
}
#lm .btn.piece-card { width: calc(100% + var(--L)); height: calc(5 * var(--S) + var(--L)); }
#lm .piece-card > .face > .fill { padding: 0; font-size: inherit; }
.piece-card .thumb, .piece-card .thumb-missing {
  height: calc(3 * var(--S) - var(--L));
  aspect-ratio: auto;
  flex: none;
  padding: calc(var(--S) / 5);
}
.piece-card .pc-caption {
  height: calc(2 * var(--S));
  flex: none;
  margin-top: 0;
  justify-content: center;
  padding: 0 calc(var(--S) / 4);
  border-top-width: var(--L);
  border-top-color: var(--ink);
}

#lm .new-group-row { display: flex; gap: var(--S); margin: var(--S) 0 0; }
#lm .new-group-row input { flex: 1 1 auto; min-width: 0; }
#lm .group-list { gap: var(--S); margin: var(--S) 0 0; }
#lm .group-list:empty { display: none; }
#lm #group-empty { margin-top: var(--S); }
/* a group: three squares tall. Its name and counts get the full width of
   the top two squares; its buttons are one seamed segment tucked into the
   bottom-right corner, sharing the row's own bottom and right strokes. */
#lm .group-row {
  display: block;
  position: relative;
  height: calc(3 * var(--S) + var(--L));
  padding: calc(var(--S) / 2) var(--spad) 0;
}
#lm .group-row.is-open { background: var(--yellow); }
.group-row .gr-main { line-height: 1.25; }
#lm .gr-actions { position: absolute; right: 0; bottom: 0; display: flex; }
#lm .gr-actions > .btn { margin: 0; }
#lm .gr-actions > .btn + .btn { margin-left: calc(-1 * var(--L)); }

/* ---- colour swatches: one square each, seams shared, rows flush -------- */
#lm .swatch-row { display: flex; flex-wrap: wrap; margin: 0; }
#lm .swatch-row .btn.sq { width: var(--box1); height: var(--box1); }
#lm .swatch-section-label { margin-top: var(--S); }
#lm :is(#group-colors, #review-color-picker, #dlg-colour-palette) > .btn { margin-top: calc(var(--S) - var(--L) / 2); }
#group-colors, #review-color-picker, #dlg-colour-palette { display: flex; flex-direction: column; align-items: flex-start; }

/* ---- Sheet -------------------------------------------------------------- */
.sheet-layout { grid-template-columns: calc(13 * var(--S)) minmax(0, 1fr); gap: var(--S); }
.sheet-layout > div:last-child { display: flex; flex-direction: column; align-items: flex-start; }
#lm .sheet-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 var(--S); margin: 0; }
#lm .sheet-warning {
  margin-top: calc(var(--S) - var(--L) / 2);
  padding: calc(var(--S) / 4) calc(var(--S) / 3 + var(--L));
  background: #fff3cd; color: #6b4e00;
  line-height: calc(var(--S) / 2);
}
#lm .sheet-warning:empty { display: none; }
/* the page itself is drawn at its real proportions inside the frame */
#lm .sheet-frame { padding: var(--S); background: var(--tan); }
#lm .sheet-preview { border: 1px solid var(--card-border); }

/* ---- Template ----------------------------------------------------------- */
.template-layout { grid-template-columns: minmax(0, 1fr) calc(13 * var(--S)); gap: var(--S); }
/* one column when the window is narrow (these restate the earlier
   breakpoints, which the column rules above would otherwise override) */
/* the settings panels keep their width in squares when they stack (their
   two-column fields are laid out for it) */
.sheet-layout > .panel-card, .template-layout > .panel-card { max-width: calc(13 * var(--S) + var(--L)); }
@media (max-width: 1000px) {
  .catalog-layout, .template-layout { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 900px) {
  .sheet-layout { grid-template-columns: minmax(0, 1fr); }
}
#lm .stage { gap: 0; }
/* the label's frame: a square of room each side (bleed shows in it), the
   label's top-left corner on a line */
#lm .stage-frame { padding: var(--S); background: var(--tan); }
#lm .stage-canvas { margin: 0; }

/* the field editor: caption lines over control rows, whole squares */
#lm .field-editor, #lm .field-editor fieldset, #lm .fe-sec { gap: 0; }
#lm .fe-cap { display: flex; align-items: center; min-height: var(--S); line-height: 1; }
#lm .fe-geo { grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 0 var(--S); }
#lm .fe-geo input { width: auto; padding: 0 4px; }
#lm .fe-tools { gap: 0 var(--S); }   /* wrapped rows sit on the next line down */
#lm .fe-state { display: flex; align-items: center; min-height: var(--S); line-height: 1.1; }
#lm .fe-foot { margin-top: var(--S); }
#lm .add-field { gap: 0; margin: 0 0 var(--S); }
/* segmented rows: flush; the −L/2 margins share the seams */
#lm .seg { flex-wrap: wrap; padding: 0; }
#lm .seg > * { margin: var(--half); }
#lm .fe-size input[type="number"] { width: calc(2 * var(--S) + var(--L)); height: var(--box1); z-index: 1; }
#lm .fe-swatches { padding: 0; }
#lm .fe-swatches .btn.sq { width: var(--box1); height: var(--box1); margin: var(--half); }
#lm .fe-custom-wrap { display: inline-flex; }
#lm .fe-custom-wrap .btn.sq { margin: 0; }
#lm .fe-custom-wrap input[type="color"] { margin: 0; height: 100%; }

/* ---- Review & Export ---------------------------------------------------- */
#lm .review-grid { gap: var(--S); }
/* a tile: the label six squares wide on the lines, its caption a square
   under it; mountGridSnap rounds the tile to whole squares */
.review-tile { gap: 0; }
#lm .review-tile .rt-caption { line-height: var(--S); height: var(--S); flex: none; }
/* a page break: a dashed rule along a line, a square of caption under it */
#lm .page-break {
  flex: 0 0 calc(100% + var(--L));
  height: var(--box1);
  padding: 0 0 0 calc(var(--L) / 2);
  border-top: var(--L) dashed var(--ink);
  line-height: var(--S);
  font-size: 11px;
}
/* The BrickBatch overlay bands (overlay.js): in the Sheet preview at their
   printed place on the page, under the label cells; in the Review grid as a
   row opening and closing each page's tiles, at the tiles' own scale. */
.sheet-preview .sheet-overlay { position: absolute; display: block; pointer-events: none; user-select: none; }
#lm .page-band {
  flex: 0 0 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  pointer-events: none;
}
#lm .page-band img { display: block; max-width: 100%; height: auto; }
#lm .review-pin { display: flex; flex-direction: column; }
#lm .review-pin h3 { margin-top: 0; }
#review-note-editor { display: flex; flex-direction: column; gap: var(--S); max-width: calc(14 * var(--S)); }
#review-note-editor:empty { display: none; }

/* ---- the name row (Review note editor, dialog) ‹ [name] › -------------- */
#lm .name-row { gap: 0; }
#lm .name-row input { margin: var(--half); flex: 1 1 auto; min-width: 0; position: relative; z-index: 1; }
#lm .name-actions { gap: var(--S); }

/* ---- the per-label dialog ----------------------------------------------
   A box on the page grid: centred on the origin in whole squares (the
   width), its top on a line (app.js placeDialogOnGrid; absolute, so it
   stays on the grid as the page scrolls), its height whole squares. Head
   two squares, foot three (buttons centred on the middle one), body in
   between: the preview stays put (sticky) while the controls scroll. */
#lm #label-dialog {
  --dlg-half: min(450px, 45vw);   /* fallback for a browser without round() */
  --dlg-half: round(down, min(470px, 46vw), var(--S));
  border: var(--L) solid var(--ink);
  border-radius: 0;
  background: var(--panel);
  padding: 0;
  margin: 0;
  position: absolute;
  inset: auto;
  left: calc(var(--ox) - var(--dlg-half) - var(--L) / 2);
  top: calc(2 * var(--S) - var(--L) / 2);
  width: calc(2 * var(--dlg-half) + var(--L));
  max-width: none;
  max-height: none;
}
#lm #label-dialog[open] {
  height: min(88vh, 780px);
  height: calc(round(down, min(88vh, 780px) - var(--L), var(--S)) + var(--L));
}
dialog#label-dialog::backdrop { background: rgba(224, 207, 173, .75); }
#lm .dlg-head {
  height: calc(2 * var(--S));
  padding: 0 var(--pad);
  align-items: center; flex-wrap: nowrap; gap: var(--S);
  border-bottom: var(--L) solid var(--ink);
  overflow: hidden;
}
#lm .dlg-head .dlg-sub { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
#lm .dlg-body {
  /* the preview column's width is whole squares, measured from the left; the
     scrollbar lands in the slack on the right, never shifting a column */
  grid-template-columns: calc(2 * var(--dlg-half) - 14 * var(--S)) calc(11 * var(--S));
  grid-template-rows: none;
  align-items: start;
  gap: var(--S);
  padding: var(--pad) 0 var(--S) var(--pad);
  overflow-y: auto;
}
/* min-height stays auto: at 0 the rows were squeezed into the body's height
   and, stacked, the preview painted over the name controls below it */
#lm .dlg-body > .stage { position: sticky; top: 0; padding: 0; overflow: visible; min-height: auto; }
#lm .dlg-side { overflow: visible; padding: 0; min-height: auto; display: flex; flex-direction: column; }
#lm .dlg-side > * + * { margin-top: var(--S); }
#lm .dlg-side > h3 + * { margin-top: 0; }
#lm .dlg-foot {
  height: calc(3 * var(--S));
  padding: 0 var(--pad);
  gap: var(--S);
  border-top: var(--L) solid var(--ink);
}
#lm .colour-add { gap: 0; margin-bottom: 0; }
#lm #dlg-colour-picker:not([hidden]) { display: flex; flex-direction: column; }
@media (max-width: 860px) {
  /* stacked: the columns scroll as one, the preview with them */
  /* one column, still whole squares wide, the scrollbar in the slack */
  #lm .dlg-body { grid-template-columns: calc(2 * var(--dlg-half) - 3 * var(--S)); }
  #lm .dlg-side { max-width: calc(11 * var(--S)); }
  #lm .dlg-body > .stage { position: static; }
  #lm .dlg-foot { height: auto; min-height: calc(3 * var(--S)); flex-wrap: wrap; padding-block: calc(var(--S) - var(--L) / 2); }
}
