:root {
  --bg: #f4f5f7;
  --surface: #ffffff;
  --surface-2: #fafbfc;
  --line: #e3e5e9;
  --line-strong: #d0d4da;
  --text: #16181d;
  --muted: #6b7280;
  --accent: #2f7d4f;
  --accent-soft: #e7f3ec;
  --danger: #d4383d;
  --stage: #4c4c50;
  --stage-2: #2b2b2e;
  --grid: #e8eaee;
  --panel-fill: #ffffff;
  --dim: #2f7d4f;
  --shadow: 0 1px 2px rgba(16, 18, 21, .06), 0 8px 24px rgba(16, 18, 21, .06);
  --radius: 10px;
}

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #16181c;
    --surface: #1e2126;
    --surface-2: #23262c;
    --line: #2e323a;
    --line-strong: #3a3f48;
    --text: #e8eaed;
    --muted: #9aa1ad;
    --accent: #4ade80;
    --accent-soft: #1c2f24;
    --danger: #f87171;
    --stage: #4c4c50;
    --stage-2: #2b2b2e;
    --grid: #262a31;
    --panel-fill: #1a1d21;
    --dim: #4ade80;
    --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 8px 24px rgba(0, 0, 0, .35);
  }
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font: 14px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
}

#app { display: flex; flex-direction: column; height: 100%; }

/* ---------- top bar ---------- */
.topbar {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 12px;
  padding: 8px 14px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
}

.brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
.logo { color: var(--accent); font-size: 18px; }

.name-input {
  font: 600 15px/1.2 inherit;
  color: var(--text);
  background: transparent;
  border: 1px solid transparent;
  border-radius: 6px;
  padding: 4px 6px;
  max-width: 230px;
}
.name-input:hover { border-color: var(--line); }
.name-input:focus { outline: none; border-color: var(--accent); background: var(--surface-2); }

.status { color: var(--muted); font-size: 12px; white-space: nowrap; }
.status.busy { color: var(--accent); }

.viewtoggle {
  display: inline-flex;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 3px;
}
.vt {
  border: 0; background: transparent; color: var(--muted);
  font: 600 13px/1 inherit; padding: 7px 16px; border-radius: 6px; cursor: pointer;
  /* "Cut file" is two words and the tab strip is in a grid column that gives
     it exactly the room it asks for. Without this it wraps onto two lines at
     any laptop width and pushes the whole top bar taller. */
  white-space: nowrap;
}
.vt[aria-selected="true"] { background: var(--surface); color: var(--text); box-shadow: var(--shadow); }

.actions { display: flex; align-items: center; gap: 8px; justify-content: flex-end; }

button { font-family: inherit; }

.icon-btn {
  width: 32px; height: 32px; display: grid; place-items: center;
  border: 1px solid var(--line); background: var(--surface);
  color: var(--text); border-radius: 8px; cursor: pointer; font-size: 15px;
}
.icon-btn:hover:not(:disabled) { background: var(--surface-2); border-color: var(--line-strong); }
.icon-btn:disabled { opacity: .35; cursor: default; }

.primary {
  border: 0; background: var(--accent); color: #fff;
  font: 600 13px/1 inherit; padding: 9px 16px; border-radius: 8px; cursor: pointer;
}
@media (prefers-color-scheme: dark) { .primary { color: #0b1410; } }
.primary:hover { filter: brightness(1.06); }

.ghost {
  border: 1px solid var(--line-strong); background: var(--surface); color: var(--text);
  font: 600 13px/1 inherit; padding: 9px 14px; border-radius: 8px; cursor: pointer;
}
.ghost:hover { background: var(--surface-2); }

/* ---------- layout ---------- */
.layout { flex: 1; display: flex; min-height: 0; }

.stage {
  position: relative; flex: 1; min-width: 0;
  background: radial-gradient(125% 105% at 28% 18%, var(--stage) 0%, var(--stage-2) 78%);
}
/* The preview backdrop is the viewer's choice, independent of the UI theme. */
.stage[data-backdrop="light"] {
  --stage: #ffffff;
  --stage-2: #dde1e8;
  --dim-ink: #23272e;
  --dim-halo: rgba(255, 255, 255, .75);
  --dim-soft: rgba(35, 39, 46, .45);
  --board: #e6e9ef;
  --board-2: #ccd2dc;
  --board-line: #3a4048;
}
.stage[data-backdrop="dark"] {
  --dim-ink: #ffffff;
  --dim-halo: rgba(0, 0, 0, .55);
  --dim-soft: rgba(255, 255, 255, .45);
  --board: #7c828c;
  --board-2: #626873;
  --board-line: #eceef2;
}

.backdrop-pick {
  position: absolute; top: 14px; left: 14px; z-index: 5;
  display: flex; gap: 3px; padding: 4px;
  background: var(--surface); border: 1px solid var(--line);
  border-radius: 10px; box-shadow: var(--shadow);
}
.backdrop-pick button {
  width: 26px; height: 26px; padding: 0; cursor: pointer;
  border: 1.5px solid var(--line-strong); border-radius: 7px;
}
.backdrop-pick button:hover { border-color: var(--muted); }
.backdrop-pick button[aria-pressed="true"] {
  border-color: var(--accent);
  box-shadow: 0 0 0 2px var(--accent-soft);
}
.stage-pane { position: absolute; inset: 0; }
.stage-pane[hidden] { display: none; }

.stage-hint {
  position: absolute; left: 50%; bottom: 96px; transform: translateX(-50%);
  background: rgba(20, 22, 26, .82); color: #fff; font-size: 12px;
  padding: 6px 12px; border-radius: 999px; pointer-events: none;
  opacity: 0; transition: opacity .2s;
}
.stage-hint.show { opacity: 1; }

/* ---------- inspector ---------- */
.inspector {
  width: 296px; flex: none; overflow-y: auto;
  background: var(--surface); border-left: 1px solid var(--line);
  padding: 12px 14px 40px;
}
.insp-title { font: 600 13px/1 inherit; color: var(--muted); margin: 4px 0 12px; letter-spacing: .02em; }

.group { border: 1px solid var(--line); border-radius: var(--radius); margin-bottom: 10px; overflow: hidden; }
.group > summary {
  list-style: none; cursor: pointer; padding: 11px 12px;
  font: 600 13px/1 inherit; display: flex; justify-content: space-between; align-items: center;
  background: var(--surface-2);
}
.group > summary::-webkit-details-marker { display: none; }
.group > summary::after { content: "⌄"; color: var(--muted); transition: transform .15s; }
.group[open] > summary::after { transform: rotate(180deg); }
.group-body { padding: 12px; display: grid; gap: 12px; }

.field { display: grid; gap: 6px; }
.field > label { font-size: 12px; color: var(--muted); }
.row { display: flex; gap: 8px; align-items: center; }
.row > * { min-width: 0; }

input[type="text"], input[type="number"], select, textarea {
  width: 100%; padding: 7px 9px; font: inherit; font-size: 13px;
  color: var(--text); background: var(--surface);
  border: 1px solid var(--line-strong); border-radius: 7px;
}
input:focus, select:focus, textarea:focus { outline: 2px solid var(--accent-soft); border-color: var(--accent); }
textarea { resize: vertical; min-height: 58px; }

input[type="range"] { width: 100%; accent-color: var(--accent); }

.num { display: flex; align-items: center; gap: 6px; }
.num input { text-align: right; }
.num .unit { font-size: 11px; color: var(--muted); }

.seg { display: flex; border: 1px solid var(--line-strong); border-radius: 8px; overflow: hidden; }
.seg button {
  flex: 1; border: 0; background: var(--surface); color: var(--muted);
  font: 500 12px/1 inherit; padding: 8px 4px; cursor: pointer; white-space: nowrap;
}
.seg button + button { border-left: 1px solid var(--line-strong); }
.seg button[aria-pressed="true"] { background: var(--accent-soft); color: var(--accent); font-weight: 600; }

.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; }
.card {
  border: 1.5px solid var(--line-strong); border-radius: 10px; background: var(--surface);
  padding: 6px 5px 7px; cursor: pointer; text-align: center;
  font: 500 10.5px/1.25 inherit; color: var(--muted);
  min-width: 0;
}
.card .art {
  display: block; border-radius: 7px; background: var(--surface-2);
  padding: 3px 2px; margin-bottom: 7px;
}
.card svg { display: block; width: 100%; height: 46px; }
.card:hover { border-color: var(--muted); }
.card[aria-pressed="true"] { border-color: var(--accent); color: var(--accent); }
.card[aria-pressed="true"] .art { background: var(--accent-soft); }

/* A preset is named, not pictured, and three of the names are long enough that
   three columns would break each of them onto four lines. */
.presets { grid-template-columns: 1fr 1fr; }
.presets .card {
  padding: 9px 8px; min-height: 44px; display: grid; place-items: center;
  line-height: 1.3;
}

/* The ten patterns. Two columns, because a tessellation shown three across on a
   phone-width panel is a texture rather than a pattern - you cannot tell a
   six-point star from an eight-point one at 90 px, which is the entire decision
   this picker exists to support. The thumbnail is the engine's own strap
   network, so it is drawn as strokes rather than filled. */
.patterns { grid-template-columns: 1fr 1fr; }
.patterns .card .art { padding: 0; overflow: hidden; }
.patterns .card svg { height: 52px; }
.pat-thumb { color: var(--muted); opacity: .85; }
.card[aria-pressed="true"] .pat-thumb { color: var(--accent); opacity: 1; }

/* Typeface groups. The phone rule at the bottom of this file already assumes
   these exist. */
.cat-tabs { display: flex; gap: 4px; flex-wrap: wrap; }
.cat-tabs button {
  border: 1px solid var(--line-strong); background: var(--surface); color: var(--muted);
  font: 500 12px/1 inherit; padding: 7px 11px; border-radius: 999px; cursor: pointer;
}
.cat-tabs button[aria-pressed="true"] {
  background: var(--accent-soft); color: var(--accent); border-color: var(--accent);
  font-weight: 600;
}

/* The tipping verdict. Three states and they are not decoration: green is a
   number shown quietly, amber names the fix, red says the thing falls over. */
.verdict {
  display: flex; justify-content: space-between; align-items: baseline; gap: 8px;
  padding: 8px 10px; border-radius: 8px; font-size: 12px;
  border: 1px solid var(--line-strong); background: var(--surface-2);
}
.verdict b { font-size: 13px; }
.verdict span { color: var(--muted); }
.verdict-ok { border-color: var(--accent); background: var(--accent-soft); }
.verdict-ok b { color: var(--accent); }
.verdict-amber { border-color: #d98324; background: color-mix(in srgb, #d98324 12%, transparent); }
.verdict-amber b { color: #b06a12; }
.verdict-red { border-color: var(--danger); background: color-mix(in srgb, var(--danger) 12%, transparent); }
.verdict-red b { color: var(--danger); }

.swatch { width: 14px; height: 14px; border-radius: 4px; border: 1px solid rgba(0,0,0,.18); flex: none; }

.hint, .muted { color: var(--muted); font-size: 12px; }
.hint { margin: -4px 0 0; }
/* A hint the user must not skim past - the glue note, which is the difference
   between a stand that holds together and one that comes apart in their hands.
   Not .warn: nothing is wrong, it is an instruction. */
.hint-strong { color: var(--text); border-left: 2px solid var(--accent); padding-left: 8px; }
.warn { color: var(--danger); font-size: 12px; }

.tick-labels { display: flex; justify-content: space-between; font-size: 10px; color: var(--muted); }

.stat { display: flex; justify-content: space-between; font-size: 12px; color: var(--muted); }
.stat b { color: var(--text); font-weight: 600; }

.link { background: none; border: 0; color: var(--accent); font: inherit; font-size: 12px; cursor: pointer; padding: 0; text-decoration: underline; }

/* ---------- dialogs ---------- */
.dlg { border: 0; border-radius: 14px; padding: 0; background: var(--surface); color: var(--text); box-shadow: var(--shadow); max-width: 480px; width: calc(100% - 32px); }
/* The Help dialog is a page of prose - longer than the viewport on a laptop and
   far longer on a phone. Scroll it inside its own box, or the heading that says
   what you are reading is off the top of the screen. */
.dlg { max-height: 86vh; overflow-y: auto; }
.dlg::backdrop { background: rgba(10, 12, 15, .5); }
.dlg-body { padding: 20px; display: grid; gap: 14px; }
.dlg h2 { margin: 0; font-size: 17px; }
.dlg-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
.check { display: flex; gap: 8px; align-items: center; font-size: 13px; color: var(--muted); }
.layer-key { display: flex; gap: 14px; font-size: 12px; color: var(--muted); }
.layer-key i { display: inline-block; width: 10px; height: 10px; border-radius: 2px; margin-right: 5px; }
.note { font-size: 12px; color: var(--accent); margin: 0; }
.steps { margin: 0; padding-left: 20px; display: grid; gap: 8px; font-size: 13px; color: var(--muted); }
.steps li { color: var(--text); }
.steps li span { color: var(--muted); }

@media (max-width: 900px) {
  .inspector { width: 250px; }
  .topbar { grid-template-columns: auto 1fr auto; }
  .name-input { max-width: 130px; }
}

.text-area {
  width: 100%; min-height: 92px; resize: vertical;
  border: 1px solid var(--line-strong); border-radius: 8px;
  padding: 8px 9px; font: 13px/1.45 inherit; color: var(--text);
  background: var(--surface);
}
.text-area:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
/* One name, one line. The tall box belongs to the tools where the words ARE
   the object; here the words go inside a medallion. */
.name-line { min-height: 0; }

.faces-list { display: flex; flex-direction: column; gap: 5px; }
.faces-list { display: flex; flex-direction: column; gap: 4px; }
.face-opt {
  display: flex; align-items: center; gap: 8px; min-height: 42px;
  border: 1px solid var(--line); background: var(--surface);
  border-radius: 8px; padding: 6px 10px; cursor: pointer; text-align: left;
}
.face-opt:hover { background: var(--surface-2); }
.face-opt[aria-pressed="true"] {
  border-color: var(--accent); background: var(--accent-soft);
}
.face-opt .thumb { flex: 1; min-width: 0; display: block; }
.face-opt svg { display: block; width: 100%; height: 26px; }
.thumb-line path { fill: none; stroke: var(--text); stroke-width: .7; }
.thumb-fill path { fill: var(--text); stroke: none; fill-rule: nonzero; }
.face-opt[aria-pressed="true"] .thumb-line path { stroke: var(--accent); }
.face-opt[aria-pressed="true"] .thumb-fill path { fill: var(--accent); }

.pill {
  flex: none; font-size: 10px; font-weight: 700; padding: 2px 6px;
  border-radius: 999px; background: var(--surface-2); color: var(--muted);
}
.pill.warn { background: #fdecec; color: var(--danger); }
@media (prefers-color-scheme: dark) { .pill.warn { background: #3a1f21; } }

.stat b.bad { color: var(--danger); }

.midbar { display: flex; justify-content: center; }




/* The warnings sit on the drawing, not in the settings panel. They are about
   the piece, and the piece is what you are looking at - a note about a 1.4 mm
   neck three scroll-lengths down the inspector is a note nobody reads. Not
   clickable, so they never get in the way of the preview underneath. */
.warnings {
  position: absolute; left: 14px; right: 14px; bottom: 14px; z-index: 4;
  display: flex; flex-direction: column; gap: 6px; pointer-events: none;
}
.warnings[hidden] { display: none; }
.warn-box {
  background: var(--surface); color: var(--text);
  border: 1px solid var(--danger); border-left-width: 4px;
  border-radius: 8px; padding: 8px 12px; font-size: 12.5px; line-height: 1.45;
  box-shadow: var(--shadow);
}

/* ---------- the keychain itself ---------- */
/* Two views, two vocabularies. In the Piece view the board is painted in the
   colour of the sheet that was chosen, so the split ring, the engraving and
   the holes all read against a real material. In the Cut file view nothing is
   painted at all: it is the file, in the layer colours the laser will be
   given, so what is on screen is what the machine is being told to do. */
.preview { position: absolute; inset: 0; width: 100%; height: 100%; }

.board {
  fill: var(--board);
  stroke: var(--board-line);
  stroke-linejoin: round;
}
/* The split ring is drawn first and the board over it, so the half threaded
   through the hole is hidden. It exists to put the hole at the scale of the
   thing that goes in it, which no number in the inspector can do. */
.splitring { stroke: color-mix(in srgb, var(--dim-ink) 55%, transparent); }
/* Part of the cut file, not part of the keychain - the middle of an o on a
   plate with the name cut through it. Ghosted, because that is exactly what it
   is: material that will be there on the bed and gone from the finished piece. */
.dropout {
  fill: color-mix(in srgb, var(--board) 55%, transparent);
  stroke: var(--board-line);
  stroke-dasharray: 1.6 1.4;
  opacity: .6;
}
/* Engraving on the board. A burn is soft and never quite black - a laser
   darkens the surface, it does not print on it - and which way it goes is set
   in JavaScript from the colour of the sheet, not from the backdrop. See
   burnFor() in view.js: on a light board the mark is dark, on a dark one the
   beam frosts the surface and the mark is pale. The fallback here is the dark
   one, for the board colours that never reach the script. */
.burn { stroke: var(--burn, rgba(42, 33, 24, .74)); }
.burn-fill { fill: var(--burn, rgba(42, 33, 24, .74)); }
/* Where it will snap. The inspector gives the number; this says where, which
   is the half that tells you which control to reach for. */
.weak { fill: none; stroke: var(--danger); opacity: .9; }

/* The cut file view, in the three layer colours the export writes. */
.cut { stroke: #ff0000; fill: none; }
.engrave { stroke: #0000ff; fill: none; }
.engrave-fill { fill: #000000; }
.stage[data-backdrop="dark"] .engrave-fill { fill: #dfe3ea; }

/* Five pieces off one bed, two of them the same shape. Naming them on the
   drawing is the difference between an assembly and a pile of plywood. */
.panel-tag { fill: var(--dim-soft, #888); font-family: inherit; }

/* ---------- dimensions over the 3D view ---------- */
/* An SVG overlay rather than lines in the scene, so the arrowheads and the type
   stay crisp at any zoom and never take the perspective. */
.dim-overlay {
  position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none;
}
.dim-line, .dim-ext { stroke: var(--dim-soft, rgba(255,255,255,.45)); stroke-width: 1; }
.dim-head { fill: var(--dim-soft, rgba(255,255,255,.45)); }
.dim-text {
  fill: var(--dim-ink, #fff); font: 600 11px/1 ui-sans-serif, system-ui, sans-serif;
  text-anchor: middle;
  paint-order: stroke; stroke: var(--dim-halo, rgba(0,0,0,.55)); stroke-width: 3px;
  stroke-linejoin: round;
}

/* ---- saved designs ------------------------------------------------------ */
/* A list of things you made, not a control panel: the name leads, the date
   trails behind it quietly, and the delete stays out of the way until the row
   is under the pointer. */
.row-actions { display: flex; gap: 6px; }
.row-actions .wide { flex: 1; }

.design-list {
  list-style: none; margin: 2px 0 0; padding: 0;
  display: flex; flex-direction: column; gap: 4px;
}
.design {
  display: flex; align-items: center; gap: 4px;
  border: 1px solid var(--line); border-radius: 9px; background: var(--surface);
}
.design:hover { border-color: var(--line-strong); }
.design.open { border-color: var(--accent); background: var(--accent-soft); }

.design-open {
  flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px;
  border: 0; background: transparent; cursor: pointer;
  padding: 7px 4px 7px 9px; text-align: left; font: inherit; color: inherit;
}
.design-name {
  font-size: 12.5px; font-weight: 600; color: var(--text);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.design-when { font-size: 11px; color: var(--muted); }

.design .icon-btn { opacity: 0; margin-right: 4px; }
.design:hover .icon-btn, .design .icon-btn:focus-visible { opacity: 1; }

.warn-line {
  margin: 0; font-size: 12px; color: var(--danger);
}

/* ---- save dialog -------------------------------------------------------- */
.field-label { font-size: 12px; font-weight: 600; color: var(--muted); }
.name-field {
  width: 100%; padding: 9px 11px; font: inherit; font-size: 14px;
  border: 1.5px solid var(--line-strong); border-radius: 9px;
  background: var(--surface); color: var(--text);
}
.name-field:focus { outline: none; border-color: var(--accent); }

/* The gallery can hold a lot of rows; the dialog scrolls rather than growing
   past the bottom of the screen. */
#filesDlg .design-list { max-height: 46vh; overflow-y: auto; }

/* ---- three actions, three weights --------------------------------------- */
/* Export, Save and the gallery are not equal, so they must not look equal.
   Export is the finish line and keeps the filled accent. Save is the same
   family one step down - clearly related, clearly not the main event. The
   gallery is navigation rather than an action, so it stays quietest of all.
   One hue throughout: a second colour here would be three shouting at once. */
.soft {
  border: 1px solid var(--accent); background: var(--accent-soft);
  color: var(--accent);
  font: 600 13px/1 inherit; padding: 9px 14px; border-radius: 8px; cursor: pointer;
}
.soft:hover { filter: brightness(0.97); }

.quiet { display: inline-flex; align-items: center; gap: 6px; }
.quiet svg { width: 14px; height: 14px; opacity: 0.75; }

/* ---------- phone ---------- */
/* The side-by-side layout stops working well below 640px: the stage collapses
   to a sliver and the top bar's three columns run off the edge. So stack it -
   preview on top at the full width of the screen, settings in a scrolling
   panel underneath - and give the top bar two rows so nothing collides.
   Nothing here touches the tablet (900px) or desktop layout. */
@media (max-width: 640px) {
  /* top bar: brand and view tabs share row one, the buttons take row two. */
  .topbar {
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "brand   mid"
      "actions actions";
    gap: 6px 8px;
    padding: 6px 10px;
  }
  .brand { grid-area: brand; gap: 6px; }
  .midbar, .topbar > .viewtoggle { grid-area: mid; }
  .actions { grid-area: actions; flex-wrap: wrap; row-gap: 6px; gap: 6px; }

  .logo { font-size: 16px; }
  /* 16px, not 14. Safari zooms the whole page when a field under 16px takes
     focus, and the layout that was just fitted into 375px is then wider than
     the screen. This field is written with no type attribute, so the
     input[type=text] rule that carries 16px never reached it. */
  .name-input { flex: 1 1 auto; min-width: 0; max-width: none; font-size: 16px; }
  /* The autosave indicator - Saving... / Saved. Dropped on a phone for room,
     and that is a real loss rather than a duplicate of the Save button: Save
     only opens the dialog and never reports autosave state. The top bar is
     already two rows tall here, so the room wins. */
  .status { display: none; }

  .vt { padding: 9px 11px; font-size: 12px; }

  /* The size readout is information, not a button; it gets its own quiet line
     rather than fighting the buttons for horizontal room. */
  .actions > #sizeReadout { flex: 1 0 100%; order: -1; font-size: 11.5px; }

  .icon-btn { width: 38px; height: 38px; font-size: 16px; }
  .primary, .ghost, .soft { padding: 11px 14px; }

  /* "Reka bentuk saya" is the widest thing in the row, so it gets the small
     type - but it keeps its words. The sibling tools collapse this to a folder
     icon; this tool's button carries no icon, and a button that renders as an
     empty white box is worse than a wide one. */
  #filesBtn { padding: 11px 10px; font-size: 12px; }

  /* ---- stacked layout ---- */
  .layout { flex-direction: column; }

  .stage {
    flex: 5 1 0;
    min-height: 240px;
    /* dimension arrows are drawn outside the drawing box; on a phone the
       settings panel is directly below them, so clip. */
    overflow: hidden;
  }

  .inspector {
    flex: 4 1 0;
    width: auto;
    min-height: 0;
    border-left: 0;
    border-top: 1px solid var(--line);
    padding: 10px 12px 28px;
    -webkit-overflow-scrolling: touch;
  }
  .insp-title { margin: 2px 0 8px; }

  /* ---- finger-sized controls ---- */
  .group > summary { padding: 14px 12px; }
  input[type="text"], input[type="number"], select, textarea {
    /* 16px also stops iOS zooming the page when a field takes focus. */
    font-size: 16px; padding: 10px 11px;
  }
  .seg button { padding: 12px 4px; font-size: 13px; }
  .cat-tabs button { padding: 9px 13px; }
  .face-opt { min-height: 48px; }
  input[type="range"] { height: 28px; }

  /* The dimension callouts cluster along the bottom and right of the drawing
     once it fills a phone-width stage, so the hint moves clear of them. */
  .stage-hint {
    top: 12px; bottom: auto; max-width: calc(100% - 110px);
    text-align: center; font-size: 11px; padding: 5px 10px;
  }

  /* ---- dialogs ---- */
  /* A row of four or five export buttons is wider than the screen, so the row
     wraps into pairs instead of pushing the primary action off the edge. */
  .dlg-body { padding: 16px; gap: 12px; }
  .dlg-actions { flex-wrap: wrap; gap: 8px; }
  .dlg-actions button { flex: 1 1 42%; min-width: 0; }
  .dlg-actions .ghost.wide { padding: 11px 12px; }
  .layer-key { flex-wrap: wrap; gap: 6px 14px; }
  #filesDlg .design-list { max-height: 40vh; }
}


/* Two panes in the one stage, one shown at a time. The 3D canvas keeps its
   size while hidden rather than being torn down and rebuilt on every tab
   press: WebGL context creation is the slow part, and a tab that takes half
   a second to come back reads as a tool that is thinking. */
.stage-pane[hidden] { display: none !important; }
#stage3d { position: absolute; inset: 0; }
#stage3d canvas { display: block; width: 100%; height: 100%; }
