/* CrunchyUI — all CSS. Linked with <link rel="stylesheet"> in the <head> so it works on file://.
 *
 * --css-build is checked by index.html. Bump BOTH whenever the stylesheet gains rules a new
 * feature needs: without the check a stale stylesheet is invisible — every script fresh, every
 * control working, the layout silently wrong. Browsers cache file:// siblings separately from the
 * page, so a plain reload really can pair a fresh index.html with a stale styles.css.
 */
:root {
  --css-build: 89;

  /* THE PAGE IS DARK, AND THIS IS THE ONLY THING THAT TELLS THE PLATFORM SO. It governs the native
     parts CSS cannot reach: an open <select> popup, scrollbars, and the browser's own checkbox and
     range chrome. Ported with the select rules below (§61 in BGM). */
  color-scheme: dark;

  /* Space a <select> keeps clear on its right for the caret drawn as a background image: 9px of
     arrow, inset 7px, plus air. ONE TOKEN because three rules need the same number, and BGM's own
     note records that a per-rule literal is how its four came to disagree. */
  --caret-gap: 22px;

  /* Set from CHECKER in ui.js at startup — these are only the fallback. */
  --chk-a: #ff00ff;
  --chk-b: #a000a0;

  --bg: #12141a;
  --panel: #1a1d26;
  --panel2: #21252f;
  --line: #2e333f;
  --ink: #dfe3ee;
  --dim: #8b94a8;
  --accent: #7ea6ff;
  --good: #7fe0a0;
  --bad: #ff8f97;

  /* ALIASES for the suite's shared token names, which the portable `suite-*` block below (copied
     unchanged from CrunchyTile) reads. This app spells muted text `--dim` and has one accent. */
  --muted: var(--dim);
  --accent2: var(--accent);
}

* { box-sizing: border-box; }

/* [hidden] LOSES TO ANY AUTHOR `display` RULE. The UA sheet says [hidden]{display:none}, but
 * `img.art { display:block }` is more specific, so hiding does nothing and the element sits there
 * showing its alt text. This companion is not optional the moment any rule sets `display` — and
 * an assertion on visibility must read getComputedStyle(...).display, never the `hidden` property,
 * which is `true` the whole time the thing is on screen. Inherited from CrunchyVFX, and hit here
 * on the very first dialog-free page. */
[hidden] { display: none !important; }

html, body { height: 100%; }

/* A COLUMN, so adding a bar between the header and the app does not mean hand-maintaining a
   `calc(100% - 46px)` that is wrong the moment anything above it changes height. */
body { display: flex; flex-direction: column; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 13px/1.5 ui-sans-serif, system-ui, "Segoe UI", Roboto, sans-serif;
}

code, .mono { font-family: ui-monospace, Menlo, Consolas, monospace; }

/* ---- chrome ------------------------------------------------------------------------------- */

header {
  display: flex; align-items: center; gap: 14px;
  padding: 10px 16px; border-bottom: 1px solid var(--line); background: var(--panel);
}
header h1 { font-size: 15px; margin: 0; letter-spacing: .2px; white-space: nowrap; }
header h1 b { color: var(--accent); }
header .tag { color: var(--dim); font-size: 12px; }
/* THE TARGET SWITCH. Deliberately the largest control in the bar: §1's split between the two
   renderers is the thing this whole app is built around, and it had been two ordinary buttons in a
   row. A segmented pair rather than a toggle, because both states are NAMED — a switch that only
   says "SVG" leaves you guessing what the other position is. */
.tswitch { display: flex; border: 1px solid var(--line); border-radius: 7px; overflow: hidden; }
.tswitch button {
  border: 0; border-radius: 0; padding: 7px 16px; font-size: 13px; font-weight: 600;
  background: var(--panel2); color: var(--dim); cursor: pointer; letter-spacing: .3px;
}
.tswitch button + button { border-left: 1px solid var(--line); }
.tswitch button:hover { color: var(--fg); }
.tswitch button.on { background: var(--accent); color: #10131a; }
/* THE HEADER DOES NOT WRAP, or the buttons jump to a second row and back as the window moves. The
   build-stamp readout used to be the thing that gave way — it was the least important item in the
   bar and it ellipsised — and it is gone (2026-08-20); `.spacer` is what absorbs the slack now, and
   it shrinks to nothing before anything else has to. */
header { flex-wrap: nowrap; }
header .fields, .hgroup { flex: none; }
header .spacer { flex: 1; }

/* Header groups, after the siblings. Undo/Redo get their OWN group with a rule down the left,
   because they are not generation — Randomize and Breed act on the KIT, these act on the WORK, and
   reading them as one row of eight buttons loses that. CrunchyBGM makes the same split. */
.hgroup { display: flex; gap: 4px; padding-left: 12px; border-left: 1px solid var(--line); }
.hgroup button { padding: 4px 8px; }
header .fields { display: flex; gap: 6px; }
button.accent { border-color: #3b4a72; color: #cfe0ff; background: #222c44; }
button.accent:hover { border-color: var(--accent); }

/* ---- overlays ------------------------------------------------------------------------------
   GENERIC. Every `.modal` gets Escape and a backdrop press from one place (index.html), never
   wired per dialog — CrunchyBGM's §39, learned after its second overlay duplicated the first's
   handlers. The backdrop IS the modal element; the box inside stops the press reaching it.

   `[hidden]` is handled by the global rule at the top of this file. It has to be: `.modal` sets
   `display: flex`, which beats the UA's `[hidden] { display: none }` outright. */
.modal {
  position: fixed; inset: 0; z-index: 80;
  display: flex; align-items: center; justify-content: center;
  background: rgba(8, 9, 14, .74);
}
.modal-box {
  background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
  box-shadow: 0 20px 60px rgba(0, 0, 0, .6);
  max-width: min(94vw, 880px); max-height: 92vh;
  display: flex; flex-direction: column; overflow: hidden;
}
.modal-head, .modal-foot {
  display: flex; align-items: center; gap: 10px;
  padding: 10px 14px; border-bottom: 1px solid var(--line);
}
.modal-foot { border-bottom: 0; border-top: 1px solid var(--line); }
.modal-head h2 { font-size: 14px; margin: 0; }
.modal-body { padding: 14px; overflow: auto; }
.modal .spacer { flex: 1; }

/* the breed grid */
.brood { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.brood .tile {
  border: 1px solid var(--line); border-radius: 7px; padding: 8px; cursor: pointer;
  background: var(--panel2); text-align: center;
}
.brood .tile:hover { border-color: var(--accent); }
.brood .tile.parent { border-color: var(--good); }
.brood .tile .row2 { display: flex; gap: 6px; justify-content: center; align-items: flex-end; }
.brood .tile .cap { font-size: 10px; color: var(--dim); margin-top: 6px;
                    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }

#deps {
  display: none; padding: 10px 16px; background: #4d1418; color: #ffd9db;
  font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 12px;
}
#deps.show { display: block; }

#app { display: flex; align-items: flex-start; gap: 0; flex: 1; min-height: 0; }

/* ---- the roster ----------------------------------------------------------------------------
   Tabs, not a dropdown: the set of elements IS the product, and a <select> shows one and hides the
   rest. Each tab draws its own element in the live theme, so the bar recolours with the kit. */
/* IT PAGES NOW RATHER THAN SCROLLING (2026-08-23). `overflow-x: auto` was the whole problem: a
   horizontal scrollbar is awkward without a trackpad, and the bar clips past ~1800px anyway. The
   pager is the kit strip's, so the two bars under the header now work the same way. */
#roster {
  display: flex; gap: 14px; flex: none; overflow: hidden; align-items: flex-end;
  padding: 3px 12px 4px; border-bottom: 1px solid var(--line); background: var(--panel2);
}
/* The pinned Icons group sits at the far end, pushed there rather than paged. */
#roster .rpin { margin-left: auto; }
#roster .pgbtn { align-self: center; }
/* THE KIT STRIP. Same bar as the roster above it because it answers the same shape of question, and
   deliberately NOT scrolling: `#roster` may scroll because you can see most of it at once and the
   overflow is a tail, where the kit list is 90 cells and a scrollbar there is a haystack. Arrows and
   a page number say how much there is; a scrollbar only says "more". */
#themebar {
  display: flex; align-items: center; gap: 8px; flex: none; overflow: hidden;
  padding: 4px 12px 5px; border-bottom: 1px solid var(--line); background: var(--panel2);
}
#themebar .tstrip { display: flex; gap: 4px; flex: 1 1 auto; overflow: hidden; }
/* A cell in the strip is the same cell as the old grid — same class, same preview, same lit state —
   so nothing about what a kit LOOKS like changed when it moved. Only narrower, to fit a row. */
#themebar .tcell { min-width: 62px; max-width: 62px; flex: none; }
#themebar .tcell .tslot { height: 34px; }
#themebar .tcell canvas { max-height: 34px; width: auto; }
#themebar .tname { font-size: 8px; }
.tbarlab { font-size: 10px; letter-spacing: .08em; text-transform: uppercase; color: var(--dim);
           flex: none; align-self: flex-start; padding-top: 2px; }
.tbarpage { font-size: 10px; color: var(--dim); flex: none; min-width: 34px; text-align: center; }

/* Two rosters, one bar (§11). The divider says "different kind of thing" without saying
   "different place" — an icon and the slot it sits in are the same kit. */
.rgroup { display: flex; flex-direction: column; gap: 1px; flex: none; }
.rgroup + .rgroup { padding-left: 14px; border-left: 1px solid var(--line); }
.rlab { font-size: 10px; letter-spacing: .08em; text-transform: uppercase; color: var(--dim); }
.rtabs { display: flex; gap: 4px; }
.etab {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  flex: none; min-width: 70px; padding: 3px 6px 2px;
  background: transparent; border: 1px solid transparent; border-radius: 5px;
}
/* A FIXED BOX WITH THE SPRITE CENTRED IN IT. The roster spans 96x20 to 12x16, so letting each chip
   size itself would step the labels up and down the bar and read as misalignment. */
.etab .art { display: flex; align-items: center; justify-content: center; height: 38px; }
.etab .art canvas { display: block; image-rendering: pixelated; }
.etab .cap { font-size: 11px; color: var(--dim); }
.etab:hover { border-color: var(--line); background: var(--panel); }
.etab.on { border-color: var(--accent); background: var(--panel); }
.etab.on .cap { color: var(--accent); }

#controls {
  width: 300px; flex: none; height: 100%; overflow-y: auto;
  padding: 12px; border-right: 1px solid var(--line); background: var(--panel);
}
/* ONE COLUMN WASTED MOST OF THE WIDTH at any desktop size, and the strips that tell you the most —
   the stretch and the crunch — were the ones pushed furthest below the fold. Two columns, grouped
   by the question each answers: left is "what am I making" and "give it to me", right is "how does
   it behave" and "what will the engine get".

   It collapses back to one below 1040px rather than squeezing, because a 300px-wide preview beside
   a 300px-wide strip is worse than either of them full width. */
#stage {
  flex: 1; height: 100%; overflow-y: auto; padding: 16px 20px;
  display: grid; grid-template-columns: 1fr; gap: 0 30px; align-content: start;
}
/* ONE TRACK. It was two — the preview on the left, the readouts on the right — and the readouts are
   tabs now, so the second track held nothing. A capped width rather than a full-bleed column: the
   preview box and the export controls read badly at 1600px, and the app is still two panes overall
   (controls, then stage), which is the split that was always doing the work. */
/* TWO TRACKS AGAIN, and for a different reason than before. The old second column held READOUTS and
   emptied into tabs; this one holds this element's own settings, which are not a view of the sprite
   and so do not belong in the tab strip. Narrow, because they are controls and controls read badly
   wide. Collapses under the same breakpoint the stage always used. */
#stage { grid-template-columns: 1fr; }
@media (min-width: 1180px) {
  #stage { grid-template-columns: minmax(0, 1020px) minmax(240px, 300px); }
}
#stage.wide { grid-template-columns: 1fr; }
#stage .col.elem { min-width: 0; }
#stage .col.elem .group { margin-bottom: 14px; }

/* THE TWO COLUMNS SCROLL SEPARATELY (maintainer's, 2026-08-21: "the center part of the window
   should be stickied, so if the right pane is scrolled down you can still see the element").
   The stage was ONE scroller holding both tracks, so reading down this element's overrides — which
   run to several screens once the palette strips and the picker grids are open — carried the
   preview off the top, and you were tuning a sprite you could no longer see.

   INDEPENDENT SCROLLERS RATHER THAN `position: sticky`, and the difference matters. Sticky pins the
   column's TOP; the moment the main column is taller than the scrollport — the Info tab's table, the
   export note, the kit sheet — its bottom goes below the fold and stays there, unreachable, because
   scrolling keeps it pinned. A column that scrolls itself can always be read to the end.

   NO MAGIC NUMBERS, WHICH IS WHY THIS SHAPE WAS PICKED. `#app` is `flex: 1; min-height: 0` and
   `#stage` is `height: 100%`, so a single stretched grid row IS the scrollport height and the
   columns inherit it — no `calc(100vh - <header> - <roster> - <kit strip>)` to go stale the next
   time a bar is added.

   ONLY ABOVE THE BREAKPOINT. Below it the stage is one track and there is no second pane to scroll
   away from, so the whole stage scrolling is both simpler and correct. */
@media (min-width: 1180px) {
  #stage { overflow: hidden; grid-template-rows: minmax(0, 1fr); align-content: stretch; }
  #stage .col { min-height: 0; overflow-y: auto; }
  /* The right pane keeps a little breathing room at its foot, or the last override section ends
     hard against the window edge with no sign there is nothing more. */
  #stage .col.elem { padding-bottom: 24px; }
}

/* PER-ELEMENT OVERRIDE SECTIONS. The chip is an offer and the section is the thing offered, so the
   chip is quiet and the section carries a rule down its left edge — a scoped copy of a kit group
   has to look like a thing pinned onto this element rather than another part of the panel. */
.esections { margin-top: 4px; }
.chiprow { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
.addover {
  background: #1b1f2b; color: #9aa3b8; border: 1px dashed #39405a; border-radius: 12px;
  padding: 4px 11px; font: inherit; font-size: 12px; cursor: pointer;
}
.addover:hover { color: #d6dcec; border-color: #5a648a; border-style: solid; }
.group.over { border-left: 2px solid #574d76; padding-left: 10px; }
.group.over > h2 { display: flex; align-items: center; justify-content: space-between; }
.delover {
  background: none; border: 1px solid #4a4160; color: #a79bc4; border-radius: 3px;
  width: 18px; height: 18px; line-height: 1; padding: 0; cursor: pointer; font-size: 13px;
}
.delover:hover { border-color: #8d7fb8; color: #ded2ff; }
/* The swatch is the point of a colour control; the arithmetic beside it says which step it landed
   on, so a shift can be read without counting along the ramp. */
/* The per-element palette list is the sidebar's picker in a narrower box — same cell, same strips,
   same scroll-to-current. Capped so a 68-entry list does not push everything else off the page. */
/* THE PARAMETRIC BADGE. Top-right of the cell's art, over the plate rather than beside the caption:
   the caption is the NAME and a mark next to it reads as part of it. Quiet — it is an affordance,
   not a warning — and it brightens with the cell it sits on so a selected icon's badge does not
   turn into the dimmest thing in a lit box. */
.scell .art { position: relative; }
.parm {
  position: absolute; top: 2px; right: 2px; z-index: 1;
  font-size: 10px; line-height: 1; padding: 1px 3px; border-radius: 3px;
  background: #1b1f2bcc; color: #8f9ab5; border: 1px solid #39405a; pointer-events: none;
}
.scell:hover .parm, .scell.on .parm { color: #d6dcec; border-color: #5a648a; }
/* The legend's copy of the badge, inline in the sentence that explains it — the same mark at the
   same weight, or the reader has to work out that the two are the same thing. */
.parmkey {
  display: inline-block; font-size: 10px; line-height: 1; padding: 1px 3px; border-radius: 3px;
  background: #1b1f2bcc; color: #8f9ab5; border: 1px solid #39405a; vertical-align: 1px;
}

.themepick.elempal { max-height: 260px; }

/* THE OVERRIDES TABLE. A colour is shown as a colour — a hex is barely one, and a bare index is the
   thing that got the first version of this feature cut. A borrowed palette carries its whole strip
   so the clash is legible in the table that explains it. */
#overrides td { vertical-align: middle; }
#overrides .gotoel { margin: 0; }
.ovsw {
  display: inline-block; width: 10px; height: 10px; margin-right: 5px; vertical-align: -1px;
  border: 1px solid #00000066; border-radius: 2px;
}
.ovramp { display: inline-flex; margin-left: 6px; vertical-align: -2px; border-radius: 2px; overflow: hidden; }
.ovramp i { width: 9px; height: 11px; }
/* An inherited role is marked, not selected — you have to be able to see WHERE the kit is pointing
   before you decide to move it, and a control that shows nothing until you touch it reads broken. */
.group.over .pick.inh { outline: 1px dashed #7b8397; outline-offset: 1px; }
.group.over .ctl.swatchrow { grid-template-columns: 88px 1fr; }
.group.over .ctl.swatchrow .picks { grid-column: 2; }

/* AN OVERRIDDEN ELEMENT IS MARKED IN THE ROSTER — the kits strip's vocabulary, for the same reason:
   it must read as "this one is special" and not as "this one is selected". */
.etab.over { border-color: #574d76; }
.etab.over .cap { font-style: italic; color: #a79bc4; }
.etab.over.on .cap { color: #d6c9f5; }
#stage .col { min-width: 0; }          /* or a wide table stops the column ever shrinking */
/* In icon mode the side column is hidden and the grid must collapse to ONE track, or the sheet
   keeps laying out inside half a stage while the other half sits empty (§8.1.12).
   A CLASS, not `:has()`. The selector would work in both browsers this ships to, but the layout
   would then depend on selector support rather than on the same boolean that hid the column —
   and the desktop webview is not the one being developed against (§0). */
#stage.wide { grid-template-columns: 1fr; }
#stage .col > h3:first-child { margin-top: 0; }
/* SUBORDINATE to an h3, for the two proof panels that now share the Engine tab. They used to be
   h3s in the side column, where each owned its own stretch of page; inside one pane they are two
   parts of one answer, and the type has to say so or the tab reads as two things bolted together. */
h4 { font-size: 10px; text-transform: uppercase; letter-spacing: .7px; color: var(--dim);
     margin: 14px 0 6px; font-weight: 600; opacity: .85; }
h4:first-child { margin-top: 0; }

/* ---- generated controls ------------------------------------------------------------------- */

.group { margin-bottom: 14px; }
.group > h2 {
  font-size: 11px; text-transform: uppercase; letter-spacing: .8px; color: var(--dim);
  margin: 0 0 6px; padding-bottom: 4px; border-bottom: 1px solid var(--line);
}
.ctl { display: grid; grid-template-columns: 88px 1fr 42px; align-items: center; gap: 8px; margin: 5px 0; }
.ctl > label { color: var(--dim); font-size: 12px; }
.ctl .val { font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 11px; color: var(--ink); text-align: right; }
/* EVERY <select> IN THE APP, NOT JUST THE ONES IN THE SIDEBAR (2026-08-24, ported from CrunchyBGM
   §61 at the maintainer's ask). This rule used to be `.ctl select`, which is the sidebar's control
   grid — so the app had TWO CLASSES OF SELECT and only one of them was styled. The other four
   (the PNG scale, the icon modal's scale, the SVG mode in the header) fell through to the native
   widget and came out as a WHITE BOX in a dark app. That is most of why the SVG mode "seemed
   hidden": it did not look like part of the application.

   BGM'S TWO HALVES, AND THEY FIX DIFFERENT WIDGETS. A `<select>` is two things and CSS reaches only
   one of them:

     `appearance: none`  opts the CLOSED control out of the native widget so these colours apply.
                         It also removes the arrow, hence the caret drawn as a background image.
                         The `background` SHORTHAND would eat that image — keep the declarations
                         apart, which is a trap this file already carried a note about.

     `color-scheme`      is the only lever over the OPEN popup, which stays a native menu no amount
                         of CSS can style. Declaring the page dark makes the platform draw its own
                         menus, scrollbars and checkboxes to match.

   IT IS A DESKTOP FIX AS MUCH AS A COSMETIC ONE. BGM's note records why it exists at all: under
   WebKitGTK a `<select>` is a real GTK combo box that IGNORES these colours, so on a system whose
   GTK theme disagrees with the app the dropdowns come out pale-on-pale and cannot be read. This app
   ships a Tauri build too, so it had the same latent bug on every select.

   BGM PINS DARK AND SFX PINS LIGHT; dark is right here for BGM's reason — these selects already
   look correct on the web and light popups in a dark editor would be a visible regression traded
   for an invisible one. */
select {
  appearance: none;
  -webkit-appearance: none;
  background-color: var(--panel2); color: var(--ink);
  border: 1px solid var(--line); border-radius: 4px;
  font: inherit; font-size: 12px;
  padding: 3px var(--caret-gap) 3px 6px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='9' height='6'%3E%3Cpath d='M0 0h9L4.5 6z' fill='%238b94a8'/%3E%3C/svg%3E");
  background-repeat: no-repeat; background-position: right 7px center;
}
/* Several engines honour this even when the popup is native, and where they do not it costs
   nothing — the `color-scheme` above is what actually carries that case. */
select option { background: var(--panel2); color: var(--ink); }

.ctl select, .ctl input[type="number"], .ctl input[type="text"] {
  grid-column: 2 / span 2;
  background: var(--panel2); color: var(--ink); border: 1px solid var(--line);
  border-radius: 4px; padding: 3px 6px; font: inherit; font-size: 12px;
  appearance: none;
}
/* ...and the select keeps the caret gap the bare rule gave it, which the line above just reset. */
.ctl select { padding-right: var(--caret-gap); }
.ctl input[type="range"] { width: 100%; accent-color: var(--accent); }

/* The ramp, editable. `input[type=color]` is a browser-supplied picker: no library, no licence.
   The vendor pseudo-elements are what let it read as a flat swatch rather than a chunky control. */
.swatches { display: flex; gap: 2px; margin: 4px 0 8px; }
input.sw {
  flex: 1; height: 22px; padding: 0; border: 1px solid var(--line); border-radius: 3px;
  background: none; cursor: pointer; min-width: 0;
}
/* The swatch and its marker share a cell so the star can sit ON the colour without the colour input
   losing its own hit area — `pointer-events: none` keeps the star from eating the click. */
.swcell { position: relative; flex: 1; min-width: 0; display: flex; }
.swmark {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-size: 11px; line-height: 1; pointer-events: none;
  color: #fff; text-shadow: 0 0 2px #000, 0 0 2px #000, 0 1px 0 #000, 0 -1px 0 #000;
}
input.sw::-webkit-color-swatch-wrapper { padding: 0; }
input.sw::-webkit-color-swatch { border: 0; border-radius: 2px; }
input.sw::-moz-color-swatch { border: 0; border-radius: 2px; }

/* A ROLE IS A SWATCH ROW, NOT A NUMBER. These pick an index into the ramp, and a slider reading
   "2" says nothing about the colour until after you have moved it. */
.ctl.swatchrow { grid-template-columns: 88px 1fr; align-items: center; }
.picks { display: flex; gap: 2px; }
.picks .pick {
  flex: 1; height: 18px; padding: 0; min-width: 0;
  border: 1px solid var(--line); border-radius: 3px; cursor: pointer;
}
.picks .pick.on { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }

/* A note INSIDE a control group, explaining what is missing and why. Smaller and dimmer than a
   stage note: it is a footnote to the controls above it, not a heading for what follows. */
.note.tight { margin: 2px 0 6px; font-size: 11px; line-height: 1.45; color: var(--dim); }

/* ---- the theme picker (§8.1.19) ---------------------------------------------------------------
   A theme IS its ramp, so the control shows ramps rather than names. Capped and scrolled for the
   same reason the icon grid is: sixteen strips is taller than the group below it deserves. */
.themepick { max-height: 246px; overflow-y: auto; padding-right: 2px; margin: 2px 0 4px; }
.tpick {
  display: block; width: 100%; text-align: left; padding: 4px 5px 3px; margin-bottom: 3px;
  background: var(--panel2); border: 1px solid transparent; border-radius: 4px;
}
.tpick .tramp { display: flex; gap: 0; height: 14px; border-radius: 2px; overflow: hidden; }
.tpick .tramp i { flex: 1; display: block; }
.tpick .tname { display: block; font-size: 11px; color: var(--dim); margin-top: 3px; }
.tpick:hover { border-color: var(--line); }
.tpick.on { border-color: var(--accent); }
.tpick.on .tname { color: var(--accent); }

/* ---- the icon sheet (§8.1.12) ----------------------------------------------------------------
   The roster in the MAIN column, because coherence is a property of the SET and 83 chips could be
   shown in a 300px sidebar but not read there. */
#sheet { margin: 4px 0 10px; }

/* EIGHT FIXED COLUMNS, not auto-fill. A count that changes with the window makes the sheet a
   different picture at every size, and the sheet's job is to be ONE readable set — the same
   argument that moved it out of the sidebar. Fixed also gives each cell real room, so the art can
   be drawn large rather than at the smallest size that fitted everywhere. */
.sgrid {
  display: grid; grid-template-columns: repeat(8, 1fr);
  gap: 6px; margin: 4px 0 14px;
}
/* ...with one fallback, because eight columns below about 1100px squeezes a cell narrower than the
   art inside it, and a fixed count that clips is not a layout. */
@media (max-width: 1180px) { .sgrid { grid-template-columns: repeat(6, 1fr); } }
@media (max-width: 860px)  { .sgrid { grid-template-columns: repeat(4, 1fr); } }

.scell {
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 10px 6px 7px; min-width: 0;
  background: var(--panel); border: 1px solid var(--line); border-radius: 5px;
}
.scell .art { display: flex; align-items: center; justify-content: center; height: 72px; }
.scell canvas { display: block; image-rendering: pixelated; }
/* A VECTOR CELL IS AN <img>, so it needs the canvas's own layout rule — and it must NOT inherit the
   canvas's `image-rendering`. Resolving a curve smoothly is the whole difference the render switch
   exists to show, and pixelating it here would draw the raster's staircase over the vector. */
.scell img { display: block; }
/* THE DROP AFFORDANCE. A dashed accent frame while a file is over the window, and a line saying
   what will happen — without it the only feedback that a drop is live is that the page did NOT
   navigate away, which is not feedback. `content` here is app chrome, the same as every label;
   §13 is about what lands in an EXPORT. */
body.dropping #app { outline: 2px dashed var(--accent); outline-offset: -6px; }
body.dropping::after {
  content: "Drop an SVG or PNG to import it as an icon";
  position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%);
  padding: 8px 16px; border-radius: 8px; pointer-events: none; z-index: 9999;
  background: var(--panel2); color: var(--ink);
  border: 1px solid var(--accent); font-size: 13px;
}
.scell img.vector { image-rendering: auto; }
.scell .cap { font-size: 11px; color: var(--dim); max-width: 100%;
              overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.scell:hover { border-color: var(--accent); }
.scell.on { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.scell.on .cap { color: var(--accent); }

/* One icon's detail. Two columns so the picture and the numbers are read together rather than
   scrolled between — the modal is small enough that stacking them would hide one. */
.idetail { display: flex; gap: 18px; align-items: flex-start; }
.ipreview { flex: none; }
.iside { flex: 1; min-width: 220px; }
.iside .group { margin: 0 0 10px; }

/* A param with four or fewer stops renders as buttons instead of a slider, so every value is
   readable without moving anything. It spans the value column too — the label IS the value. */
.segs { display: flex; gap: 3px; grid-column: 2 / span 2; }
.segs .seg { flex: 1; padding: 3px 0; min-width: 0; font-size: 11px;
             font-family: ui-monospace, Menlo, Consolas, monospace; }
.segs .seg.on { border-color: var(--accent); color: var(--accent); background: var(--panel); }

button {
  background: var(--panel2); color: var(--ink); border: 1px solid var(--line);
  border-radius: 4px; padding: 5px 10px; font: inherit; font-size: 12px; cursor: pointer;
}
button:hover { border-color: var(--accent); }
button[disabled] { opacity: .4; cursor: default; border-color: var(--line); }
button.on { border-color: var(--accent); color: var(--accent); }

input.seed { width: 92px; background: var(--panel2); color: var(--ink);
             border: 1px solid var(--line); border-radius: 4px; padding: 3px 6px;
             font: inherit; font-size: 12px; }
.seedlab { color: var(--dim); font-size: 11px; }

label.chk { display: inline-flex; align-items: center; gap: 5px; color: var(--dim);
             font-size: 12px; cursor: pointer; }
label.chk input { accent-color: var(--accent); }
#dlScale { background: var(--panel2); color: var(--ink); border: 1px solid var(--line);
           border-radius: 4px; padding: 4px 8px; font: inherit; font-size: 12px; }

.catlab { font-size: 10px; text-transform: uppercase; letter-spacing: .7px; color: var(--dim);
          margin: 8px 0 3px; }
.kits { display: flex; flex-wrap: wrap; gap: 4px; }
button.kit { padding: 3px 7px; font-size: 11px; }

/* The transparency key. `i.chk` reuses the same --chk vars the canvases do, so the swatch and the
   thing it explains can never drift apart — a legend showing the wrong colour is worse than none. */
.legend { display: inline-flex; align-items: center; gap: 6px; color: var(--dim); font-size: 11px; }
.legend i.chk {
  width: 15px; height: 15px; border-radius: 3px; border: 1px solid var(--line);
  background-color: var(--chk-a);
  background-image:
    linear-gradient(45deg, var(--chk-b) 25%, transparent 25%, transparent 75%, var(--chk-b) 75%),
    linear-gradient(45deg, var(--chk-b) 25%, transparent 25%, transparent 75%, var(--chk-b) 75%);
  background-size: 8px 8px;
  background-position: 0 0, 4px 4px;
}

.row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 8px 0; }

/* WHAT GOES IN THE ARCHIVE. A long list most sessions never open, so it is a <details> and the
   summary carries the two numbers that decide whether anyone needs to. The items are a dense grid
   rather than a column: 151 names down the page would push Export off the screen entirely, and the
   point of the control is that it stays cheap to ignore. */
#exPick { margin: 6px 0 10px; font-size: 12px; }
#exPick > summary { cursor: pointer; color: var(--dim); padding: 3px 0; }
#exPick > summary:hover { color: var(--ink); }
#exPick > summary b { color: var(--ink); font-weight: 600; }
#exGroups { margin: 6px 0 0; padding: 8px 10px; border: 1px solid var(--line); border-radius: 7px;
            background: var(--panel); max-height: 320px; overflow-y: auto; }
/* THE EXPORT PACKS. A row per pack: name, how many defs, how big, and the button. Laid out as a
   grid rather than a flex row so the SIZES line up in a column — the whole point of the tab is
   comparing them, and a ragged column of numbers is the one thing that stops you doing that. */
.packrow { display: grid; grid-template-columns: 1fr auto 92px auto; align-items: center;
           gap: 10px; padding: 7px 10px; border-radius: 6px; }
.packrow + .packrow { margin-top: 4px; }
.packrow:nth-child(odd) { background: var(--panel2, rgba(255,255,255,.02)); }
/* THE DEFAULT READS AS THE DEFAULT. A rule above it and a brighter name, because "Kit" is the one
   most people want and it sits BELOW the five it is made of — which is the honest order (the parts,
   then the combinations) and would otherwise bury the recommendation at the bottom. */
.packrow.lead { margin-top: 10px; border-top: 1px solid var(--line); border-radius: 0 0 6px 6px;
                padding-top: 11px; }
.packrow.lead .packname b { color: var(--hi, var(--ink)); }
/* THE REASON ARRIVES BEFORE THE NUMBERS. Boxed rather than a plain note, because it is explaining a
   figure that would otherwise look like a bug — and a paragraph of grey text above a list of sizes
   is a paragraph nobody reads. */
.movenote { margin: 0 0 10px; padding: 8px 10px; border-radius: 6px;
            border: 1px solid var(--line); background: var(--panel2, rgba(255,255,255,.02)); }
.movenote b { color: var(--ink); }
.packname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.packn { color: var(--dim); font-variant-numeric: tabular-nums; }
.packsize { text-align: right; font-variant-numeric: tabular-nums; }

/* ---- stage -------------------------------------------------------------------------------- */

h3 { font-size: 11px; text-transform: uppercase; letter-spacing: .8px; color: var(--dim);
     margin: 18px 0 8px; }
h3:first-child { margin-top: 0; }

/* THE PREVIEW'S TAB STRIP — Export and Live, one box. Deliberately NOT `.tswitch`, which is the
   header's segmented control: that shape says "two settings of one thing" (which renderer), and
   this says "two views of one thing" (the sprite, and the sprite working). A segmented pill sitting
   above a pane would read as a second target switch.

   THE SELECTED TAB MERGES INTO ITS PANE, which is the same trick the `tab` element does in the
   engine and it earns its keep for the same reason: shading alone leaves two boxes where one
   happens to be lighter, and breaking the line between the tab and the panel is what says "this is
   the page you are on". Here it is `margin-bottom: -1px` over the pane's own border, so the active
   tab's bottom edge is covered by the pane rather than drawn — a repaint, not an erase, exactly as
   in `frameOps`. Keep the two `-1px`s in step: they are the join. */
.vtabs { display: flex; gap: 3px; margin: 0 0 -1px 2px; position: relative; z-index: 1; }
.vtabs button {
  font: inherit; font-size: 12px; padding: 5px 14px; cursor: pointer;
  color: var(--dim); background: transparent;
  border: 1px solid transparent; border-bottom: none; border-radius: 7px 7px 0 0;
}
.vtabs button:hover { color: var(--ink); }
.vtabs button.on {
  color: var(--ink); background: var(--panel); border-color: var(--line);
  /* The seam. The pane paints over this row, so the tab has no bottom edge where the two meet. */
  padding-bottom: 6px;
}
.vpane { border: 1px solid var(--line); border-radius: 0 7px 7px 7px; background: var(--panel);
         padding: 12px; }
.vpane > :first-child { margin-top: 0; }
/* THE KIT SHEET'S SCENE TABS ARE SPRITES, not `.vtabs`. Everything here is layout only — no
   colours, no borders, no radius — because every pixel you can see is the kit's, drawn by the
   engine. A CSS box around a sprite tab would be the app disagreeing with the element about where
   the tab ends.

   The button is a bare hit target with the label sitting ON the sprite, exactly as the kit sheet's
   own lettering sits on its scenes: §13 says no export ever contains text, and the cheapest way to
   keep that true is for the words to live where a PNG encoder cannot reach them. */
/* THREE LAYERS, AND THE MIDDLE ONE IS THE BAR. The strip used to be lifted as a whole, which put
   every tab in front of the bar — so an UNSELECTED tab's bottom edge sat on top of the thing it is
   supposed to be tucked behind, and the two states differed only in shade. Half the cue was
   missing: a tab you are not on goes BEHIND the panel, and that is as much of the metaphor as the
   open edge is.
      selected tab  z 2   its open bottom paints over the bar's top border — the seam
      the bar       z 1   covers the closed tabs' bottom edges, tucking them behind it
      other tabs    z 0
   `.ktabs` must NOT carry a z-index of its own: that makes it a stacking context and traps the
   selected tab's `z: 2` inside it, so the bar (a sibling, later in the DOM) wins and paints over
   every tab including the selected one. Which is the bug this comment is here to stop recurring. */
.ktabs { display: flex; align-items: flex-end; padding-left: 12px; }
.ktab { position: relative; z-index: 0; padding: 0; border: 0; background: none; cursor: pointer;
        line-height: 0; }
.ktab.on { z-index: 2; }
/* The last-resort tab: no sprite could be built, so the label needs a box of its own or two of them
   pile up in the corner and the strip reads as broken CSS rather than as a kit the engine could not
   draw. Deliberately plain — it is a fault indicator, not a style. */
.ktab.bare { padding: 4px 12px; border: 1px dashed var(--line); border-radius: 5px 5px 0 0;
             line-height: 1.4; }
.ktab.bare .klab { position: static; }
/* Either target: a canvas of real pixels, or the same description as vector. `pixelated` belongs to
   the raster one only — a vector tab is smooth by definition and that difference is the point of
   there being two (§1). `.kitpart` is positioned absolutely INSIDE a scene; out here it is an
   ordinary block, which the scoped rule below already leaves alone. */
.ktab canvas, .ktab img { display: block; }
.ktab canvas { image-rendering: pixelated; }
.ktab img { image-rendering: auto; }
.ktab .klab {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-size: 11px; font-weight: 700; letter-spacing: .5px; line-height: 1;
  pointer-events: none;
  /* Colour and halo are set per tab in index.html, derived from the pixel actually behind the
     label — a fixed pair is readable on most kits and invisible on the light ones, and "most kits"
     is not a standard the page people judge a kit on gets to use. These are the fallback. */
  color: var(--ink);
  text-shadow: -1px 0 0 #000, 1px 0 0 #000, 0 -1px 0 #000, 0 1px 0 #000;
}
.ktab:not(.on) .klab { opacity: .72; }
/* Positioned so it can sit between the two tab layers above. */
.ktabband { position: relative; z-index: 1; }
canvas.ktabband { display: block; image-rendering: pixelated; }
img.ktabband { display: block; image-rendering: auto; }
#kitwrap .kitpane { width: max-content; max-width: 100%; }

/* A checkerboard, never a flat colour: a near-black ramp entry on a near-black page reads as a
   hole, and "transparent" and "very dark" must never look the same in a preview you trust. */
.checker, canvas.art, img.art {
  background-color: var(--chk-a);
  background-image:
    linear-gradient(45deg, var(--chk-b) 25%, transparent 25%, transparent 75%, var(--chk-b) 75%),
    linear-gradient(45deg, var(--chk-b) 25%, transparent 25%, transparent 75%, var(--chk-b) 75%);
  background-size: 12px 12px;
  background-position: 0 0, 6px 6px;
}
canvas.art, img.art { image-rendering: pixelated; display: block; }
/* The SVG preview must NOT be pixelated — showing it scale smoothly beside the raster is the
   entire point of having two targets. */
img.art.vector { image-rendering: auto; }

/* The verdict is the only thing coloured: a table of numbers where one row is red reads faster than
   a legend, and the two failing levels are deliberately different — "faint" is a judgement call and
   "invisible" is not. */
.facts tr.lg-faint td:last-child { color: #e0b055; }
.facts tr.lg-lost td { color: #e8697d; }
.facts tr.lg-lost td:last-child { font-weight: 600; }

.frame { display: inline-block; padding: 10px; border: 1px solid var(--line); border-radius: 6px;
         background: var(--panel); }
.thumbs { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-start; }
.thumb { text-align: center; }
.thumb .cap { font-size: 11px; color: var(--dim); margin-top: 4px; }

table.facts { border-collapse: collapse; font-family: ui-monospace, Menlo, Consolas, monospace;
              font-size: 12px; }
table.facts td { padding: 2px 14px 2px 0; }
table.facts td:first-child { color: var(--dim); }
table.facts b { color: var(--good); font-weight: 600; }

.note { color: var(--dim); font-size: 12px; max-width: 62ch; }
.warn { color: var(--bad); }

/* ---- the theme picker (§8.1.24). Three columns because the cell has to be big enough that a
   drift and a glow are legible in it; below about 70px wide they turn into fringe. */
.ctl.stack { display: block; }
.ctl.stack > label { display: block; width: auto; margin-bottom: 3px; }
.themegrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; margin-top: 4px; }
.tcell { overflow: hidden; display: flex; flex-direction: column; align-items: center; gap: 2px;
         padding: 4px 2px; background: #171b26; border: 1px solid #2a3040; border-radius: 3px;
         cursor: pointer; overflow: hidden; }
.tcell:hover { border-color: #47506a; }
.tcell.on { border-color: #6ea8ff; background: #1d2434; }
.tcell canvas { image-rendering: pixelated; max-width: 100%; height: auto; }
.tcell .tname { font-size: 9px; color: #9aa4bd; line-height: 1.1; text-align: center;
                white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
.tcell.on .tname { color: #cfe0ff; }

.themepage { display: flex; align-items: center; justify-content: center; gap: 10px; margin-top: 5px; }
.pgbtn { padding: 1px 9px; font-size: 13px; line-height: 1.3; }
.pgbtn:disabled { opacity: 0.35; cursor: default; }
.pgnum { font-size: 11px; color: var(--dim); font-variant-numeric: tabular-nums; }

/* ---- the kit sheet (§8.2). The canvas is the sprites; the spans are placeholder lettering that
   never enters an export (§13). */
/* STACKED AND LEFT-ALIGNED. Side by side, the two screens competed for the same eye and the pair
   drifted to the middle of a wide stage with a gulf beside the sidebar — so comparing a control to
   its effect meant crossing empty space. Stacking puts one screen under the other at the same left
   edge, which is also the order they are read in, and `flex-start` anchors that edge to the panel
   the controls live in. The gap is the only thing separating them now that neither sits on a
   backdrop, so it has to be bigger than a border. */
/* GAP ZERO, and that is load-bearing now. It was 18px to separate two stacked scenes; the children
   are a tab strip, the bar it sits on and the scene under that, and any gap at all breaks the seam
   the tabs exist to demonstrate. The scenes no longer stack, so nothing wants the space. */
#kitwrap { display: flex; flex-direction: column; align-items: flex-start;
           gap: 0; padding: 10px 0 10px 12px; }
/* The picture pickers. Same shape as the theme grid on purpose — a grid of pictures means "pick a
   thing by looking at it", and two controls that do the same job should not look different. Three
   across rather than the theme grid's three-wide-by-four: these live inside a group among other
   controls, so the page has to be short enough not to bury what follows it. */
/* THE PICKER'S FILTER. On every picker, not only the long ones: a box that appears once a list
   passes some length is a control that moves under you as the roster grows, and the roster only
   grows. Sits between the header and the grid, full width, so the eye goes header -> filter ->
   results in reading order. */
.pickfind { display: flex; align-items: center; gap: 6px; margin: 4px 0 5px; width: 100%; }
.pickfindin {
  flex: 1; min-width: 0; font: inherit; font-size: 12px;
  padding: 4px 7px; border-radius: 4px;
  background: var(--panel2); color: var(--ink); border: 1px solid var(--line);
}
.pickfindin:focus { outline: none; border-color: var(--accent); }
.pickfindin::placeholder { color: var(--dim); }
/* Only drawn while a query is live — without it a short result set and a short list look the
   same, which is the one thing a narrowed grid cannot say for itself. */
.pfound { font-size: 11px; color: var(--dim); white-space: nowrap; }
/* The download's own filename, under the pack's name. Monospace because it is a FILENAME and the
   hyphens and the extension are the part that matters; dim because it is an answer to a question
   nobody asked until they had already read the row above it. */
.packfile { display: block; font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 11px; color: var(--dim); }
/* The sheet reuses `.pickfindin`, so there is one filter box in the app rather than two that drift.
   It only needs a width here: in the sidebar the input fills a 300px column, and over the sheet it
   would otherwise stretch the whole eight-column grid. */
#sheetFindRow { align-items: center; gap: 8px; }
#sheetFind { max-width: 320px; }
/* A native colour input, sized like the other element controls. `appearance:none` for the same
   reason every <select> here needs it: under WebKitGTK the unstyled widget is a system control that
   ignores the app's colours, so on a Tauri build it comes out as a pale box in a dark panel. */
.elcolor {
  appearance: none; -webkit-appearance: none;
  width: 100%; height: 26px; padding: 2px;
  background: var(--panel2); border: 1px solid var(--line); border-radius: 4px; cursor: pointer;
}
.elcolor::-webkit-color-swatch-wrapper { padding: 0; }
.elcolor::-webkit-color-swatch { border: none; border-radius: 2px; }
.elcolor::-moz-color-swatch { border: none; border-radius: 2px; }
.pickgrid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; width: 100%; }
.pcell { display: flex; flex-direction: column; align-items: center; gap: 2px;
         padding: 3px 2px 2px; background: var(--panel2); border: 1px solid var(--line);
         border-radius: 4px; cursor: pointer; overflow: hidden; }
.pslot { display: flex; align-items: center; justify-content: center; height: 46px; width: 100%; }
.pcell canvas { display: block; image-rendering: pixelated; max-width: 100%; }
.pcell .pname { font-size: 10px; color: var(--dim); max-width: 100%;
                overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Occupies its track, draws nothing, catches nothing. */
.pcell.ghost { visibility: hidden; pointer-events: none; }
.pcell:hover { border-color: var(--accent); }
.pcell.on { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.pcell.on .pname { color: var(--accent); }
.pgnum { font-size: 11px; color: var(--dim); }

/* ---- a picker, collapsed ------------------------------------------------------------------
   THE FIVE GRIDS WERE 55% OF THE PANEL. Surface holds 29 of the app's 53 controls and every one
   of the picture pickers, so five 3x3 pages at ~200px each pushed Palette and everything under it
   far below the fold. A chip is ~46px, which is the whole fix: 1000px of grids becomes 260px of
   current values.

   NOT A TAB BAR, and not an accordion either. Both hide siblings, and these five compose into ONE
   look — the kit sheet's argument that coherence is a property of the SET (§8.2) applies just as
   well to the controls that make it. Collapsed still shows every section header and every current
   value at once; what it drops is the nine options you did not pick.

   The same trade the Icons roster already makes: one chip that remembers where you were. */
/* THE LABEL IS NOT THE TOGGLE, and that is not a style choice — `#controls .ctl > label` is what
   two DOM assertions read to prove a param that applies is on screen, matching textContent
   exactly. Both the element type and the text have to stay untouched, so the affordance goes on
   the chip. */
.pcaret { font-size: 9px; line-height: 1; color: var(--dim); }
/* Sits in the pager row and matches it, but deliberately does NOT share `.pgbtn` — see index.html. */
.pgshut { padding: 1px 9px; font-size: 13px; line-height: 1.3; margin-left: 6px; }

/* ---- the live widget ------------------------------------------------------------------------
   The checker behind it for the same reason every other preview has one: "transparent" and "very
   dark" must never look the same (§8.1.1). `touch-action: none` so dragging on a touch screen does
   not scroll the page out from under the widget you are pressing. */
canvas.live {
  image-rendering: pixelated; display: block;
  touch-action: none; user-select: none;
  background-color: var(--chk-a);
  background-image:
    linear-gradient(45deg, var(--chk-b) 25%, transparent 25%, transparent 75%, var(--chk-b) 75%),
    linear-gradient(45deg, var(--chk-b) 25%, transparent 25%, transparent 75%, var(--chk-b) 75%);
  background-size: 12px 12px; background-position: 0 0, 6px 6px;
}
canvas.live.togglable { cursor: pointer; }
canvas.live.draggable { cursor: grab; }
canvas.live.draggable:active { cursor: grabbing; }

/* FULL WIDTH, TITLE INSIDE (maintainer's, 2026-08-23). The chip used to start in column 2 — where
   every other control's input starts — with the label in column 1, which is why it read as inset.
   It spans the row now and carries its own title line above the existing one; the outer label is
   hidden rather than removed, because three DOM rows read `#controls .ctl > label` for its text and
   its hint title. */
.ctl.pick > label { display: none; }
/* THE CONTROL'S OWN LABEL, MOVED — so it keeps the label's type exactly (`.ctl > label` is
   12px/dim) and only its position changes. Uppercasing it read well but made a control title look
   like a SECTION heading, and the ask was to keep the design and move the title. */
.pickone .ptitle, .pickhead .ptitle {
  display: block; text-align: center; font-size: 12px; color: var(--dim); padding: 1px 0 3px;
}
.pickone .prow, .pickhead .prow { display: flex; align-items: center; gap: 8px; }
.pickone:hover .ptitle, .pickhead:hover .ptitle { color: var(--ink); }

.pickone {
  grid-column: 1 / -1; min-width: 0;
  display: block;
  background: var(--panel2); border: 1px solid var(--line); border-radius: 4px;
  padding: 2px 8px 2px 2px; cursor: pointer;
  font: inherit; font-size: 12px; color: var(--ink); text-align: left;
}
.pickone:hover { border-color: var(--accent); }
.pickone:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; }
/* A FIXED ART BOX WIDE ENOUGH FOR THE WIDEST SWATCH, so the names line up down the column and no
   canvas is ever scaled to fit. The frame swatches are 40x30 and the overlay's panel is 46 square;
   50 clears both, and the 46px height comes from `.pslot` unchanged. Letting CSS shrink one would
   resample a pixel image, which is the one thing this app does not do (§0.2). */
.pickone .pslot { flex: 0 0 auto; width: 50px; }
.pickone .poname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* AN EXAMPLE, NOT YOUR KIT. A section set to `none` borrows the first real value so it can say
   what it is for — and it is faded because §8.1.1's rule is not only about the harness: anything
   shown that is not the thing has to be visibly not the thing. Crisp art beside the word "none"
   would be a picture of a kit the user does not have. */
.pickone.eg .pslot { opacity: .42; }
.pickone.eg .poname { color: var(--dim); font-style: italic; }
.pickone.eg:hover .pslot { opacity: .75; }

/* HOW MANY THERE ARE. Right-aligned against the caret, dim, tabular — it is a fact about the list
   rather than a label, and it must not compete with the value's name for the eye. `margin-left:auto`
   moved off `.pcaret` onto this, so the pair sits together at the end of the row. */
.pcount { font-size: 10px; color: var(--dim); font-variant-numeric: tabular-nums;
          margin-left: auto; opacity: .8; }
.pickone:hover .pcount, .pickhead:hover .pcount { opacity: 1; }
.pickone .pcaret, .pickhead .pcaret { margin-left: 5px; }

/* THE OPEN PICKER'S OWN HEADER — the same row you clicked, with the caret turned over, so the way
   out is where the way in was. Styled from `.pickone` rather than sharing its class, because the
   suite asserts the collapsed chip is gone while the grid is open (two representations of one
   control is what that check is for). Square-bottomed, so it reads as attached to its grid. */
.pickhead {
  display: block; width: 100%;
  padding: 4px 8px; text-align: left; font: inherit; color: var(--ink);
  background: var(--panel2); border: 1px solid var(--accent);
  border-bottom-color: var(--line);
  border-radius: 6px 6px 0 0; cursor: pointer;
}
.pickhead:hover { border-color: var(--accent); }
.pickhead:focus-visible { outline: 1px solid var(--accent); outline-offset: 1px; }
.pickhead .pslot { flex: 0 0 auto; width: 50px; }
.pickhead .poname { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.pickhead + .pickgrid { border-top-left-radius: 0; border-top-right-radius: 0; }

/* A CONTROL THAT BELONGS TO AN OPEN SECTION. The rule down the left is the same device the header's
   `.hgroup` uses to say "these go together" — cheaper than a box, and it does not introduce a
   second colour language the way a tinted section would (the previews are the colour in here).
   The label column narrows by the indent, and that misalignment is the POINT: it is what says these
   rows answer to the one above them rather than to the group. */
.ctl.owned {
  margin-left: 9px; padding-left: 9px; border-left: 1px solid var(--line);
  grid-template-columns: 79px 1fr 42px;
}
.ctl.owned > label { color: var(--dim); }
/* The theme grid gets the picture pickers' two rules: a fixed art box so the names line up, and
   invisible fillers so a short page is still a full one. */
.tslot { display: flex; align-items: center; justify-content: center; height: 54px; width: 100%; }
/* CLAMPED, because a fixed box does not constrain what is put in it. A theme whose panel carries
   outer ornaments builds a taller sprite than one that does not, `fitZoom` already bottoms out at
   1x, and the canvas then overflowed the slot and sat on top of the name — the box was fixed and
   the picture was not. */
.tslot canvas { max-width: 100%; max-height: 100%; }
.tcell.ghost { visibility: hidden; pointer-events: none; }

.kitscreen { position: relative; image-rendering: pixelated; }
.kitscreen canvas { image-rendering: pixelated; display: block; }
/* A vector part is placed, not blitted — same coordinates the raster path uses. NOT pixelated:
   showing the SVG scale smoothly beside the raster is the entire point of having two targets. */
.kitscreen .kitpart { position: absolute; image-rendering: auto; }
/* colour and halo come from the KIT, set inline — see renderKitSheet */
.kittext { position: absolute; line-height: 1; white-space: nowrap; pointer-events: none; }
/* A label given a BOX rather than a corner — see `at` in index.html. Centring by flexbox instead of
   by guessing the glyph's width and the font's ascent, which is off by a couple of pixels in a
   direction that changes with the character. */
.kittext.boxed { display: flex; align-items: center; justify-content: center; }
.kittext.b { font-weight: 700; letter-spacing: 0.5px; }

/* ---- SEARCH (§12, anchored) --------------------------------------------------------------------
   App chrome, deliberately: this is workspace navigation sitting above the thing you are editing,
   and drawing it from the kit would put the app's own controls at the mercy of a rolled ramp
   (§8.1.27-29, the same call the Preview tabs made). */
.group.find { padding-bottom: 6px; }
#findBox {
  width: 100%; box-sizing: border-box; padding: 6px 8px;
  background: #11151c; color: #cfd6e4; border: 1px solid #2a3342; border-radius: 4px;
  font: inherit; font-size: 12px;
}
#findBox:focus { outline: none; border-color: #4c7fd4; }
.findout { display: flex; flex-direction: column; gap: 2px; margin-top: 6px; }
.findout:empty { margin-top: 0; }
.findhit {
  display: flex; align-items: baseline; gap: 6px; width: 100%; text-align: left;
  padding: 4px 6px; background: #161b24; border: 1px solid transparent; border-radius: 3px;
  color: #cfd6e4; font: inherit; font-size: 12px; cursor: pointer;
}
.findhit:hover { background: #1d2431; border-color: #33405a; }
.findhit .fname { font-weight: 600; }
.findhit .fkind { margin-left: auto; color: #7b879b; font-size: 10px; text-transform: uppercase; }
/* WHY A RESULT IS OUT OF REACH, said on the result itself rather than after you click it. */
.findhit .fwhy { color: #d8a657; font-size: 10px; }
.findhit.sel { background: #223049; border-color: #4c7fd4; }
.findmore {
  width: 100%; margin-top: 2px; padding: 3px 6px; font-size: 11px;
  background: transparent; border: 1px dashed #33405a; border-radius: 3px; color: #8b97ab;
  cursor: pointer;
}
.findmore:hover { color: #cfd6e4; border-color: #4c7fd4; }
.findhit:disabled { cursor: default; opacity: 0.72; }
.findhit:disabled:hover { background: #161b24; border-color: transparent; }
/* The flash is how the eye finds a row it was not already on; it fades so it does not become
   furniture. */
.ctl.flash { animation: ctlflash 1.2s ease-out; }
@keyframes ctlflash {
  from { background: #2b3a55; }
  to   { background: transparent; }
}

/* The flow's play control. App chrome, like every other widget of the app's own (§8.1.27-29). */
.playbtn { margin-top: 6px; font-size: 11px; padding: 3px 10px; }
.playbtn.on { border-color: #4c7fd4; color: #cfe0ff; }

/* ---- A KIT CELL IN THE MERGED PICKER. The kits list is gone: a kit and a theme were measured to
   be the same kind of thing, so they share one grid (see `styleCells`). The cell is deliberately
   only just distinguishable — a hairline warm edge saying "this one also brings a frame" — because
   a loud badge would re-assert the difference the merge exists to retire. Not colour ALONE: the
   name is italic too, so the distinction survives a monochrome display. */
/* The way OUT of a group that has hidden something. A note that says where a control went is half
   an answer; this carries the trip. Sized as body text rather than as a control, because it is a
   sentence with a door in it and not a setting. */
.gotoel {
  display: inline-block; margin-top: 3px; padding: 2px 8px; font-size: 11px;
  background: #1b2333; color: #9dc0ff; border: 1px solid #33405a; border-radius: 3px; cursor: pointer;
}
.gotoel:hover { background: #223049; border-color: #4c7fd4; color: #cfe0ff; }

.tcell.kit { border-color: #3a3550; }
.tcell.kit .tname { font-style: italic; color: #a79bc4; }
.tcell.kit:hover { border-color: #574d76; }
.tcell.kit.on .tname { color: #d6c9f5; }

/* ---------- WELCOME TOUR (§12) ----------
   PORTED FROM CrunchyVFX, whose runner has been in service longest and is dependency-free: the
   overlay covers the screen, and the HOLE's enormous box-shadow is what dims everything except the
   highlighted target. Copied rather than reinvented, which is the suite's own vendoring rule —
   `icons-vfx.js` is never edited here for the same reason, and a second implementation of a
   spotlight would drift from that one in exactly the ways nobody notices until a card lands
   off-screen.

   The one thing that is NOT ported is how a step finds its target; see `TOUR` in index.html. */
.tour { position: fixed; inset: 0; z-index: 300; }
.tour[hidden] { display: none; }
/* 100vmax, not 9999px (2026-10-08): a shadow that wide is a ~20000 px texture, and with the tour open
   it closed CrunchyTile's browser window on AJ's machine. 100vmax still covers any viewport. */
.tour-hole {
  position: absolute; border-radius: 8px; border: 2px solid var(--accent);
  pointer-events: none; transition: left 0.18s ease, top 0.18s ease, width 0.18s ease, height 0.18s ease;
  box-shadow: 0 0 0 100vmax rgba(8, 9, 14, 0.82);
}
.tour-hole.notarget { border-color: transparent; }
.tour-card {
  position: absolute; width: min(360px, 90vw);
  background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
  padding: 14px 16px; box-shadow: 0 12px 40px rgba(0, 0, 0, 0.55);
}
.tour-title { font-size: 15px; font-weight: 600; color: var(--accent); margin-bottom: 6px; }
.tour-body { font-size: 13.5px; color: var(--ink); line-height: 1.5; }
/* WHERE IT LIVES, DERIVED. A step that names a control says so from `PARAMS`' own group rather than
   from typed prose, so the sentence cannot describe the wrong place after a control moves (§12). */
.tour-where { font-size: 12px; color: var(--dim); margin-top: 7px; }
.tour-foot { display: flex; align-items: center; justify-content: space-between; margin-top: 14px; gap: 10px; }
.tour-progress { font-size: 12px; color: var(--dim); }
.tour-btns { display: flex; gap: 8px; }
.tour-btns button { padding: 6px 12px; font-size: 13px; }

/* The live pane's placeholder caption. Laid OVER the canvas, never painted into it — see
   `LIVE_TEXT` in elements.js for why that distinction is structural rather than stylistic.
   Italic and slightly transparent so it reads as a stand-in rather than as part of the sprite
   (§8.1.1 — anything the harness draws must be visibly not-content). */
#live { position: relative; }
.livetext {
  position: absolute; pointer-events: none; white-space: nowrap;
  font-family: "Pixel Operator", ui-monospace, monospace; font-style: italic;
  opacity: .78; text-shadow: 0 1px 0 rgba(0, 0, 0, .35);
}

/* THE CRUNCHY SUITE (§80, css-build 19): suite.js's dialog. PORTABLE like the lib-* block: the suite's shared
   custom properties and `suite-*` classes only, so it copies into every Crunchy tool unchanged. */
.suite-overlay { position: fixed; inset: 0; z-index: 260; background: rgba(8, 7, 12, 0.78); display: flex;
                 align-items: center; justify-content: center; padding: 16px; }
.suite-panel { width: min(640px, 100%); max-height: 100%; overflow: auto; background: var(--panel);
               border: 1px solid var(--line); border-radius: 8px; padding: 16px 18px; }
.suite-head { display: flex; align-items: center; gap: 8px; }
.suite-title { margin: 0; flex: 1; font-size: 17px; color: var(--accent); }
.suite-close { font-size: 16px; line-height: 1; padding: 4px 9px; }
.suite-line { margin: 6px 0 12px; color: var(--ink); }
.suite-list { display: grid; gap: 8px; }
.suite-tool { display: flex; align-items: center; gap: 12px; background: var(--panel2); border: 1px solid var(--line);
              border-radius: 6px; padding: 10px 12px; }
.suite-tool.here { border-color: var(--accent); }
.suite-icon { font-size: 22px; width: 1.4em; text-align: center; color: var(--accent2); }
.suite-body { flex: 1; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.suite-name { font-size: 14px; }
.suite-what { font-size: 12.5px; color: var(--muted); }
.suite-go { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; white-space: nowrap; }
.suite-link { font-size: 12.5px; color: var(--accent2); text-decoration: none; border: 1px solid var(--line);
              border-radius: 4px; padding: 3px 9px; }
.suite-link:hover { border-color: var(--accent2); }
.suite-here, .suite-soon { font-size: 12px; color: var(--muted); }
.suite-here { color: var(--accent); }
.suite-foot { margin: 12px 0 0; font-size: 12px; color: var(--muted); }
@media (max-width: 520px) { .suite-tool { flex-wrap: wrap; } .suite-go { flex-direction: row; } }

#suiteBtn { white-space: nowrap; }

/* THE LIBRARY (css-build 89): the desktop asset viewer (library.js), VENDORED from CrunchyTile with
   library.js itself. PORTABLE: only the suite's shared
   custom properties and `lib-*` classes, so this block copies into any Crunchy tool (PROJECTS-HANDOFF.md). */
.lib-overlay[hidden] { display: none !important; }   /* self-contained, as .suite-overlay below */
.lib-overlay { position: fixed; inset: 0; z-index: 250; background: rgba(8, 7, 12, 0.78); display: flex;
               align-items: center; justify-content: center; }
.lib-panel { width: min(1100px, 94vw); height: min(820px, 90vh); display: flex; flex-direction: column; gap: 10px;
             background: var(--panel); border: 1px solid var(--line); border-radius: 8px; padding: 14px 16px; }
.lib-busy .lib-panel { cursor: progress; }
.lib-top, .lib-bar, .lib-tools, .lib-link { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.lib-title { margin: 0; font-size: 16px; color: var(--accent); }
.lib-spacer { flex: 1; }
.lib-close { font-size: 16px; line-height: 1; padding: 4px 9px; }
.lib-path { font-size: 11px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 46ch; }
.lib-links { display: grid; gap: 6px; background: var(--panel2); border: 1px solid var(--line); border-radius: 6px; padding: 8px 10px; }
.lib-link strong { min-width: 4.5em; }
.lib-link.linked strong { color: var(--accent2); }
.lib-toggle { font-size: 12px; display: inline-flex; align-items: center; gap: 4px; }
.lib-search { flex: 1; min-width: 14em; }
.lib-chip { font-size: 12px; padding: 3px 10px; border-radius: 12px; }
.lib-chip.on { border-color: var(--accent); color: var(--accent); }
.lib-grid { flex: 1; overflow: auto; padding-right: 4px; scrollbar-color: var(--line) transparent; }
.lib-h { font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); margin: 10px 0 6px; font-weight: 600; }
.lib-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }
.lib-card { background: var(--panel2); border: 1px solid var(--line); border-radius: 6px; padding: 6px; display: flex;
            flex-direction: column; gap: 4px; min-width: 0; }
.lib-pic { padding: 6px; height: 110px; display: flex; align-items: center; justify-content: center; overflow: hidden;
           background: var(--bg); }
.lib-thumb { image-rendering: pixelated; width: 100%; height: 100%; object-fit: contain; }
.lib-name { font-size: 13px; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: text; }
.lib-rename { width: 100%; font-size: 13px; }
.lib-meta { font-size: 11px; color: var(--muted); }
.lib-badges { display: flex; gap: 4px; flex-wrap: wrap; }
.lib-badge { font-size: 10px; padding: 0 5px; border-radius: 3px; border: 1px solid var(--accent2); color: var(--accent2); }
.lib-acts { display: flex; gap: 4px; flex-wrap: wrap; }
.lib-acts button { font-size: 11px; padding: 2px 6px; }
.lib-danger.armed { border-color: var(--bad); color: var(--bad); }
.lib-start { flex: 1; overflow: auto; display: flex; flex-direction: column; gap: 6px; align-items: flex-start; }
.lib-recent { display: flex; flex-direction: column; align-items: flex-start; gap: 1px; min-width: 24em; text-align: left; }
.lib-hint { color: var(--muted); font-size: 13px; max-width: 70ch; }
.lib-status { font-size: 12px; color: var(--muted); min-height: 1.5em; }
