/* SiloCut — application chrome.
 *
 * Palette lifted from conmen_gallery/public/styles.css so this wears the same neutral dark
 * greys and the same single muted accent. Accent is for selection and focus only — never
 * decoration. No @import, no external fonts, no url() to another origin.
 */

:root {
  --bg: #131315;
  --bg-2: #191a1d;
  --bg-3: #1f2024;
  --line: #2c2e33;
  --line-2: #3a3d44;
  --fg: #e8e8ea;
  --fg-2: #a5a7ad;
  --fg-3: #74777e;
  --accent: #6f8fd0;
  --danger: #d08a8a;
  --ok: #7fb08a;
  /* Between the two: something worth reading that is not something going wrong. Undo shortening
     itself to keep the tab alive is the case it exists for — red would read as a failure and the
     default grey reads as nothing. */
  --warn: #d0b184;
  --radius: 4px;

  --stage-bg: #0d0d0f;
  --check-a: #232327;
  --check-b: #2a2a2e;

  --tools-w: 46px;
  /* Kept in step with PANEL_DEFAULT in ui/shell.js, which overwrites this on boot. This value is
     what the page renders with for the one frame before the script runs — a different number here
     is a visible jump on every load. */
  --panels-w: 370px;
  --controls-w: 264px;
  --rail-w: 42px;
  --bar-h: 40px;
  /* How far the tool overlay extends past the picture, so a handle sitting ON the edge is drawn
     whole and can be grabbed from outside. Read by styles.css and by tools.js — keep them equal. */
  --bleed: 32px;

  --font: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
  --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}

* { box-sizing: border-box; }

html, body {
  height: 100%;
  margin: 0;
  overflow: hidden;
}

body {
  display: grid;
  /* minmax(0,·) on BOTH axes, everywhere. A grid child defaults to min-width/min-height:auto,
     which means "as wide as my content" — so a 177-cell filmstrip stretched the whole body to
     ~13,000px and parked the panels and the artboard off-screen. Every track that can hold
     overflowing content is pinned to 0 below, so it scrolls inside itself instead. */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: var(--bar-h) minmax(0, 1fr) auto 26px;
  font-family: var(--font);
  font-size: 13px;
  line-height: 1.45;
  color: var(--fg);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
}

h2 { font-size: 11px; font-weight: 600; margin: 0; }

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

.spacer { flex: 1; }

/* ── top bar ──────────────────────────────────────────────────────────── */

.topbar {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 0 12px;
  background: var(--bg-2);
  border-bottom: 1px solid var(--line);
}

.wordmark {
  font-weight: 600;
  letter-spacing: 0.02em;
}

.doc-name {
  color: var(--fg-3);
  font-size: 12px;
  padding-left: 14px;
  border-left: 1px solid var(--line);
  max-width: 40ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.btn {
  height: 26px;
  padding: 0 12px;
  font: inherit;
  font-size: 12px;
  color: var(--fg);
  background: var(--bg-3);
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  cursor: pointer;
}
.btn:hover { background: #26272c; }
.btn:active { transform: translateY(1px); }

.btn.primary {
  border-color: color-mix(in srgb, var(--accent) 55%, var(--line-2));
  background: color-mix(in srgb, var(--accent) 18%, var(--bg-3));
}
.btn.primary:hover { background: color-mix(in srgb, var(--accent) 28%, var(--bg-3)); }

/* ── the account control ──────────────────────────────────────────────────
 *
 * A plain `.btn`, deliberately not `primary`: Export owns the one bright thing in this bar, and
 * signing in is optional in a way exporting is not. No new colour, no badge, no dot.
 *
 * ⚠️ EVERY CLASS HERE THAT CARRIES A `display` ALSO CARRIES ITS OWN `[hidden]` RULE. A `display`
 * on a class outranks the UA sheet's `[hidden] { display: none }` — the bug this file has already
 * hit four times (.ctl-block, .ctl-steps, .move-frames, .step-body) — and all three of these are
 * hidden by ATTRIBUTE, from publish.js: the wrapper and the button whenever /api/me has not
 * answered or cannot be reached, the avatar whenever there is no picture. Without the guards a
 * signed-out editor would carry an empty pill in the top bar and a broken image inside it. */
.account { display: flex; align-items: center; margin-right: 8px; }
.account[hidden] { display: none; }

.account-btn {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* A Discord username can be 32 characters. Capped and clipped so a long one cannot push Export
     off the end of the bar — the corner belongs to Export. */
  max-width: 24ch;
  overflow: hidden;
}
.account-btn[hidden] { display: none; }

.account-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

/* Sized here rather than trusted to arrive small: the src is a URL from an API response, and an
   image that decides its own size in the top bar would reflow the whole row. */
.account-avatar { display: block; width: 16px; height: 16px; border-radius: 50%; object-fit: cover; }
.account-avatar[hidden] { display: none; }

/* ── work area ────────────────────────────────────────────────────────── */

.work {
  display: grid;
  /* tools · controls · stage · HANDLE · panels */
  grid-template-columns: var(--tools-w) var(--controls-w) minmax(0, 1fr) 5px var(--panels-w);
  min-width: 0;
  min-height: 0;
}

/* The drag handle between the stage and the panels.
 *
 * 5px wide but with a wider invisible grab area either side, because a 5px target is a target you
 * miss. The ::before is what you actually hit; the strip you see is the element itself. */
.col-resize {
  position: relative;
  z-index: 3;
  background: var(--line);
  cursor: col-resize;
  touch-action: none;
}
.col-resize::before {
  content: '';
  position: absolute;
  top: 0;
  bottom: 0;
  left: -4px;
  right: -4px;
}
.col-resize:hover,
.col-resize:focus-visible,
.col-resize.is-dragging { background: var(--accent); outline: none; }

/* While dragging, kill text selection and force the cursor everywhere — without this the pointer
   flickers between col-resize and whatever it happens to be over, and dragging across the canvas
   selects the page. */
body.is-resizing { cursor: col-resize; user-select: none; }
body.is-resizing * { cursor: col-resize !important; }

/* tools */

.tools {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: 6px 0;
  background: var(--bg-2);
  border-right: 1px solid var(--line);
}

.tool {
  width: 32px;
  height: 32px;
  display: grid;
  place-items: center;
  color: var(--fg-2);
  background: transparent;
  border: 1px solid transparent;
  border-radius: var(--radius);
  cursor: pointer;
}
.tool svg { width: 20px; height: 20px; fill: currentColor; }
.tool:hover:not(:disabled) { background: var(--bg-3); color: var(--fg); }
.tool.is-active {
  background: color-mix(in srgb, var(--accent) 22%, var(--bg-3));
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
  color: var(--fg);
}
.tool:disabled { opacity: 0.3; cursor: default; }

.tool-gap {
  width: 22px;
  height: 1px;
  margin: 6px 0;
  background: var(--line);
}

/* ── control panel ────────────────────────────────────────────────────── */

.controls {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  overflow-y: auto;
  background: var(--bg-2);
  border-right: 1px solid var(--line);
}

.ctl-block {
  padding: 12px;
  border-bottom: 1px solid var(--line);
}

.ctl-h {
  margin: 0 0 6px;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--fg-2);
}

.ctl-note {
  margin: 0 0 9px;
  font-size: 11.5px;
  line-height: 1.4;
  color: var(--fg-3);
}

.ctl-hint {
  margin: 0 0 8px;
  padding: 7px 9px;
  font-size: 11px;
  line-height: 1.45;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-radius: 3px;
}
.ctl-hint b { color: var(--fg); }

/* ── the guided rail: his six steps as an accordion ───────────────────────
 *
 * One step open at a time. The collapsed rows carry the NUMBER, the TITLE and the SUMMARY of
 * what was actually chosen — never a tick, which says only "yes" to a question nobody asked.
 *
 * ⚠️ `.step-body` DECLARES NO `display`, AND THAT IS DELIBERATE — READ THIS BEFORE ADDING ONE.
 * The body is collapsed with the `hidden` ATTRIBUTE from controls.js. Any `display` declared on
 * the class outranks the UA sheet's `[hidden] { display: none }`, and every step would then be
 * permanently open — the accordion would look like the seven-section scroll it replaced. This
 * file already carries `.ctl-block[hidden]`, `.ctl-steps[hidden]` and the warning on
 * `.move-frames` to undo exactly that mistake three times over. The belt-and-braces rule below
 * is the fourth. test/guidesteps.test.mjs asserts it, because Node cannot see a stylesheet run.
 */
.guide-lead {
  margin: 0;
  padding: 10px 12px;
  font-size: 11px;
  line-height: 1.45;
  color: var(--fg-3);
  border-bottom: 1px solid var(--line);
}

.step { border-bottom: 1px solid var(--line); }
.step-h { margin: 0; font-size: inherit; font-weight: inherit; }

.step-toggle {
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 10px 12px;
  font: inherit;
  text-align: left;
  color: var(--fg-2);
  background: none;
  border: 0;
  cursor: pointer;
}
.step-toggle:hover { background: var(--bg-3); color: var(--fg); }

/* The step number, always visible, always in the same place. It is the only thing on a collapsed
   row that says "this is a sequence" — the reason the panel is an accordion and not a menu. */
.step-n {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 19px;
  height: 19px;
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--fg-3);
  background: var(--bg-3);
  border: 1px solid var(--line-2);
  border-radius: 50%;
}

.step-label { min-width: 0; display: flex; flex-direction: column; gap: 1px; }

.step-title {
  font-size: 11.5px;
  text-transform: uppercase;
  letter-spacing: 0.07em;
}

/* WHAT HE CHOSE, not a tick. "30 frames · 512×512" is worth reading; a green check is not.
   Clipped rather than wrapped: a summary that reflows pushes the next step's header down and the
   rail stops being a fixed set of rows you can aim at. */
.step-sum {
  font-size: 10.5px;
  color: var(--fg-3);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.step-sum:empty { display: none; }

.step.is-open > .step-h > .step-toggle { color: var(--fg); background: var(--bg-3); }
.step.is-open > .step-h > .step-toggle .step-n {
  color: var(--fg);
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 45%, transparent);
}

/* Done is carried by the NUMBER filling in, not by a separate glyph. The summary beside it is the
   real report; this is only the at-a-glance. */
.step.is-done > .step-h > .step-toggle .step-n {
  color: var(--bg);
  background: var(--accent);
  border-color: var(--accent);
}
.step.is-done > .step-h > .step-toggle .step-sum { color: var(--fg-2); }

.step-body { padding: 0 12px 12px; }
.step-body[hidden] { display: none; }

/* The ONE plain line saying why a step's buttons are off. Distinct from .ctl-note so it does not
   read as more instructions — it is the answer to "why can I not press this". */
.step-gate {
  margin: 0 0 9px;
  padding: 7px 9px;
  color: var(--fg-2);
  background: var(--bg-3);
  border-left: 2px solid var(--line-2);
  border-radius: 0 3px 3px 0;
}
.step-gate[hidden] { display: none; }

/* "Across the frames" moved in here from the Move & resize panel, where its top border separated
   it from the flip and resize buttons above it. In step 3 it is usually the ONLY content, and a
   rule across the top of a lone group is a divider between nothing and nothing.
   ⚠️ A DESCENDANT RULE ON PURPOSE — nothing is added to `.move-frames` itself, which is guarded
   by test/keyframecontrols.test.mjs against picking up a `display` and never hiding again. */
.step-body .move-frames { margin-top: 0; padding-top: 0; border-top: 0; }
.step-body .ctl-sub:first-child { margin-top: 0; }

/* ── step 6's history: everything he has made ──────────────────────────────
 *
 * ROWS, NOT TILES, AND THAT IS THE WHOLE LAYOUT DECISION. The panel is 264px wide by default
 * (--controls-w), and each entry has to carry a name plus four facts that tell two similar
 * exports apart — "512 × 512 · 30 frames · 412 KB · 2 minutes ago". Tiled at that width you get
 * two columns of about 120px and every one of those facts wraps or truncates, which is a grid
 * that looks like a gallery and cannot be read. A 48px picture with two lines beside it survives
 * any panel width the drag handle can produce.
 *
 * ⚠️ EVERY HIDDEN THING IN HERE THAT CARRIES A `display` ALSO CARRIES ITS OWN `[hidden]` RULE.
 * A `display` on a class outranks the UA sheet's `[hidden] { display: none }`, and this codebase
 * has now hit that exact bug four times (.ctl-block, .ctl-steps, .move-frames, .step-body). Only
 * .gal-grid genuinely needs the guard today; the other three are written anyway so that adding a
 * `display` to them later cannot silently re-open a panel that is supposed to be gone. */
.gallery {
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid var(--line);
}

.gal-grid {
  display: grid;
  gap: 6px;
  margin: 0 0 9px;
}
.gal-grid[hidden] { display: none; }
.gal-note[hidden] { display: none; }
.gal-local[hidden] { display: none; }
.gal-clear[hidden] { display: none; }

/* Publish, and the line under it. Same guard as everything above, and the button genuinely needs
   it: `.ctl-btn` is full-width, this makes it a block, and a `display` on a class outranks the UA
   sheet's `[hidden] { display: none }` — publish.js hides the button by attribute the moment the
   gallery API stops answering, so without the guard an offline editor would show a Publish button
   that cannot publish.

   The hint is blanked to '' rather than hidden, so `:empty` is what stops its 9px margin from
   opening a gap under a control with nothing to say. The `[hidden]` rule is written anyway: adding
   a `display` here later must not be able to re-open a line that is supposed to be gone. */
.publish-btn { display: block; margin: 0 0 6px; }
.publish-btn[hidden] { display: none; }
.publish-hint[hidden] { display: none; }
.publish-hint:empty { display: none; }

/* The name of the file is the accent, not the row. A list where every row glows is a list with no
   emphasis left to spend. */
.gal-local {
  margin: 9px 0 0;
  font-size: 10.5px;
  color: var(--fg-3);
}

.gal-item {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: stretch;
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.gal-item:hover { border-color: var(--line-2); }

.gal-open {
  display: grid;
  grid-template-columns: 48px 1fr;
  align-items: center;
  gap: 8px;
  min-width: 0;
  padding: 5px;
  font: inherit;
  color: var(--fg-2);
  text-align: left;
  background: none;
  border: 0;
  border-radius: 3px 0 0 3px;
  cursor: pointer;
}
.gal-open:hover { background: var(--bg-3); color: var(--fg); }

.gal-shot {
  display: grid;
  place-items: center;
  width: 48px;
  height: 48px;
  overflow: hidden;
  background: var(--bg-3);
  border-radius: 3px;
}
.gal-shot img { display: block; max-width: 100%; max-height: 100%; }

/* No thumbnail is not a broken image — an older record may simply predate them. An empty frame
   says "no picture"; a broken-image glyph would say "this is damaged", which is a lie. */
.gal-shot.is-blank { border: 1px dashed var(--line-2); }

.gal-meta { display: grid; gap: 2px; min-width: 0; }

/* min-width:0 above plus the ellipsis here is what stops a long name from widening the whole
   grid column and pushing the × off the panel. */
.gal-name {
  font-size: 11.5px;
  color: var(--fg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.gal-facts {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--fg-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Its own hit area and its own accessible name — it is a second action on the row, not a glyph
   decorating the first one. */
.gal-x {
  padding: 0 9px;
  font: inherit;
  font-size: 14px;
  line-height: 1;
  color: var(--fg-3);
  background: none;
  border: 0;
  border-left: 1px solid var(--line);
  border-radius: 0 3px 3px 0;
  cursor: pointer;
}
.gal-x:hover { color: var(--danger); background: var(--bg-3); }

/* Export controls. The select is full width because its longest option is "Discord sticker —
   animated" and a truncated preset name is a preset nobody picks. */
.ctl-select {
  width: 100%;
  height: 28px;
  padding: 0 8px;
  margin-bottom: 8px;
  font: inherit;
  font-size: 12px;
  color: var(--fg);
  background: var(--bg-3);
  border: 1px solid var(--line);
  border-radius: 3px;
  cursor: pointer;
}
.ctl-select:disabled { color: var(--fg-3); cursor: default; }

/* The measured result. Green when the file is within its limit, red when it is not — and it
   still says the file was written, because a sticker 40 KB over is worth having while you
   decide what to cut. */
.ctl-result {
  margin: 8px 0 0;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--fg-2);
}
.ctl-result[data-tone='ok'] { color: var(--ok); }
.ctl-result[data-tone='error'] { color: var(--danger); }
.ctl-result[hidden] { display: none; }

/* ── Build a Con overlay ──────────────────────────────────────────────────
 *
 * Same two-column shape as the export overlay: what you are making on the left, the choices on the
 * right. Deliberately plain — the 171 pieces of real art are the only colour in here, and chrome
 * competing with them would make the swatches harder to read, not the panel more impressive.
 */

/* `.modal-card.con-card`, not `.con-card` — the base `.modal-card` rule is defined LATER in this
   file and wins at equal specificity, which is the trap the export card already fell into. */
.modal-card.con-card {
  width: min(1100px, 100%);
  height: min(780px, 100%);
}

.cp-body {
  display: grid;
  grid-template-columns: 300px minmax(0, 1fr);
  min-height: 0;
  flex: 1;
}

.cp-preview {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px;
  border-right: 1px solid var(--line);
  min-height: 0;
  overflow-y: auto;
}

/* The checkerboard, so a transparent background reads as transparent rather than as black. */
.cp-stage {
  background:
    repeating-conic-gradient(var(--check-a) 0 25%, var(--check-b) 0 50%) 0 0 / 16px 16px;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
}

.cp-stage canvas { display: block; width: 100%; height: auto; }

.cp-rolls { display: flex; gap: 6px; }
.cp-rolls .ctl-btn { width: auto; flex: 1; min-width: 0; }

.cp-rarity { margin: 0; font-size: 12.5px; color: var(--fg); }
.cp-rarity-sub { margin: 0; font-size: 11px; color: var(--fg-3); font-family: var(--mono); }

.cp-breakdown {
  margin: 2px 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  gap: 2px;
  font-size: 10.5px;
  color: var(--fg-3);
}

.cp-breakdown li { display: flex; justify-content: space-between; gap: 8px; }
.cp-breakdown li span:first-child { color: var(--fg-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cp-breakdown li span:last-child { font-family: var(--mono); flex: none; }

.cp-choices { display: flex; flex-direction: column; min-height: 0; }

.cp-pickers { overflow-y: auto; padding: 12px 14px; display: grid; gap: 14px; min-height: 0; }

.cp-picker { display: grid; gap: 6px; }

.cp-head { display: flex; align-items: baseline; gap: 8px; }
.cp-name { font-size: 11.5px; font-weight: 600; color: var(--fg); }
.cp-value { font-size: 11px; color: var(--fg-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.cp-occluded {
  font-size: 9.5px;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--danger);
  border: 1px solid var(--danger);
  border-radius: 2px;
  padding: 0 4px;
  flex: none;
  cursor: help;
}
.cp-occluded[hidden] { display: none; }

/* A type the chosen Headwear covers. Dimmed rather than removed: the choice is still yours and
   still remembered, it just is not being painted right now. Removing the row would make taking the
   mask off feel like the app had forgotten what was underneath. */
.cp-picker.is-occluded .cp-list { opacity: 0.35; }
.cp-picker.is-occluded .cp-name { color: var(--fg-3); }

.cp-list {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
  gap: 5px;
}

.cp-swatch {
  display: grid;
  gap: 2px;
  justify-items: center;
  padding: 4px 3px 5px;
  background: var(--bg-2);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  cursor: pointer;
  color: var(--fg-3);
  font: inherit;
  text-align: center;
}

.cp-swatch:hover { background: var(--bg-3); }

.cp-swatch.is-selected {
  border-color: var(--accent);
  background: var(--bg-3);
  color: var(--fg);
}

.cp-thumb {
  width: 100%;
  aspect-ratio: 1;
  display: grid;
  place-items: center;
  background:
    repeating-conic-gradient(var(--check-a) 0 25%, var(--check-b) 0 50%) 0 0 / 10px 10px;
  border-radius: 2px;
  overflow: hidden;
}

.cp-thumb img { display: block; width: 100%; height: 100%; object-fit: contain; }
.cp-thumb.is-none { color: var(--fg-3); font-size: 14px; background: var(--bg); }

.cp-swatch-name {
  font-size: 9.5px;
  line-height: 1.2;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.cp-share { font-family: var(--mono); font-size: 8.5px; color: var(--fg-3); }
/* "empty", not "0.0%". A synthetic none has a share of zero because no Con is missing its skin,
   and a percentage there would read as the rarest trait in the collection. */
.cp-share.is-empty { font-family: var(--font); font-style: italic; }

.cp-missing { margin: 0; padding: 10px 14px; border-top: 1px solid var(--line); flex: none; }

/* ── export overlay ───────────────────────────────────────────────────────
 *
 * Two columns: what you are making on the left, which frames go into it on the right. They are
 * side by side because the choice is one choice — picking frames without seeing them play is how
 * you end up exporting the wrong twenty.
 */

/* `.modal-card.export-card`, not `.export-card`: the base `.modal-card` rule is defined LATER in
   this file, so at equal specificity it won the width and this card silently stayed 880px wide
   while its height came from here. Two halves of one box, decided in two places. */
.modal-card.export-card {
  width: min(1100px, 100%);
  height: min(760px, 100%);
}

.ex-body {
  display: grid;
  grid-template-columns: 320px minmax(0, 1fr);
  gap: 0;
  flex: 1;
  min-height: 0;
}

.ex-preview {
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 14px 16px;
  border-right: 1px solid var(--line);
  overflow-y: auto;
}

/* The checkerboard, so a transparent cut-out reads as transparent rather than as white. */
.ex-stage {
  display: grid;
  place-items: center;
  padding: 8px;
  background-color: var(--check-a);
  background-image:
    linear-gradient(45deg, var(--check-b) 25%, transparent 25%),
    linear-gradient(-45deg, var(--check-b) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, var(--check-b) 75%),
    linear-gradient(-45deg, transparent 75%, var(--check-b) 75%);
  background-size: 16px 16px;
  background-position: 0 0, 0 8px, 8px -8px, -8px 0;
  border: 1px solid var(--line);
  border-radius: 4px;
}
.ex-stage canvas { display: block; max-width: 100%; height: auto; }

.ex-transport {
  display: flex;
  align-items: center;
  gap: 10px;
}
.ex-transport .ctl-btn { width: auto; min-width: 92px; }
.ex-pos { font-family: var(--mono); font-size: 11px; color: var(--fg-3); }

.ex-field { display: grid; gap: 4px; font-size: 11.5px; color: var(--fg-3); }
.ex-field .ctl-select { margin-bottom: 0; }

/* The faded-layer warning, sitting under #exHint in the export panel. It borrows .ctl-hint's box
   and only re-tones it: this is a FACT about the file he is about to make, not a tip about the
   format, so it must not read as more of the same blue advice directly above it.

   ⚠️ NO `display` PROPERTY IN HERE, and none in .ctl-hint either. A `display` on the class
   outranks the UA sheet's `[hidden] { display: none }`, and this element is hidden by attribute
   on every clean export — an empty padded box would then sit in the panel permanently. This
   file has already had to undo exactly that mistake twice, with `.ctl-block[hidden]` and
   `.ctl-steps[hidden]`; test/opacity.test.mjs asserts the absence rather than trusting it. */
.ex-warn {
  color: var(--warn);
  background: color-mix(in srgb, var(--warn) 12%, transparent);
}
.ex-warn b { color: var(--fg); }

.ex-frames { display: flex; flex-direction: column; min-height: 0; }

.ex-frames-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 11px 16px;
  border-bottom: 1px solid var(--line);
}

.ex-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
  gap: 6px;
  padding: 12px 16px;
  overflow-y: auto;
  min-height: 0;
  align-content: start;
}
/* A still format uses one frame, so the grid stops being a choice and says so by fading rather
   than by vanishing — a control that disappears reads as a bug. */
.ex-grid.is-muted { opacity: 0.45; }

.ex-cell {
  display: grid;
  justify-items: center;
  gap: 2px;
  padding: 4px;
  background: none;
  border: 1px solid var(--line);
  border-radius: 4px;
  cursor: pointer;
}
.ex-cell:hover { background: var(--bg-3); }
.ex-cell.is-picked {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 20%, var(--bg-3));
}

.ex-shot {
  display: grid;
  place-items: center;
  width: 64px;
  height: 64px;
  overflow: hidden;
}
.ex-shot canvas { display: block; max-width: 100%; max-height: 100%; }

.ex-n { font-family: var(--mono); font-size: 9.5px; color: var(--fg-3); }
.ex-cell.is-picked .ex-n { color: var(--fg); }

.modal-foot .ctl-result { margin: 0; }

/* Staging reads as "a decision is waiting on you", not as another panel section. */
.ctl-block.staging {
  background: color-mix(in srgb, var(--accent) 10%, var(--bg-2));
  border-left: 2px solid var(--accent);
}
.ctl-block.staging .ctl-h { color: var(--fg); }
.ctl-block.staging .ctl-btn { margin-bottom: 6px; }
.ctl-block[hidden] { display: none; }

.ctl-sub {
  margin: 12px 0 6px;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--fg-3);
}
.ctl-sub:first-of-type { margin-top: 8px; }

.ctl-fine {
  margin: 0 0 8px;
  font-size: 10.5px;
  line-height: 1.4;
  color: var(--fg-3);
}

.ctl-slider {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 4px;
  font-size: 10.5px;
  color: var(--fg-3);
}
.ctl-slider output { font-family: var(--mono); color: var(--fg-2); min-width: 22px; }
.ctl-slider input { flex: 1; accent-color: var(--accent); }

/* "Turn", inside Move & resize. A number field and the button that zeroes it, on one row.

   The field is deliberately narrow and monospaced: it holds at most "-720" plus a decimal, and a
   proportional font makes a number that changes under the arrow keys jitter sideways. The degree
   sign is a separate span rather than part of the value, because the value has to stay a real
   number for `type="number"` to give the arrow keys and the spinner. */
.ctl-turn {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
}
.ctl-turn-field {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1;
  min-width: 0;
  font-size: 10.5px;
  color: var(--fg-3);
}
.ctl-turn-field input {
  flex: 1;
  min-width: 0;
  background: var(--bg-3);
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: 5px 6px;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--fg);
  accent-color: var(--accent);
}
.ctl-turn-field input:focus-visible { outline: 1px solid var(--accent); outline-offset: 0; }
.ctl-turn-field input:disabled { opacity: 0.4; cursor: default; }
/* Matches .ctl-btn:disabled, so the whole row greys out together rather than the label staying
   bright over a dead field. */
.ctl-turn-field:has(input:disabled) { opacity: 0.4; }
.ctl-turn-deg { font-family: var(--mono); color: var(--fg-2); }
.ctl-turn .ctl-btn { width: auto; flex: 0 0 auto; }

/* "Width × Height", in step 1. The same metrics as .ctl-turn above on purpose — it is the same
   idiom (a value you type, plus one click back to a known-good one) and the two should not read
   as two different kinds of control.

   THE BUTTON IS NOT ON THIS ROW, which is the one place it departs from .ctl-turn. Turn has ONE
   field and can fit Straighten beside it; two labelled fields plus "Match the base" on a sidebar
   this narrow would leave three squeezed columns and nothing legible. "Match the base" therefore
   sits full-width underneath, exactly the relationship #btnFitLayer has to the .ctl-row above it.

   The × is aria-hidden: it is the shape of "640 × 358" for the eye, and a screen reader already
   gets "Width" and "Height" from the two labels. */
.ctl-size {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
}
.ctl-size-field {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1;
  min-width: 0;
  font-size: 10.5px;
  color: var(--fg-3);
}
.ctl-size-field input {
  flex: 1;
  min-width: 0;
  background: var(--bg-3);
  border: 1px solid var(--line);
  border-radius: 5px;
  padding: 5px 6px;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--fg);
  accent-color: var(--accent);
}
.ctl-size-field input:focus-visible { outline: 1px solid var(--accent); outline-offset: 0; }
.ctl-size-field input:disabled { opacity: 0.4; cursor: default; }
/* Matches .ctl-btn:disabled, so the whole row greys out together rather than the label staying
   bright over a dead field — the pairing .ctl-turn-field already carries. */
.ctl-size-field:has(input:disabled) { opacity: 0.4; }
.ctl-size-x { font-family: var(--mono); color: var(--fg-2); }

/* "Opacity", directly under Turn. A range input, its readout and the button that puts it back to
   solid, on one row — the same three-part shape and the same metrics as .ctl-turn above, so the
   two rows line up rather than looking like two different controls that happen to be adjacent.

   THE READOUT IS A SEPARATE NODE, which .ctl-turn does not need: a number field shows its own
   value, a range input shows a thumb position. It is monospaced and min-width'd so the row does
   not shuffle sideways as the number goes 5% → 100% mid-drag — the same jitter argument that made
   the Turn field monospaced, and it matters more here because the value changes continuously
   under the pointer rather than a keystroke at a time. */
.ctl-fade {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-bottom: 6px;
}
.ctl-fade-field {
  display: flex;
  align-items: center;
  gap: 6px;
  flex: 1;
  min-width: 0;
  font-size: 10.5px;
  color: var(--fg-3);
}
.ctl-fade-field input[type='range'] {
  flex: 1;
  min-width: 0;
  height: 18px;
  accent-color: var(--accent);
  cursor: pointer;
}
.ctl-fade-field input:focus-visible { outline: 1px solid var(--accent); outline-offset: 2px; }
.ctl-fade-field input:disabled { opacity: 0.4; cursor: default; }
/* Matches .ctl-btn:disabled, so the whole row greys out together rather than the label staying
   bright over a dead slider. Same rule as .ctl-turn-field's, for the same reason. */
.ctl-fade-field:has(input:disabled) { opacity: 0.4; }
.ctl-fade-value {
  min-width: 34px;
  text-align: right;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--fg-2);
}
.ctl-fade .ctl-btn { width: auto; flex: 0 0 auto; }

.ctl-steps { margin-top: 10px; }
.ctl-steps[hidden] { display: none; }

.ctl-count {
  margin: 0 0 8px;
  font-family: var(--mono);
  font-size: 10.5px;
  color: var(--fg-3);
}

.ctl-row {
  display: flex;
  gap: 6px;
  margin-bottom: 6px;
}
.ctl-row .ctl-btn { width: auto; flex: 1; }

/* "Across the frames", inside Move & resize. A rule above it because it is a different idea from
   everything else in that panel: flipping and resizing act on the layer on every frame at once,
   this acts on the one frame you are looking at.

   ⚠️ NO `display` PROPERTY HERE, deliberately. This element is shown and hidden with the `hidden`
   attribute from controls.js, and any `display` declared on the class outranks the UA sheet's
   `[hidden] { display: none }` — the element would then never hide. This file already carries
   `.ctl-block[hidden]` and `.ctl-steps[hidden]` to undo exactly that mistake; do not make a third.
   `.ctl-sub`'s own top margin gives the heading its air, so there is nothing else to declare. */
.move-frames {
  margin-top: 12px;
  padding-top: 4px;
  border-top: 1px solid var(--line);
}

.ctl-btn {
  width: 100%;
  height: 30px;
  padding: 0 10px;
  font: inherit;
  font-size: 12px;
  color: var(--fg);
  background: var(--bg-3);
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  cursor: pointer;
}
.ctl-btn:hover:not(:disabled) { background: #26272c; }
.ctl-btn:disabled { opacity: 0.4; cursor: default; }
.ctl-btn.primary {
  border-color: color-mix(in srgb, var(--accent) 55%, var(--line-2));
  background: color-mix(in srgb, var(--accent) 18%, var(--bg-3));
}
.ctl-btn.primary:hover:not(:disabled) { background: color-mix(in srgb, var(--accent) 28%, var(--bg-3)); }
/* Which cutting tool is armed. Two buttons that both start a cut need to say which one you are
   currently in, or the hint text below them looks like it changed on its own. */
.ctl-btn.is-on {
  border-color: var(--accent);
  color: var(--fg);
  box-shadow: inset 2px 0 0 var(--accent);
}

/* ── frame picker ─────────────────────────────────────────────────────── */

.modal {
  position: fixed;
  inset: 0;
  z-index: 20;
  display: grid;
  /* ⚠️ minmax(0, 1fr), NOT the implicit `auto`. An auto track is sized by the card's CONTENT, so
     a card holding a 177-cell frame grid produced a ~2900px row inside an 805px viewport — and
     `place-items: center` then centred the card in the ROW, putting it at y=1127. The backdrop
     covered the screen, the dialog was a thousand pixels below it, and nothing looked broken
     except that no dialog appeared. Bounding the track to the container is what makes "centred"
     mean centred on screen. */
  grid-template-rows: minmax(0, 1fr);
  grid-template-columns: minmax(0, 1fr);
  place-items: center;
  padding: 20px;
  background: rgb(0 0 0 / 72%);
}
.modal[hidden] { display: none; }

.modal-card {
  display: flex;
  flex-direction: column;
  width: min(880px, 100%);
  max-height: 100%;
  background: var(--bg-2);
  border: 1px solid var(--line-2);
  border-radius: 6px;
  box-shadow: 0 12px 48px rgb(0 0 0 / 60%);
}

.modal-head { padding: 14px 16px; border-bottom: 1px solid var(--line); }
.modal-head h2 { font-size: 14px; }
.modal-sub { margin: 5px 0 0; font-size: 11.5px; color: var(--fg-3); }

.modal-foot {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 11px 16px;
  border-top: 1px solid var(--line);
}
.modal-foot .ctl-btn { width: auto; min-width: 96px; }

.fp-count { font-size: 11.5px; color: var(--fg-3); }

/* ── prepare step (modal) ─────────────────────────────────────────────── */

/* Near-full-screen. This is the precision step — the image wants every pixel it can get. */
.edit-card {
  width: min(1720px, 100%);
  height: 100%;
}

.step-tag {
  margin: 0 0 4px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
}

/* Controls left, image right, both pinned. minmax(0,·) on the image column so a large picture
   cannot stretch the grid past the card — the same trap the filmstrip fell into. */
.edit-body {
  display: grid;
  grid-template-columns: 252px minmax(0, 1fr);
  gap: 18px;
  padding: 16px;
  /* flex:1 is the whole fix for "too much dead space" and "image too small" — they were the
     same bug. The card is full height, but without this the body is only as tall as the
     control rail's content, so the image box was sized by the buttons beside it. */
  flex: 1;
  min-height: 0;
  min-width: 0;
}

.edit-stage { display: flex; min-width: 0; min-height: 0; }

.edit-view {
  position: relative;
  flex: 1;
  min-width: 0;
  min-height: 0;
  background: #0d0d0f;
  border: 1px solid var(--line);
  border-radius: 4px;
}
/* The canvas fills its box and is sized to that box times the device pixel ratio, so what you
   aim at is a real pixel rather than an interpolated one. */
.edit-view canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  cursor: grab;
  touch-action: none;
}
.edit-view canvas.is-grabbing { cursor: grabbing; }
.edit-view.is-cropping canvas { cursor: crosshair; }
/* Tracing a cut. Same crosshair as cropping — both are "aim at the picture", and a second
   cursor for the same intent is a distinction without a difference. */
.edit-view.is-cutting canvas { cursor: crosshair; }

.edit-modes { display: flex; gap: 5px; margin-bottom: 6px; }

.edit-rail {
  display: flex;
  flex-direction: column;
  min-width: 0;
  overflow-y: auto;
  border-right: 1px solid var(--line);
  padding-right: 18px;
}
.edit-rail .ctl-btn { margin-bottom: 6px; }
.edit-rail .ctl-sub:first-child { margin-top: 0; }

.edit-cta {
  font-size: 12px;
  color: var(--fg-2);
}

.fp-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(100px, 1fr));
  gap: 8px;
  padding: 14px 16px;
  overflow-y: auto;
  min-height: 0;
}

.fp-cell {
  display: grid;
  justify-items: center;
  gap: 3px;
  padding: 5px;
  background: none;
  border: 1px solid var(--line);
  border-radius: 4px;
  cursor: pointer;
}
.fp-cell:hover { background: var(--bg-3); }
.fp-cell.is-picked {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 18%, var(--bg-3));
}

.fp-shot {
  display: grid;
  place-items: center;
  width: 92px;
  height: 92px;
  overflow: hidden;
  background-color: var(--check-a);
  background-image:
    linear-gradient(45deg, var(--check-b) 25%, transparent 25%),
    linear-gradient(-45deg, var(--check-b) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, var(--check-b) 75%),
    linear-gradient(-45deg, transparent 75%, var(--check-b) 75%);
  background-size: 8px 8px;
  background-position: 0 0, 0 4px, 4px -4px, -4px 0;
}
.fp-shot canvas { display: block; max-width: 100%; max-height: 100%; }

.fp-num { font-family: var(--mono); font-size: 9.5px; color: var(--fg-3); }
.fp-cell.is-picked .fp-num { color: var(--accent); }

/* stage */

/* The scrollbar sits on the LEFT of the stage, not the right.
 *
 * A browser puts it on the inline-end edge, which here is hard against the Layers panel — two
 * vertical strips of chrome touching, and the one you reach for is the one you did not mean.
 * `direction: rtl` moves it to the other edge; there is no CSS property that places a scrollbar
 * directly, so this is the mechanism, not a preference about text.
 *
 * Which is exactly why `.viewer` immediately sets `direction: ltr` again — otherwise the rtl
 * inherits into the content and reverses the flex row, putting the "previous frame" arrow on the
 * right of the picture and "next" on the left. The flip is chrome only; nothing inside it moves.
 *
 * Safe because nothing reads this element's scrollLeft. In rtl the horizontal scroll origin moves
 * to the right edge, so anything doing its own scroll arithmetic here would need to know. */
.stage-wrap {
  position: relative;
  display: grid;
  direction: rtl;
  min-width: 0;
  min-height: 0;
  overflow: auto;
  padding: 24px;
  background: var(--stage-bg);
}

.stage-wrap.dropping::after {
  content: '';
  position: absolute;
  inset: 8px;
  border: 2px dashed var(--accent);
  border-radius: 6px;
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  pointer-events: none;
}

/* rail | image | rail.
   Right-aligned against the panels, and margin-based rather than place-items/justify — a grid
   item centred or pinned by alignment cannot be scrolled back to its top-left corner once it
   overflows its scroll container, which a 4000x4000 trait image hits on the first zoom-in.
   `margin-left: auto` pushes it right when there is free space and collapses to 0 when there
   isn't, so both cases work from one rule.
   align-items defaults to stretch, so each rail is exactly as tall as the image beside it — no
   JS has to keep their heights in sync. */
/* rail | image | rail, with real space between them. The gap is what stops an arrow sitting flush
   against the picture and swallowing the corner handle underneath the overlay's bleed. */
.viewer {
  display: flex;
  /* Undo the stage's rtl the moment we are inside the content — see .stage-wrap. Without this the
     flex row reverses and the frame arrows swap sides. The margins below stay PHYSICAL (left/right,
     not start/end) so they mean the same thing under either direction. */
  direction: ltr;
  align-items: stretch;
  gap: 18px;
  padding: 18px;
  margin: auto 0 auto auto;
}

.artboard {
  position: relative;
  box-shadow: 0 2px 16px rgb(0 0 0 / 55%);
  background-color: var(--check-a);
  background-image:
    linear-gradient(45deg, var(--check-b) 25%, transparent 25%),
    linear-gradient(-45deg, var(--check-b) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, var(--check-b) 75%),
    linear-gradient(-45deg, transparent 75%, var(--check-b) 75%);
  background-size: 16px 16px;
  background-position: 0 0, 0 8px, 8px -8px, -8px 0;
}

.artboard.is-empty { display: none; }

.artboard canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* The tool surface — every pointer the tools need, and the only thing drawn on top of the art.
 *
 * ⚠️ IT IS ALWAYS INTERACTIVE. This used to be `pointer-events: none` with two class-gated rules
 * turning it on, and the tool was dead twice: once because nothing applied the class at boot, and
 * once for a reason never isolated at all. Interactivity that depends on a class means a missing
 * class reads as "broken"; the JS handlers already return early on the wrong tool, so letting the
 * surface take the event and having code decide is both simpler and fails the right way. Nothing
 * else on the artboard wants clicks — drag/drop listens on the stage wrapper, which still gets
 * the events by bubbling.
 *
 * The negative inset is the bleed: handles sit ON the box edge, so half of every edge square and
 * three quarters of every corner square fall OUTSIDE the picture. Clipped at inset:0 they showed
 * as slivers and the outer half could not be grabbed at all.
 *
 * No cursor here on purpose — tools.js sets one per handle (grab / ew-resize / nwse-resize) and a
 * CSS cursor would win over it.
 */
/* ⚠️ SELECTOR SPECIFICITY IS LOAD-BEARING HERE. `.artboard canvas` below sets width/height to
   100%, and it outranks a bare `.overlay`. Written that way, the overlay was laid out at the
   picture's size while holding a backing store 28px larger, and the selection box drew offset.
   Written with left/top left to JS, a moment where the inline styles were missing dropped the
   element to its STATIC position — directly below the stage canvas, a full canvas-height down the
   page, taking every handle and every clickable pixel with it.

   So all four of these live here, at a specificity that wins:
     left/top   pin it into the bleed. Never `auto` — auto means static position, i.e. under the
                picture, which is exactly the failure above.
     width/height: auto  a canvas's intrinsic size IS its width/height attributes, so one CSS
                pixel is one canvas pixel and tools.js only has to set the backing store. */
.artboard canvas.overlay {
  position: absolute;
  left: calc(var(--bleed) * -1);
  top: calc(var(--bleed) * -1);
  right: auto;
  bottom: auto;
  width: auto;
  height: auto;
  pointer-events: auto;
}

/* Step rails — outside the image, never over it. Height comes from the flex row, so they are
   always exactly as tall as the artboard without any JS measuring anything. */

/* Quiet until wanted. They sit beside the picture for the whole session, so at full strength they
   compete with the artwork; at 40% they read as available without being part of the image. Hover
   and keyboard focus bring them back to full. */
.frame-arrow {
  flex: none;
  width: var(--rail-w);
  display: grid;
  place-items: center;
  padding: 0;
  color: var(--fg-2);
  background: color-mix(in srgb, var(--bg-2) 70%, transparent);
  border: 1px solid var(--line);
  border-radius: 5px;
  opacity: 0.4;
  cursor: pointer;
  transition: opacity 0.14s ease, background 0.14s ease, color 0.14s ease;
}
.frame-arrow[hidden] { display: none; }
.frame-arrow svg { width: 26px; height: 26px; transition: transform 0.1s ease; }
.frame-arrow:hover,
.frame-arrow:focus-visible {
  opacity: 1;
  background: var(--bg-3);
  color: var(--fg);
}
.frame-arrow:active svg { transform: scale(0.85); }

/* Touch. A 42px rail is under every thumb-target guideline, and on a phone there is no hover to
   reveal them — so they are permanently readable and wide enough to hit. */
@media (pointer: coarse) {
  .frame-arrow {
    width: 56px;
    opacity: 0.75;
  }
}
/* The prev/next rules that stripped the inner border and squared the inner corners are gone: they
   existed to fuse each arrow to the picture, and the arrows are now separated from it by the
   viewer's gap. A half-bordered button floating in space looks broken. */

.frame-flag {
  position: absolute;
  top: 8px;
  left: 50%;
  transform: translateX(-50%);
  padding: 2px 8px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.04em;
  color: #1a1206;
  background: #e0a33f;
  border-radius: 3px;
  pointer-events: none;
}
.frame-flag[hidden] { display: none; }

/* empty state */

.empty-state {
  position: absolute;
  inset: 0;
  display: grid;
  align-content: center;
  justify-items: center;
  gap: 2px;
  color: var(--fg-3);
  cursor: pointer;
  user-select: none;
}
.empty-state:hover { color: var(--fg-2); }
.empty-state[hidden] { display: none; }

.empty-mark svg {
  width: 56px;
  height: 56px;
  margin-bottom: 14px;
  color: var(--line-2);
}
.empty-state:hover .empty-mark svg { color: var(--fg-3); }

.empty-title { margin: 0; font-size: 15px; color: var(--fg-2); }
.empty-sub { margin: 4px 0 0; font-size: 12px; }
.empty-formats {
  margin: 14px 0 0;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.04em;
  color: var(--line-2);
}

/* ── panels ───────────────────────────────────────────────────────────── */

.panels {
  display: grid;
  /* Layers takes the slack; Library, History and Move & resize are as tall as their contents. */
  grid-template-rows: minmax(0, 1fr) auto auto auto;
  min-width: 0;
  min-height: 0;
  overflow: hidden;
  background: var(--bg-2);
  border-left: 1px solid var(--line);
}

.panel {
  display: flex;
  flex-direction: column;
  min-height: 0;
  border-bottom: 1px solid var(--line);
}

.panel-h {
  padding: 9px 12px;
  color: var(--fg-2);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  background: var(--bg-2);
  border-bottom: 1px solid var(--line);
}

/* A header that also carries buttons. Same padding and rule as .panel-h so the two kinds of
   header line up with each other down the panel column. */
.panel-h-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  padding-top: 5px;
  padding-bottom: 5px;
}
.panel-h-row h2 {
  margin: 0;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--fg-2);
}
.panel-h-actions { display: flex; gap: 4px; }

/* Dim until you go looking for it. Resetting the panel width is housekeeping, not part of making
   a picture, and a full-contrast button beside the panel title would compete with the layers. */
.panel-reset { color: var(--fg-3); }
.panel-reset:hover { color: var(--fg); }

/* ── saved cuts ───────────────────────────────────────────────────────── */

/* The count beside the heading. It is the only thing on screen that says this list is filling up
   as you work, which is what makes it read as a history rather than a settings box. */
.ctl-tally {
  margin-left: 4px;
  padding: 1px 5px;
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: 0;
  color: var(--fg-2);
  background: var(--bg-3);
  border-radius: 8px;
}
.ctl-tally:empty { display: none; }

.shape-list { display: flex; flex-direction: column; gap: 3px; }
.shape-list[hidden] { display: none; }

.shape-row { display: flex; align-items: stretch; gap: 3px; }

.shape-go {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
  min-width: 0;
  padding: 4px 8px;
  background: var(--bg-3);
  border: 1px solid var(--line-2);
  border-radius: 3px;
  color: var(--fg-2);
  font: inherit;
  font-size: 11.5px;
  text-align: left;
  cursor: pointer;
}
.shape-go:hover:not(:disabled) { background: #26272c; color: var(--fg); }
.shape-go:disabled { opacity: 0.45; cursor: default; }

/* The outline drawn small. Filled AND stroked: a thin sliver of a shape is invisible as an
   outline alone at 26px, and unreadable as a solid alone when it has holes in its silhouette. */
.shape-thumb {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  background: var(--bg);
  border-radius: 2px;
}
.shape-thumb path {
  fill: color-mix(in srgb, var(--accent) 45%, transparent);
  stroke: var(--accent);
  stroke-width: 1.2;
  stroke-linejoin: round;
}
.shape-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.shape-del {
  flex: 0 0 auto;
  width: 22px;
  background: none;
  border: 1px solid transparent;
  border-radius: 3px;
  color: var(--fg-3);
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
}
.shape-del:hover { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 40%, transparent); }

/* ── history ──────────────────────────────────────────────────────────── */

.topbar-history { display: flex; gap: 2px; margin-right: 8px; }
.top-icon { color: var(--fg-2); }
.top-icon:hover:not(:disabled) { color: var(--fg); }
.top-icon:disabled { color: var(--fg-3); opacity: 0.45; cursor: default; }

/* The panel is `auto` height while shut and takes real space once open — a history that pushed
   Layers off the screen the moment it had a few steps in it would be worse than not having one. */
.panel-history { flex: 0 0 auto; min-height: 0; }
.panel-history.is-open { display: flex; flex-direction: column; min-height: 0; max-height: 40vh; }

.history-twist {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0;
  background: none;
  border: 0;
  color: inherit;
  cursor: pointer;
  font: inherit;
}
.history-twist h2 {
  margin: 0;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--fg-2);
}
.history-twist:hover h2 { color: var(--fg); }
.history-twist-mark { display: flex; align-items: center; color: var(--fg-3); }
.history-pos { font-family: var(--mono); font-size: 10px; color: var(--fg-3); }

.history-list {
  overflow-y: auto;
  min-height: 0;
}
.history-list[hidden] { display: none; }

.history-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  padding: 5px 12px;
  background: none;
  border: 0;
  border-left: 2px solid transparent;
  color: var(--fg-2);
  font: inherit;
  font-size: 11.5px;
  text-align: left;
  cursor: pointer;
}
.history-row:hover { background: var(--bg-3); color: var(--fg); }

.history-dot {
  flex: 0 0 auto;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--fg-3);
}
.history-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.history-row.is-current {
  color: var(--fg);
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border-left-color: var(--accent);
}
.history-row.is-current .history-dot { background: var(--accent); }

/* Undone steps stay visible and greyed rather than disappearing. They ARE the redo branch, and a
   list that deletes them is a list that cannot answer "what did I just throw away". */
.history-row.is-undone { color: var(--fg-3); opacity: 0.6; }
.history-row.is-undone .history-dot { background: transparent; box-shadow: inset 0 0 0 1px var(--fg-3); }

.panel-empty {
  margin: 0;
  padding: 14px 12px;
  color: var(--fg-3);
  font-size: 12px;
}
.panel-empty[hidden] { display: none; }

.panel-hint {
  margin: 0;
  padding: 7px 12px;
  font-size: 11px;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-bottom: 1px solid var(--line);
}
.panel-hint[hidden] { display: none; }

/* ── library ──────────────────────────────────────────────────────────────
 *
 * His ask: "add the option to save and favorite layers that u add and or cut for quick drag and
 * drop into layers so we can save time and use the exact same layer always." This panel is a
 * shelf, not a document manager — rows, not tiles, for the same reason step 6's export history
 * (.gal-item) is rows: the facts that tell two saved layers apart do not survive a narrow tile.
 *
 * Collapses exactly like .panel-history — same is-open/max-height contract — because a strip of
 * saved parts should not always take space over the layers being worked on.
 *
 * --ok is reserved for tie-selection (see the comment above .layer-tick); favorite uses --warn,
 * the only other saturated token in the palette, so a favorite star never reads as "selected" or
 * "tied to another layer".
 */

.panel-library { flex: 0 0 auto; min-height: 0; }
.panel-library.is-open { display: flex; flex-direction: column; min-height: 0; max-height: 40vh; }

.library-twist {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 0;
  background: none;
  border: 0;
  color: inherit;
  cursor: pointer;
  font: inherit;
}
.library-twist h2 {
  margin: 0;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--fg-2);
}
.library-twist:hover h2 { color: var(--fg); }
.library-twist-mark { display: flex; align-items: center; color: var(--fg-3); }

/* The source toggle — Mine | Everyone.
 *
 * A segmented pair rather than two chips, because the two are exclusive and a person has to be
 * able to see which one they are on without reading: the pressed half takes the raised surface.
 * It sits in the header row between the twist and the count, so it costs the column no height.
 *
 * ⚠️ ITS OWN `[hidden]` RULE. The `display: flex` below outranks the UA sheet's
 * `[hidden] { display: none }`, and library.js hides this by attribute whenever the panel is
 * collapsed. Without the companion, a collapsed Library would still show a toggle for a list that
 * is not on screen — the same bug this stylesheet has now shipped four times elsewhere. */
.library-source {
  display: flex;
  flex: 0 0 auto;
  gap: 2px;
  padding: 2px;
  background: var(--bg-3);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.library-source[hidden] { display: none; }

/* No `display` here on purpose, so a <button>'s own is left alone and no companion rule is owed. */
.library-src {
  padding: 1px 7px;
  font: inherit;
  font-size: 10px;
  letter-spacing: 0.04em;
  color: var(--fg-3);
  background: none;
  border: 0;
  border-radius: 2px;
  cursor: pointer;
}
.library-src:hover { color: var(--fg); }
.library-src[aria-pressed='true'] { color: var(--fg); background: var(--bg); }

.library-list {
  overflow-y: auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  gap: 4px;
  padding: 6px;
}
.library-list[hidden] { display: none; }

/* The affordance for the WHOLE list while a saved layer is dragged over it — deliberately
   different from .drop-above/.drop-below, which mark one insertion point during a layer reorder.
   A library drop has no position to insert at, so this says "let go here", not "it lands here",
   while still speaking the same accent-coloured language as those two. */
.layer-list.drop-library {
  outline: 1.5px dashed var(--accent);
  outline-offset: -1.5px;
  background: color-mix(in srgb, var(--accent) 6%, transparent);
}

.library-item {
  position: relative;
  display: grid;
  /* thumb · name+facts · favorite · forget */
  grid-template-columns: 34px minmax(0, 1fr) auto auto;
  align-items: center;
  gap: 6px;
  padding: 4px 6px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  cursor: grab;
}
.library-item:hover { background: var(--bg-3); border-color: var(--line-2); }
.library-item:active { cursor: grabbing; }

/* The exact checkerboard recipe from .layer-thumb (styles.css, "layers" section) — unchanged, so
   a saved layer's transparency reads the same as every other thumbnail in the app. */
.library-thumb {
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line-2);
  border-radius: 2px;
  background-color: var(--check-a);
  background-image:
    linear-gradient(45deg, var(--check-b) 25%, transparent 25%),
    linear-gradient(-45deg, var(--check-b) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, var(--check-b) 75%),
    linear-gradient(-45deg, transparent 75%, var(--check-b) 75%);
  background-size: 8px 8px;
  background-position: 0 0, 0 4px, 4px -4px, -4px 0;
  overflow: hidden;
}
.library-thumb canvas,
.library-thumb img { display: block; width: 100%; height: 100%; object-fit: contain; }

.library-meta { display: grid; gap: 2px; min-width: 0; }
.library-name {
  font-size: 11.5px;
  color: var(--fg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.library-facts {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--fg-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Who made it — "names based on creators". Between the name and the mono facts line, and NOT in
   the mono face: it is a person, not a measurement, and setting it in the same font as "512 × 512
   · 30 frames" would file it under specifications.

   ⚠️ NO `display` DECLARATION, DELIBERATELY. This element is omitted from the row entirely when
   there is no name to put in it — see buildRow in library.js — rather than emptied or hidden, so
   there is no `[hidden]` companion to owe and nothing that can hold a blank line open under a row
   whose creator is unknown. */
.library-by {
  font-size: 10px;
  color: var(--fg-2);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Self-contained rather than layered on .icon-btn, so these read correctly whichever base class
   the panel's own script does or does not also apply. */
.library-star,
.library-del {
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  padding: 0;
  color: var(--fg-3);
  background: none;
  border: none;
  border-radius: 3px;
  cursor: pointer;
}
.library-star svg,
.library-del svg { display: block; }
.library-star:hover { background: var(--line); color: var(--warn); }
.library-star[aria-pressed='true'] { color: var(--warn); }
.library-del:hover { background: color-mix(in srgb, var(--danger) 28%, transparent); color: var(--fg); }

.library-clear[hidden] { display: none; }

/* Publish, on the shelf. A chip like Clear beside it, not a `primary` anything: it is a shortcut,
   and the top bar already owns the one bright control on this page. It carries the layer's name,
   which can be long, so it takes the full width and clips rather than widening the column.

   ⚠️ ITS OWN `[hidden]` RULE, like every other one in this file — the `display` below outranks the
   UA sheet's `[hidden] { display: none }`, and main.js hides this by attribute whenever there is
   no saved layer to publish or the gallery API is not answering. */
.library-publish {
  display: block;
  width: 100%;
  margin-top: 6px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.library-publish[hidden] { display: none; }

/* ── inventory ────────────────────────────────────────────────────────────
 *
 * "an inventory of every layer they ever added to canvas and bookmarks and lists and rename and
 * favorite". A second shelf inside the SAME panel as the saved library, with its own twist, its
 * own count and its own rules — kept visually quieter than the library above it, because the
 * library is the shelf he curates and this one is the one that just fills up.
 *
 * ⚠️ NO THUMBNAIL COLUMN. This list is text on purpose: the store hands back the full bytes and no
 * small picture, so a picture per row would pull every layer he has ever added into memory to
 * paint a sidebar. The caption is the four facts that tell two layers apart.
 *
 * ⚠️ EVERY CLASS BELOW THAT DECLARES A `display` AND IS HIDDEN BY ATTRIBUTE CARRIES ITS OWN
 * `[hidden]` COMPANION. A class `display` outranks the UA sheet's `[hidden] { display: none }`,
 * and this stylesheet has shipped that bug five times. */
.inv-h { border-top: 1px solid var(--line); }

/* ⚠️ THE PANEL'S OWN CEILING, FOR THE SECOND TWIST. `.panel-library.is-open` is set by the Library
   twist alone, so an expanded Inventory under a COLLAPSED Library had no `max-height` at all — and
   `.panels` is `overflow: hidden`, so on a short window the growth would have pushed "Move &
   resize" off the bottom of the column with nothing saying why. Its own class rather than sharing
   `is-open`: two twists writing one class is two modules fighting over one attribute. */
.panel-library.inv-open { display: flex; flex-direction: column; min-height: 0; max-height: 55vh; }

/* The list chips. ⚠️ THEY ARE `.chip`s AND `.chip.is-on`, NOT A FOURTH CHIP FAMILY. The first
   version of this row declared its own `.inv-tab` — `.library-src` geometry wearing `.chip`'s
   skin — and sat six pixels above a row of real `.chip`s that differed from it by one pixel of
   font size. Two near-identical chip rows that close together do not read as a hierarchy, they
   read as a rendering fault. `.chip.is-on` was already this stylesheet's answer to a pressed chip.
   `.inv-tab` survives as a CLICK HOOK with no declarations of its own. */
.inv-lists {
  display: flex;
  flex-wrap: wrap;
  gap: 3px;
  padding: 0 12px 6px;
}
.inv-lists[hidden] { display: none; }

.inv-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  padding: 0 12px 6px;
}
.inv-bar[hidden] { display: none; }
.inv-bar .chip:disabled { opacity: 0.45; cursor: not-allowed; }

/* Its own scroll and its own ceiling. The panel it sits in caps at 40vh with the saved shelf
   already inside it, so an inventory of two hundred layers must not push the whole column. */
.inv-list {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 0 6px 6px;
  overflow-y: auto;
  min-height: 0;
  max-height: 26vh;
}
.inv-list[hidden] { display: none; }

/* The standing explanation of the cap. Quiet grey rather than the accent bar `.panel-hint` uses:
   it is four lines long and permanently on screen, and four lines of accent-washed blue in a 300px
   column is a banner. A rule you need to know once should not shout every time you open the panel.

   ⚠️ NO `display` DECLARATION, DELIBERATELY, so the UA sheet's `[hidden] { display: none }` still
   works on the <p> and no companion rule is owed. */
.inv-note {
  margin: 0;
  padding: 0 12px 8px;
  font-size: 10.5px;
  line-height: 1.45;
  color: var(--fg-3);
}

/* ⚠️ TWO CELLS, NOT FIVE, AND THE SAME METRICS AS `.library-item` THIRTY PIXELS ABOVE IT. The
   first version was a five-column grid at `gap: 1px; padding: 2px 4px` — a toolbar's spacing, in
   a panel whose other two row types both use 6px and 4px 6px. Three row idioms in one column, and
   the two that disagreed were the two sitting next to each other. The four buttons are now one
   flex group, exactly the `.layer-row` + `.layer-actions` arrangement this stylesheet already
   uses, so the row reads as a row with actions on it rather than as a control strip. */
.inv-item {
  display: grid;
  /* name+facts · the four actions, as one group */
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 6px;
  padding: 4px 6px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  cursor: pointer;
}
/* 1px between the four, like `.layer-actions`. NOT `opacity: 0` like `.layer-actions`: three of
   these four are the things he asked for by name, and a control revealed on hover is a control a
   pointerless visitor never finds. */
.inv-actions { display: flex; align-items: center; gap: 1px; }
.inv-item:hover { background: var(--bg-3); border-color: var(--line-2); }

.inv-meta { display: grid; gap: 1px; min-width: 0; }
.inv-name {
  font-size: 11.5px;
  color: var(--fg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.inv-facts {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--fg-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ⚠️ STATE RULES ONLY. `iconButton()` emits `class="icon-btn inv-act …"`, and `.icon-btn` further
   down this file already declares the size, the grid, the padding, the colour, the background, the
   border, the radius and the cursor — same specificity, later in the source, so it wins every one
   of them. A "self-contained" block here would be dead declarations plus a comment claiming the
   opposite of what ships, and a later edit to a width in it would silently do nothing. This is the
   arrangement `.layer-eye` / `.layer-lock` / `.layer-del` already use. */
.inv-act { flex: 0 0 auto; }
/* ⚠️ 0.55, NOT 0.3. The bookmark is disabled-with-a-title rather than hidden precisely so it stays
   findable before any list exists; at 22px, a 0.3 wash on this colour reads as absent, which is
   the state that choice was made to avoid. */
.inv-act:disabled { opacity: 0.55; cursor: not-allowed; }
.inv-act:disabled:hover { background: none; color: var(--fg-3); }
/* The star is the only way to make a layer permanent, so it is the only one that takes a colour
   when it is on — the others are actions, not states. */
.inv-star[aria-pressed='true'] { color: var(--warn); }
.inv-mark[aria-pressed='true'] { color: var(--accent); }
.inv-forget:hover { background: color-mix(in srgb, var(--danger) 28%, transparent); color: var(--fg); }

/* ── drafts ───────────────────────────────────────────────────────────────
 *
 * "and also for their creations and drafts so they can switch between without losing the layers".
 * In the top bar beside the document name, because that is where the document's identity already
 * is, and a drafts list at the bottom of a scrolling sidebar is a list nobody scrolls to. */
.drafts { position: relative; }

.drafts-btn { color: var(--fg-2); }

.drafts-pop {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  /* Between .modal (20) and .tour (40): it has to paint over the stage and the panels, and the
     walkthrough still has to paint over it. */
  z-index: 30;
  width: 320px;
  max-width: 86vw;
  display: flex;
  flex-direction: column;
  background: var(--bg-2);
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  box-shadow: 0 10px 28px rgb(0 0 0 / 45%);
  overflow: hidden;
}
.drafts-pop[hidden] { display: none; }

.drafts-head {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  border-bottom: 1px solid var(--line);
}
.drafts-head h2 {
  margin: 0;
  flex: 1 1 auto;
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--fg-2);
}
.drafts-count { font-family: var(--mono); font-size: 10px; color: var(--fg-3); }

.drafts-list {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 6px;
  overflow-y: auto;
  min-height: 0;
  max-height: 46vh;
}
.drafts-list[hidden] { display: none; }

.drafts-item {
  display: grid;
  /* name+facts · the three actions, as one group — the same two-cell shape as `.inv-item`. */
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 6px;
  padding: 4px 6px;
  border: 1px solid transparent;
  border-radius: var(--radius);
  cursor: pointer;
}
.drafts-item:hover { background: var(--bg-3); border-color: var(--line-2); }
/* The one he is looking at, marked rather than hidden from the list — a switcher that omits the
   current document makes him count to work out which one he is in. */
.drafts-item.is-current { background: color-mix(in srgb, var(--accent) 12%, transparent); }

.drafts-meta { display: grid; gap: 1px; min-width: 0; }
.drafts-name {
  font-size: 12px;
  color: var(--fg);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.drafts-facts {
  font-family: var(--mono);
  font-size: 10px;
  color: var(--fg-3);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* State rules only, layered on `.icon-btn` — see the note on `.inv-act` above. */
.drafts-actions { display: flex; align-items: center; gap: 1px; }
.drafts-act { flex: 0 0 auto; }
.drafts-star[aria-pressed='true'] { color: var(--warn); }
.drafts-del:hover { background: color-mix(in srgb, var(--danger) 28%, transparent); color: var(--fg); }

/* groups
 *
 * The header row is a sibling of the layer rows, not a container around them — the panel is a
 * flat list and the indent is what says "inside". A real nesting element here would have to be
 * kept in sync with a model that is deliberately flat. */

.group-row {
  display: grid;
  /* twist · eye · lock · mark · name · actions. Same minmax(0,…) reason as .layer-row.
     The mark column is 34px to match `.layer-row`'s thumb column — a narrower track would squeeze
     the preview back below a layer's however big `.group-thumb` is declared. */
  grid-template-columns: 18px 22px 22px 34px minmax(0, 1fr) auto;
  align-items: center;
  gap: 6px;
  padding: 5px 8px 5px 2px;
  background: color-mix(in srgb, var(--fg) 4%, var(--bg-2));
  border-bottom: 1px solid color-mix(in srgb, var(--line) 55%, transparent);
  border-left: 2px solid transparent;
  cursor: pointer;
}
.group-row:hover { background: color-mix(in srgb, var(--fg) 8%, var(--bg-2)); }
.group-row.is-selected { border-left-color: var(--accent); }
/* The group's members, composited. Checkered like the layer thumbnails so transparency reads as
   transparency rather than as a black picture.
   Same 34px box and same 1px border as `.layer-thumb`, deliberately: with `box-sizing: border-box`
   the border is what sets the inner square the canvas is clipped to, so a group preview only
   matches a layer preview pixel-for-pixel if BOTH numbers match. It was 26px with no border, and
   the group's picture came out visibly smaller than every layer's. Change the two together. */
.group-thumb {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border: 1px solid var(--line-2);
  border-radius: 2px;
  overflow: hidden;
  background-color: var(--check-a);
  background-image:
    linear-gradient(45deg, var(--check-b) 25%, transparent 25%),
    linear-gradient(-45deg, var(--check-b) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, var(--check-b) 75%),
    linear-gradient(-45deg, transparent 75%, var(--check-b) 75%);
  background-size: 8px 8px;
  background-position: 0 0, 0 4px, 4px -4px, -4px 0;
}
.group-thumb canvas { display: block; }
.group-name { font-weight: 600; }
.group-count { font-size: 10px; color: var(--fg-3); font-family: var(--mono); }
.group-twist { color: var(--fg-3); }

/* The indent, and the rule that ties a member visually to its header. */
.layer-row.is-grouped {
  padding-left: 14px;
  box-shadow: inset 2px 0 0 color-mix(in srgb, var(--fg) 14%, transparent);
}

/* ── ticking layers, and the diagram that joins them ──────────────────────
 *
 * GREEN, not the accent blue. Selection is already blue (the left bar and the row tint), and the
 * two states are live at the same time — the layer you are editing and the set you are about to
 * tie together are different questions. Sharing a colour would make one unreadable.
 */

.layer-tick { color: var(--fg-3); }
.layer-tick:hover { color: var(--ok); }
.layer-row.is-ticked .layer-tick { color: var(--ok); }

.layer-row.is-ticked {
  /* outline, not border: it does not take part in layout, so a ticked row cannot shift the list
     by a pixel as the outline appears and disappears. */
  outline: 1.5px solid var(--ok);
  outline-offset: -1.5px;
  background: color-mix(in srgb, var(--ok) 10%, transparent);
}

/* The gutter only exists while a connector is being drawn — the rows are back to full width the
   moment the ties are cleared, so nothing is permanently indented for a feature not in use. */
.layer-list.has-ties .layer-row,
.layer-list.has-ties .group-row { padding-left: 22px; }
.layer-list.has-ties .layer-row.is-grouped { padding-left: 34px; }
.layer-list .layer-row,
.layer-list .group-row { position: relative; }

/* The spine. One vertical segment per row: bottom half at the top end, top half at the bottom
   end, full height for everything the line passes through on the way. */
.tie-top::before,
.tie-bottom::before,
.tie-full::before {
  content: '';
  position: absolute;
  left: 8px;
  width: 1.5px;
  background: var(--ok);
}
.tie-full::before { top: 0; bottom: 0; }
.tie-bottom::before { top: 50%; bottom: 0; border-radius: 1px 1px 0 0; }
.tie-top::before { top: 0; height: 50%; border-radius: 0 0 1px 1px; }

/* The arm: a short line out of the spine into the row, with an arrowhead on the end. Line and
   head are ONE element clipped to that shape, rather than a line plus a rotated square hung off
   some unrelated child — the arrow is one thing, so it is one box. */
.tie-arm::after {
  content: '';
  position: absolute;
  left: 8px;
  top: 50%;
  width: 13px;
  height: 8px;
  margin-top: -4px;
  background: var(--ok);
  clip-path: polygon(0 40%, 62% 40%, 62% 6%, 100% 50%, 62% 94%, 62% 60%, 0 60%);
}

/* The bar that appears once two rows are ticked. */
.tie-bar {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
  background: color-mix(in srgb, var(--ok) 14%, var(--bg-2));
  border-bottom: 1px solid color-mix(in srgb, var(--ok) 40%, transparent);
}
.tie-bar[hidden] { display: none; }
.tie-bar-count {
  flex: 1;
  font-size: 11px;
  color: var(--fg-2);
  font-family: var(--mono);
}
.tie-bar-go {
  width: auto;
  margin: 0;
  padding: 4px 10px;
  font-size: 11.5px;
  background: color-mix(in srgb, var(--ok) 30%, var(--bg-3));
  border-color: color-mix(in srgb, var(--ok) 55%, transparent);
}
.tie-bar-go:hover:not(:disabled) { background: color-mix(in srgb, var(--ok) 42%, var(--bg-3)); }

/* layers */

.panel-layers { min-height: 0; }

.layer-list {
  overflow-y: auto;
  min-height: 0;
}

.layer-row {
  position: relative;
  display: grid;
  /* TICK · eye · lock · thumb · name · actions — six columns for six children.
     Count them against buildRow's append() before changing either. The tick was once a sixth
     child while this declared five columns, and the grid put it on an implicit SECOND ROW
     underneath the layer, at every panel width.
     `minmax(0, 1fr)` on the name, not `1fr`: a grid item's default min-width is its content, so a
     long layer name would push the row's buttons off the edge instead of ellipsing. */
  grid-template-columns: 22px 22px 22px 34px minmax(0, 1fr) auto;
  align-items: center;
  gap: 6px;
  padding: 5px 8px 5px 4px;
  border-bottom: 1px solid color-mix(in srgb, var(--line) 55%, transparent);
  border-left: 2px solid transparent;
  cursor: pointer;
}
.layer-row:hover { background: var(--bg-3); }
.layer-row.is-selected {
  background: color-mix(in srgb, var(--accent) 16%, var(--bg-3));
  border-left-color: var(--accent);
}
.layer-row.is-locked { cursor: default; }
.layer-row.is-base { border-left-color: var(--fg-3); }
.layer-row.is-dragging { opacity: 0.4; }

/* Insertion line while dragging. Drawn on the row, so it can't be scrolled out of sync. */
.layer-row.drop-above::before,
.layer-row.drop-below::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  height: 2px;
  background: var(--accent);
  pointer-events: none;
}
.layer-row.drop-above::before { top: -1px; }
.layer-row.drop-below::after { bottom: -1px; }

/* icon buttons ---------------------------------------------------------- */

.icon-btn {
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  padding: 0;
  color: var(--fg-3);
  background: none;
  border: none;
  border-radius: 3px;
  cursor: pointer;
}
.icon-btn svg { display: block; }
.icon-btn:hover { background: var(--line); color: var(--fg); }

.layer-eye[aria-pressed='true'] { color: var(--fg-2); }
.layer-eye[aria-pressed='false'] { color: var(--line-2); }
.layer-lock[aria-pressed='true'] { color: var(--accent); }
.layer-base[aria-pressed='true'] { color: var(--accent); }
.layer-del:hover { background: color-mix(in srgb, var(--danger) 28%, transparent); color: var(--fg); }

.layer-actions {
  display: flex;
  gap: 1px;
  opacity: 0;
}
/* ⚠️ A GROUP HEADER IS NOT A `.layer-row`, ON PURPOSE — buildGroupRow() says so, because the
   drag-to-reorder code queries `.layer-row` and a header must never become a drop target. It does
   however build the same `.layer-actions` div, with rename, ungroup and delete-the-whole-group
   inside it. So for as long as these two selectors named only `.layer-row`, those three buttons
   existed, worked, and sat at `opacity: 0` on every frame of the app's life: no hover, no
   selection, no gesture anywhere revealed them. Reported as "there is no button to fully delete a
   group" — which is what an invisible control is.

   The lesson generalises: `.layer-actions` is a contract about what a row LOOKS like, and any row
   that opts into it has to be named here as well. Today the only two builders are the layer row
   (shell.js:226) and the group header (shell.js:364), and both are covered below. */
.layer-row:hover .layer-actions,
.layer-row.is-selected .layer-actions,
.group-row:hover .layer-actions,
.group-row.is-selected .layer-actions { opacity: 1; }
.layer-actions .icon-btn[aria-pressed='true'] { opacity: 1; }
.layer-row .layer-base[aria-pressed='true'] { opacity: 1; }

/* name + base badge ------------------------------------------------------ */

.layer-label {
  display: flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
}

.layer-badge {
  flex: none;
  padding: 1px 4px;
  font-family: var(--mono);
  font-size: 9px;
  letter-spacing: 0.06em;
  color: var(--fg-3);
  border: 1px solid var(--line-2);
  border-radius: 2px;
}

.layer-rename {
  width: 100%;
  min-width: 0;
  height: 20px;
  padding: 0 5px;
  font: inherit;
  font-size: 12px;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--accent);
  border-radius: 3px;
}
.layer-rename:focus { outline: none; }

.layer-thumb {
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border: 1px solid var(--line-2);
  border-radius: 2px;
  background-color: var(--check-a);
  background-image:
    linear-gradient(45deg, var(--check-b) 25%, transparent 25%),
    linear-gradient(-45deg, var(--check-b) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, var(--check-b) 75%),
    linear-gradient(-45deg, transparent 75%, var(--check-b) 75%);
  background-size: 8px 8px;
  background-position: 0 0, 0 4px, 4px -4px, -4px 0;
  overflow: hidden;
}
.layer-thumb canvas { display: block; }

.layer-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 12px;
}
.layer-name.is-dim { color: var(--fg-3); }

/* move & resize
 *
 * This slot held a Properties readout — name, source size, placed size, position, scale, flip,
 * opacity, frames, state. It was replaced rather than moved: every one of those is either
 * visible on the canvas already or unactionable as text. The controls that were stranded in the
 * left rail took its place, next to the selection they operate on.
 *
 * The .ctl-* classes come across unchanged because they were never scoped to .controls. */

.panel-transform { flex: 0 0 auto; }

.panel-body { padding: 11px 12px 12px; }
.panel-body .ctl-hint { margin-bottom: 0; }

/* ── filmstrip ────────────────────────────────────────────────────────── */

.filmstrip {
  min-width: 0;
  background: var(--bg-2);
  border-top: 1px solid var(--line);
}
.filmstrip[hidden] { display: none; }

.film-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 6px 10px;
  border-bottom: 1px solid var(--line);
}

.film-title {
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--fg-2);
}

.film-count {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--fg-3);
}

.film-hint {
  font-size: 10.5px;
  color: var(--line-2);
}

.film-sizes { display: flex; gap: 2px; }
.film-sizes .chip { min-width: 22px; }

.chip.is-on {
  color: var(--fg);
  background: color-mix(in srgb, var(--accent) 22%, var(--bg-3));
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}

.chip.play {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  height: 20px;
  min-width: 62px;
}
.play-glyph { font-size: 9px; line-height: 1; }

.goto {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 10.5px;
  color: var(--fg-3);
}

.goto-input {
  width: 52px;
  height: 18px;
  padding: 0 4px;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--fg);
  background: var(--bg);
  border: 1px solid var(--line-2);
  border-radius: 3px;
}
.goto-input:focus { outline: none; border-color: var(--accent); }

.marks-chip { color: #e0a33f; border-color: color-mix(in srgb, #e0a33f 40%, var(--line)); }
.marks-chip[hidden] { display: none; }
#btnMark.is-on {
  color: #1a1206;
  background: #e0a33f;
  border-color: #e0a33f;
}

.film-track {
  display: flex;
  align-items: flex-end;
  gap: 6px;
  min-width: 0;
  max-width: 100%;
  padding: 8px 10px 10px;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: thin;
}

.film-cell {
  flex: none;
  display: grid;
  justify-items: center;
  gap: 3px;
  padding: 3px;
  background: none;
  border: 1px solid transparent;
  border-radius: 3px;
  cursor: pointer;
}
.film-cell:hover { background: var(--bg-3); }
.film-cell.is-current {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 14%, var(--bg-3));
}

.film-shot {
  display: grid;
  place-items: center;
  overflow: hidden;
  background-color: var(--check-a);
  background-image:
    linear-gradient(45deg, var(--check-b) 25%, transparent 25%),
    linear-gradient(-45deg, var(--check-b) 25%, transparent 25%),
    linear-gradient(45deg, transparent 75%, var(--check-b) 75%),
    linear-gradient(-45deg, transparent 75%, var(--check-b) 75%);
  background-size: 8px 8px;
  background-position: 0 0, 0 4px, 4px -4px, -4px 0;
  border: 1px solid var(--line);
}
.film-shot canvas { display: block; }

.film-num {
  font-family: var(--mono);
  font-size: 9.5px;
  color: var(--fg-3);
}
.film-cell.is-current .film-num { color: var(--accent); }

/* A marked frame keeps its mark visible even while it is the current one, so the amber bar
   sits inside the shot rather than fighting the accent border for the same edge. */
.film-cell.is-marked .film-shot { border-color: #e0a33f; box-shadow: inset 0 -3px 0 #e0a33f; }
.film-cell.is-marked .film-num { color: #e0a33f; }

/* A keyed frame — the layer is pinned to a position on this one.
   Kept apart from the amber mark on THREE channels, not just colour: a diamond rather than a bar,
   the TOP-LEFT rather than the bottom edge, and pale rather than amber. A marked frame that is
   also keyed still reads as both. `position` is scoped to the keyed cell so an ordinary shot's
   layout is untouched, and the diamond sits fully inside the shot's `overflow: hidden`. */
.film-cell.has-key .film-shot { position: relative; }
.film-cell.has-key .film-shot::after {
  content: '';
  position: absolute;
  top: 3px;
  left: 3px;
  width: 5px;
  height: 5px;
  background: var(--fg);
  box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.55);
  transform: rotate(45deg);
  pointer-events: none;
}

/* ── status bar ───────────────────────────────────────────────────────── */

.statusbar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 0 10px;
  font-size: 11px;
  color: var(--fg-3);
  background: var(--bg-2);
  border-top: 1px solid var(--line);
}

.zoom-group {
  display: flex;
  align-items: center;
  gap: 3px;
}

.chip {
  height: 18px;
  min-width: 20px;
  padding: 0 6px;
  font: inherit;
  font-size: 11px;
  color: var(--fg-2);
  background: var(--bg-3);
  border: 1px solid var(--line);
  border-radius: 3px;
  cursor: pointer;
}
.chip:hover { background: #26272c; color: var(--fg); }

.zoom-label {
  min-width: 42px;
  text-align: center;
  font-family: var(--mono);
}

.doc-size {
  font-family: var(--mono);
  padding-left: 12px;
  border-left: 1px solid var(--line);
}

.frame-label {
  font-family: var(--mono);
  color: var(--accent);
}
.frame-label:empty { display: none; }

.status { font-size: 11.5px; }
.status[data-tone='ok'] { color: var(--ok); }
.status[data-tone='warn'] { color: var(--warn); }
.status[data-tone='error'] { color: var(--danger); }

/* Save state. Quiet when it is fine, loud only when the work is genuinely not stored — a
   permanently bright "Saved" trains the eye to stop reading it, which is the one moment it has
   to be read. The dotted underline marks the tooltip that says where the work actually lives. */
.save-state {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--fg-3);
  cursor: help;
  border-bottom: 1px dotted var(--line);
}
.save-state[data-state='saving'] { color: var(--fg-2); }
.save-state[data-state='unsaved'] { color: var(--accent); }
.save-state[data-state='error'] { color: var(--danger); border-bottom-color: var(--danger); }

.save-over[data-armed] {
  color: var(--danger);
  border-color: color-mix(in srgb, var(--danger) 55%, transparent);
}

/* ── the walkthrough ──────────────────────────────────────────────────── */

/* The quiet first-run dot on the top bar's Walkthrough button. An attribute, not a class, and set
   only while tour.js's seen flag is empty — a badge that never goes away is an alarm. */
.tour-btn[data-cue='new'] {
  border-color: color-mix(in srgb, var(--accent) 55%, var(--line-2));
}
.tour-btn[data-cue='new']::after {
  content: '';
  display: inline-block;
  width: 5px;
  height: 5px;
  margin-left: 6px;
  vertical-align: 1px;
  border-radius: 50%;
  background: var(--accent);
}

/* ⚠️ NO BACKDROP AND NO POINTER EVENTS. `.modal` above is `inset: 0` over a 72% black wash, which
   is right for a dialog that owns the screen and wrong for every part of this one: the walkthrough
   points at controls it expects you to reach past it and click. So the layer is transparent and
   inert, and only the card takes clicks back. Above `.modal`'s z-index 20 so a step is still
   readable if an overlay is opened while it is up. */
.tour {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: block;
  pointer-events: none;
}
/* ⚠️ THE COMPANION TO `display: block` ABOVE, and it is not decoration. A `display` on the element
   itself outranks the `[hidden]` attribute's default `display: none`, and this codebase has
   shipped a permanently-visible panel that way three times. Removing this line makes the whole
   walkthrough sit over the app forever. */
.tour[hidden] { display: none; }

/* The ring. Drawn as a detached box positioned from getBoundingClientRect rather than as an
   outline on the target itself: the target belongs to another module, controls.js re-renders rows
   underneath it, and the green already means "these layers are tied together". Inert, or it would
   swallow the click on the very control it is pointing at. */
.tour-ring {
  position: absolute;
  pointer-events: none;
  border: 2px solid var(--accent);
  border-radius: var(--radius);
  /* ⚠️⚠️ TWO SHADOWS, AND THE SECOND ONE IS THE DIM — "during the walkthrough everything else
     should be darkend a bit to highlight the area of focus" (Senzu). The accent halo is first so
     it paints on top of the wash; the 9999px spread on the second darkens the WHOLE viewport
     except this ring's own rectangle, which is what "everything else" means.

     ⚠️ IT IS A SHADOW ON THE RING, NOT A BACKDROP ELEMENT, AND THAT IS THE WHOLE POINT. A
     full-screen overlay would have to be defeated to let clicks through, and the walkthrough
     points at controls it expects you to reach past it and press. A box-shadow is painted, never
     hit-tested — and this element is `pointer-events: none` regardless — so the dim cannot
     intercept a click on the control it is highlighting. DO NOT convert this to an element.

     ⚠️ THE DIM DIES WITH THE RING, and it needs no code to do it: a step with no anchor sets
     `ringEl.hidden = true` in tour.js, and `.tour-ring[hidden]` below removes the box entirely —
     shadow included. A dark screen with nothing highlighted is therefore unreachable.

     ⚠️ #tourCard IS A LATER SIBLING in index.html, so it paints above this shadow with no z-index
     needed. Moving the card before the ring in the markup would bury the step text under the wash.

     55% is the chosen alpha: dark enough that the ringed control reads as the only lit thing,
     light enough that the document behind it is still legible while he reads the step. */
  box-shadow:
    0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent),
    0 0 0 9999px rgb(0 0 0 / 55%);
  transition: left 120ms ease, top 120ms ease, width 120ms ease, height 120ms ease;
}
.tour-ring[hidden] { display: none; }

.tour-card {
  position: absolute;
  pointer-events: auto;
  width: 280px;
  max-width: calc(100vw - 24px);
  padding: 12px 14px 10px;
  background: var(--bg-2);
  border: 1px solid var(--line-2);
  border-radius: var(--radius);
  box-shadow: 0 10px 28px rgb(0 0 0 / 45%);
}
.tour-card:focus { outline: none; }

.tour-count {
  margin: 0 0 6px;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--fg-3);
}
.tour-count[hidden] { display: none; }

.tour-title {
  margin: 0 0 6px;
  font-size: 13px;
  font-weight: 600;
  color: var(--fg);
}

.tour-text {
  margin: 0 0 10px;
  font-size: 12px;
  line-height: 1.45;
  color: var(--fg-2);
}

.tour-row {
  display: flex;
  align-items: center;
  gap: 6px;
}
.tour-gap { flex: 1; }
.tour-row .ctl-btn { width: auto; padding: 0 12px; }
.tour-row .ctl-btn[hidden] { display: none; }
.tour-quit { color: var(--fg-3); }

/* Backup and restore, at the foot of the Drafts menu. A rule above the block because it is a
   different KIND of action from the draft list above it: those switch what you are looking at,
   these move the whole store to another browser. */
.drafts-transfer { border-top: 1px solid var(--line, #2a2a33); margin-top: 10px; padding-top: 10px; }
.drafts-transfer h3 { font-size: 12px; font-weight: 600; margin: 0 0 6px; opacity: .8; }
.drafts-transfer-row { display: flex; gap: 8px; flex-wrap: wrap; }
#backupStatus[data-state="error"] { color: var(--danger, #ff6b6b); }
