/* app-builder.css — the meme builder, the phone rules, sound review, the tag brush and the hotkey legend.

   ⚖ ONE STYLESHEET, TEN PARTS, AND THE ORDER IS THE CASCADE. This was web/app.css, a single
   5,357-line file, until the 2026-08-05 split (it held lines 3768-4089). The parts must be
   linked in this order on every page:
     app.css → app-rank.css → app-curation.css → app-moderation.css → app-profile.css → app-feeds.css → app-tagrows.css → app-skins.css → app-builder.css → app-viewer.css
   That is not tidiness, it is the specificity tie-break: two rules of equal weight are
   resolved by which one comes LAST, and this file's own sections rely on it — "THE LAST WORD
   ON MODIFIER VERBS" (in app-viewer.css) says in its heading that it must be the final say.
   Re-ordering the <link> tags re-orders the cascade and changes what the site looks like,
   silently, on whichever surface the tie mattered. server/tests/cssparts.test.js pins the
   order on all 18 pages.

   ⚠ The xp-*.css files load AFTER every part, exactly as they loaded after the whole of
   app.css before. Do not insert a part below them. */
/* ===== MEME BUILDER =============================================================
   ONE set of markup, TWO real layouts. On a wide screen it is an app: tool rail left,
   canvas centre, docked property panel right — nothing hidden, nothing overlapping the
   artwork. On a phone the same nodes become a bottom tool bar and a property SHEET,
   because a phone cannot show all of it at once and pretending otherwise produces
   controls too small to hit. Both are driven by the same JS; there is no mobile
   codepath to drift.
   =============================================================================== */
.bld{position:fixed;inset:0;display:grid;background:var(--void);color:var(--ink);
  grid-template-columns:auto 1fr auto;grid-template-areas:"rail main panel"}
.bld-main{grid-area:main;display:flex;flex-direction:column;min-width:0;min-height:0}
.bld-top{flex:none;display:flex;align-items:center;gap:10px;padding:8px 12px;
  border-bottom:1px solid var(--hair);background:var(--panel)}
.bld-title{font-size:11px;letter-spacing:calc(.14em * var(--track));text-transform:var(--caps);
  color:var(--dim);margin:0 auto}
.bld-stage{flex:1;min-height:0;display:flex;align-items:center;justify-content:center;
  padding:12px;overflow:hidden;position:relative}

/* ---- tools ---- */
/* ⚖ THE TOOLS TILE IN THE COLUMN TOO (forage-qw2w.13.3) — `gap:0`, and the box eats the gap.
   The phone bar was tiled by forage-s37 (the block at the foot of this file) and the DESKTOP
   rail was left on `gap:4px`, which tools/vh scores against its 8px minimum: 12 HARD TAP-GAP
   rows on the `pointer:fine` cell at 1280x900, tightest 4.0px, over four fixtures (run
   20260905T095903Z-817c5859c90e). The argument is forage-s37's, unchanged — "TAP-GAP accepts
   either answer, `>= 8px` OR the two controls tile, and tiling is the one that costs nothing:
   every point in the column then belongs to exactly one tool, which is a STRONGER promise than
   8px of dead space." ⛔ `gap:8px` is refused for the second reason as well: it spreads seven
   tools by 24px, which is a look nobody asked for.
   ⚠ SO THE BOX ABSORBS THE GAP AND THE DRAWN BUTTON DOES NOT: `padding-block` 9px -> 11px (the
   4px it ate), and the drawn rectangle becomes an `::after` at `inset-block:1px` — the padding
   box is 2px inside the border box, so that lands the paint exactly 2px in from the grown box's
   top, which is where the border box used to start. `inset-inline:-1px` for the same reason the
   masthead's band uses -1px: the pseudo hangs back over the 1px border it replaces, so the
   drawn rectangle is the 58px-wide one it always was. `.bld-ib` has no `overflow`, so nothing
   clips it (the trap `.bld-chip` has).
   ⚠ AND THE RAIL GIVES BACK THE FIRST HALF-GAP (10px of block padding -> 8px), WHICH IS THE
   HALF THAT MAKES IT FREE. Drawn top of tool n was 10 + n*(56.94 + 4); it is now
   8 + n*60.94 + 2 — the same number, measured at 1440x900 before and after.
   ⚠ THE PAINT MOVES WITH IT. `:hover` and `.on` set `background`/`border-color` on the ELEMENT,
   which is now 4px taller than the rectangle, so they are restated on the pseudo and the
   element's own ground is turned off. The COLOUR stays on the element — that is the label, and
   a `--void`-on-nothing label is how forage-qw2w.2.4 measured 1.06:1.
   ⚠ `.bld-transport .bld-ib` IS THE SAME CLASS IN A DIFFERENT BAR and is deliberately not
   touched: every rule here is scoped `.bld-rail .bld-ib`. The phone block at the foot of the
   file undoes all of it and re-tiles horizontally on its own terms. */
.bld-rail{grid-area:rail;display:flex;flex-direction:column;gap:0;padding:8px;
  border-right:1px solid var(--hair);background:var(--raised);overflow-y:auto}
.bld-ib{display:flex;flex-direction:column;align-items:center;gap:3px;min-width:58px;
  padding:9px 8px;border:1px solid transparent;background:none;color:var(--dim);
  cursor:pointer;text-decoration:none;border-radius:2px}
.bld-ib:hover{color:var(--ink);background:var(--sunk);border-color:var(--hair)}
.bld-ib.on{color:var(--accent);border-color:var(--accent-deep);background:var(--sunk)}
.bld-ib:disabled{opacity:.3;cursor:default;background:none;border-color:transparent;color:var(--faint)}
.bld-ib-l{font-size:9px;letter-spacing:calc(.08em * var(--track));text-transform:var(--caps)}
.bld-ib.bld-go{color:var(--void);background:var(--good);border-color:var(--good);
  flex-direction:row;gap:7px;padding:8px 14px}
.bld-ib.bld-go .bld-ib-l{font-size:11px;font-weight:700}
.bld-ib.danger{color:var(--bad)}
/* the tiling half of the rail's rule above — the box, and the drawn rectangle it carries. */
.bld-rail .bld-ib{position:relative;isolation:isolate;padding-block:11px;
  background:none;border-color:transparent}
.bld-rail .bld-ib::after{content:"";position:absolute;z-index:-1;inset-block:1px;
  inset-inline:-1px;border:1px solid transparent;border-radius:2px}
.bld-rail .bld-ib:hover::after{background:var(--sunk);border-color:var(--hair)}
.bld-rail .bld-ib.on::after{background:var(--sunk);border-color:var(--accent-deep)}
/* ⚠ THE FOCUS RING STAYS ON THE ELEMENT, i.e. 2px looser than the drawn button, which is what
   the phone bar's tiling already does (it overrides no focus rule either). A ring redrawn on a
   `z-index:-1` pseudo paints BEHIND its own element, and a keyboard ring that is sometimes
   invisible is a worse trade than one that is 2px generous. */
/* ⚖ 36px DRAWN, 44px TARGETED (forage-skz) — the floor and the reasoning are docs/DECISIONS.md
   "The masthead's hit area is 44px and its drawn chrome is not". The control at 36px is
   `.bld-ib.bld-go`, which is not in the rail at all (the RAIL's seven `.bld-ib` measure 52.9px
   and clear the floor already): it lives in `.bld-top` (page-build-boot.js `topbar()`), drawn
   ROW-wise with an 18px icon and 8px of padding, and it is the door onto the output sheet —
   the button that commits the whole edit, and the shortest thing on the screen. The sheet's own
   build button is the same class and is lifted by the same rule.
   ⚖ AND IT TAKES THE OVERHANG, NOT THE BAND (forage-qw2w.2.4). It used to grow the BOX to
   --tap-min, hand the growth back with a negative margin-block, and redraw the 36px green
   rectangle as an `::after` band — which left the button's own `background` transparent. The
   label is `--void`, a colour chosen to read on `--good`, and with nothing painting behind it
   on the ELEMENT the nearest opaque ancestor was `.bld-top`'s `--panel`: the harness measured
   the go button's label at **1.06:1** (rgb(10,11,13) on rgb(16,19,24)) at 390x844 and 768x1024,
   on four fixtures. The screen looked right and the CHAIN was a lie — tools/vh's CONTRAST probe
   composites `background-color` up the ancestors and cannot see a pseudo-element's paint
   (`paintedBackground`, and it says so), which is the same blindness that would hide a skin
   getting this pair wrong for real. So the fill goes back on the element and the FLOOR becomes
   an unpainted halo: `.btn::after` (app.css), the owner's 2026-08-20 ruling — "⛔ CLOSED ROAD:
   min-height:var(--tap-min) on the button ITSELF" — and the shape `.cset-sech` (app-viewer.css)
   takes for the same 34px/44px reason. Measured after: **9.00:1** on the default skin, and
   5.02:1 at its worst across the seven (yotsuba-b); every skin clears 4.5:1.
   ⚠ NOTHING ABOUT THE BOX MOVES, so the drawn rectangle, its 2px radius, the bar, the stage and
   the sheet are the fine-pointer ones TO THE PIXEL — no --band, no negative margin, no
   `isolation`, no `z-index:-1`, and no `box-sizing` on the pseudo. Those were three traps the
   band needed and a halo that paints nothing does not: tools/vh's `pseudoTap()` credits it as
   the target only BECAUSE it is unpainted, and `mobiletaps.test.js` reads the same shape.
   ⚠ THE OVERHANG IS VERTICAL ONLY ((44−36)/2 = 4px each way) — `.btn`'s own reasoning: the go
   button has `.bld-chip` beside it, and a horizontal halo would steal that tap. 4px reaches
   into `.bld-top`'s 8px padding, which holds no control.
   ⚠ `position:relative` is what anchors it; an absolutely positioned `::after` with no
   positioned ancestor anchors to the PAGE.
   ⚠ THE DISABLED LOOK IS UNCHANGED ON PURPOSE: `.bld-ib:disabled` is declared ABOVE `.bld-go`,
   so a building button stays green at .3 opacity. Do not add a `:disabled::after` rule "for
   symmetry" — the halo is invisible, and there would be nothing for it to dim.
   ⚖ AND THE HALO IS NO LONGER SCOPED TO A COARSE POINTER (forage-qw2w.13.1). It was, because
   the FLOOR is a touch rule; but what the media query bought was nothing — the halo paints no
   pixel at either pointer — while what it cost was a 36px target under the mouse and a HARD
   TAP-SIZE row on tools/vh's `pointer:fine` cell at 1280x900 (run 20260905T095903Z, four
   fixtures; both coarse cells already read `offenders:0`). A bigger door on the button that
   commits the whole edit is not worse with a mouse. ⛔ This is NOT licence to unwrap the other
   coarse floors: the ones that grow a BOX (`.bld-chip` below, `.pn-rail` in app-panel.css) move
   drawn pixels and steal their neighbours' taps, and their media queries are load-bearing. */
.bld-ib.bld-go{position:relative}
.bld-ib.bld-go::after{content:"";position:absolute;left:0;right:0;
  top:50%;transform:translateY(-50%);height:var(--tap-min)}

/* ---- property panel ---- */
.bld-panel{grid-area:panel;width:310px;display:flex;flex-direction:column;
  border-left:1px solid var(--hair);background:var(--panel);min-height:0}
.bld-tabs{flex:none;display:flex;border-bottom:1px solid var(--hair)}
.bld-tab{flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;padding:9px 4px;
  border:0;border-bottom:2px solid transparent;background:none;color:var(--faint);cursor:pointer;
  font-size:9px;letter-spacing:calc(.08em * var(--track));text-transform:var(--caps)}
.bld-tab:hover{color:var(--ink)}
.bld-tab.on{color:var(--accent);border-bottom-color:var(--accent)}
.bld-tabbody{flex:1;min-height:0;overflow-y:auto;padding:10px 12px}
.bld-lbl{font-size:9px;letter-spacing:calc(.14em * var(--track));text-transform:var(--caps);
  color:var(--faint);margin:10px 0 5px}
.bld-lbl:first-child{margin-top:0}
.bld-row{display:flex;flex-wrap:wrap;gap:10px 12px;align-items:center;padding:2px 0}
.bld-ctl{display:flex;align-items:center;gap:6px;font-size:9px;color:var(--dim);
  letter-spacing:calc(.08em * var(--track));text-transform:var(--caps)}
.bld-ctl input[type=color]{width:32px;height:26px;padding:0;border:1px solid var(--hair);background:none}
/* THE COLOUR WELL IS A PAIR — the swatch and its eyedropper (forage-t1k.24). One rule, and
   every colour control in the builder has one, because they all come out of colorIn(). */
.bld-colw{display:inline-flex;align-items:center;gap:4px}
.bld-pick{display:flex;align-items:center;justify-content:center;padding:5px;cursor:pointer;
  border:1px solid var(--hair);background:var(--sunk);color:var(--dim)}
.bld-pick:hover{border-color:var(--accent);color:var(--ink)}
.bld-pick.on{border-color:var(--accent);background:var(--accent);color:var(--void)}
/* ARMED. The cursor is the only thing on a desktop that says the next click is a sample and
   not an edit; on a phone the hint under the stage is (there is no cursor to change). */
.bld-stage.picking,.bld-stage.picking canvas,.bld-stage.picking .bld-media{cursor:crosshair}
.bld-range{display:inline-flex;align-items:center;gap:6px}
/* ⚖ EVERY SLIDER IN THE BUILDER IS AMBER, NOT THE BROWSER'S BLUE (forage-t1k.41). This rule
   used to set `width` and nothing else, so band height, the trim sliders, every filter
   adjustment and the output sheet's quality/fps sliders painted in the UA default inside an
   app whose whole control language is one accent on near-black — the same class of defect as
   the coin-vs-score marks: a control that never got dressed sitting beside controls that did.
   ⚠ `accent-color` IS THE WHOLE FIX AND IT IS THE HOUSE TREATMENT, not a shortcut. It themes
   the filled track and the thumb in one declaration and it takes a VAR, so it repaints per
   skin for free — verified red under yotsuba, amber under terminal, rather than assumed.
   `.volbar .volrange` (app-feeds.css) and `.pk-track input` (poker.css) were already written
   this way; this rule is the third, not a new idea. ⛔ Do NOT reach for `appearance:none` +
   hand-drawn `::-webkit-slider-*` / `::-moz-range-*` pseudo-elements here: that road costs two
   vendor sets per state, and it is already taken ONE place for a reason — `.bld-scrub.trimmed`
   below paints the trim window INTO the track, which a colour cannot express.
   ⚠ The clip scrubber is the one range OUTSIDE `.bld-range`, so it takes the same declaration
   on its own rule below rather than being wedged into this selector — it is `flex:1`, and a
   `width` written for a panel control has no business on it. */
.bld-range input[type=range]{width:96px;accent-color:var(--accent)}
.bld-num{min-width:26px;text-align:right;font-variant-numeric:tabular-nums;color:var(--ink)}
.bld-chips{display:flex;flex-wrap:wrap;gap:5px}
.bld-chip{border:1px solid var(--hair-hi);background:var(--sunk);color:var(--dim);
  padding:5px 9px;font-size:10px;letter-spacing:calc(.06em * var(--track));text-transform:var(--caps);
  cursor:pointer;white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis}
.bld-chip:hover{border-color:var(--accent);color:var(--ink)}
.bld-chip.on{color:var(--void);background:var(--accent);border-color:var(--accent)}
/* THE DESTRUCTIVE CHOICE, in the one place the builder offers one: "discard" in the leave
   sheet (forage-t1k.30). Outlined in --bad rather than filled with it — a filled red button
   beside a filled accent one reads as the pair of primaries, and the primary here is
   "keep editing". It fills only under the pointer that is committing to it. */
.bld-chip.danger{border-color:var(--bad);color:var(--bad)}
.bld-chip.danger:hover{border-color:var(--bad);background:var(--bad);color:var(--void)}
/* ⚖ 44px OF CHIP, 28px OF DRAWN CHIP (forage-ir6) — the shape and the reasoning are
   docs/DECISIONS.md "The masthead's hit area is 44px and its drawn chrome is not"; .scorechip
   (app-rank.css), .navburger (app.css) and .cas-door (casino.css) carry the same three lines.
   THIS is the builder's most-used idiom — fonts, alignment, shapes, filter presets, the caption
   band, the trim verbs, and the output sheet's chips inherit it — and at padding:5px 9px around
   10px type it measured 28px, a shade over half the floor, on a mobile-first archive.
   ⚠ THE HIT AREA GREW; THE TYPE DID NOT. The box takes --tap-min, the drawn chip becomes a
   ::after band of the height it always had, and an equal NEGATIVE margin-block hands the growth
   back to the flex row — which is not cosmetic here: the phone panel is a PINNED-HEIGHT sheet
   (owner 2026-08-11, builderdrag.test.js) with ~219px of body, so a row that got 16px taller
   would be paid for out of the one budget the builder cannot borrow against.
   ⚠ THE BAND CARRIES A BACKGROUND, so it needs z-index:-1 (a positioned ::after paints after its
   element's inline content and would otherwise cover the chip's own label) — and `isolation`
   ON THE CHIP, so the negative layer bottoms out here instead of falling through .bld-panel's
   background. .cas-door leans on .cas-mast's z-index for that; a chip has no such ancestor and
   is dropped into four different containers, so it isolates itself.
   ⚠ AND THE GAP IS HALF THE FIX. 5px between two 44px boxes is a mis-tap generator, and stacked
   wrap lines 5px apart would make the boxes OVERLAP by 11px — aiming at a chip and hitting the
   one below it is worse than a small target. The row gap becomes exactly --tap-min − --band so
   the boxes TILE (every point in the row belongs to one chip and only one), and the column gap
   takes the 8px adjacent-control minimum the masthead work set. */
@media(pointer:coarse){
  .bld-chips{gap:calc(var(--tap-min) - 28px) 8px}
  .bld-chip{--band:28px;position:relative;isolation:isolate;
    min-height:var(--tap-min);min-width:var(--tap-min);
    margin-block:calc((var(--band) - var(--tap-min)) / 2);
    border:0;padding-inline:10px}
  .bld-chip,.bld-chip:hover,.bld-chip.on{background:none}
  /* ⚠ THE BAND IS INSET TO 0, NOT TO -1px, AND THAT IS THIS CHIP'S ONE DEVIATION FROM THE
     MASTHEAD SHAPE. A chip carries `overflow:hidden` for its ellipsis, so a band hanging 1px
     past the padding box is CLIPPED — measured, not reasoned: the first draft drew the band's
     top and bottom hairlines and no sides at all, and the chips read as open-ended brackets.
     The masthead's -1px exists to sit back over the 1px border it replaces; here the border
     is dropped and that pixel is handed to `padding-inline` instead, so the drawn rectangle,
     the label's position and the chip's outer width are all exactly what they were.
     ⚠ AND `box-sizing:border-box` IS WHAT MAKES THAT SENTENCE TRUE (forage-s2g). app.css sets
     the border-box default with `*`, and `*` DOES NOT MATCH PSEUDO-ELEMENTS, so `height:28px`
     plus a 1px border drew a **30px** rectangle — 1px proud at the top and 1px at the bottom
     of every chip in the builder, for the eleven hours between forage-ir6 and this line. The
     same 2px was live on `.navburger`, `.scorechip`/`.coinchip` and `.cas-door`; all four are
     fixed in the same pass. Any new band written to this shape needs this declaration. */
  .bld-chip::after{content:"";position:absolute;z-index:-1;left:0;right:0;top:50%;
    height:var(--band);transform:translateY(-50%);box-sizing:border-box;
    background:var(--sunk);border:1px solid var(--hair-hi)}
  .bld-chip:hover::after{border-color:var(--accent)}
  .bld-chip.on::after{background:var(--accent);border-color:var(--accent)}
  /* the ring hugs the DRAWN chip, not the invisible box it now sits in */
  .bld-chip:focus-visible{outline:none}
  .bld-chip:focus-visible::after{outline:1px solid var(--accent);outline-offset:2px}
}
/* ⚖ AND THE TOP BAR'S CHIP TAKES THE OVERHANG UNDER A MOUSE (forage-qw2w.13.2) — 28px drawn,
   44px targeted, the `.bld-ib.bld-go` shape at the top of this file rather than the band above.
   tools/vh read the size readout in `.bld-top` (page-build-boot.js `topbar()`) at **28px** on
   its `pointer:fine` cell at 1280x900, on four fixtures (run 20260905T095903Z-817c5859c90e);
   both coarse cells already read `offenders:0`, because the band block above has them.
   ⚠ IT IS A `pointer:fine` BLOCK, NOT AN UNWRAPPED RULE, AND THAT IS ABOUT `::after`. On a
   coarse pointer that pseudo IS this chip's drawn chrome — background, border, the focus ring —
   and `.bld-top .bld-chip::after` outranks `.bld-chip::after`, so an unscoped halo would erase
   the touch chip's paint. The two blocks are mutually exclusive, so the pseudo is free in here.
   ⚠ AND IT IS SCOPED TO `.bld-top`, WHICH IS THE HALF THAT MATTERS. `.bld-chip` also fills
   `.bld-chips`, a WRAPPING row at a 5px row gap: a 44px halo on a 28px chip hangs 8px past it
   top and bottom, so every wrap line would overlap the one below it and a chip would eat its
   neighbour's tap — the thing forage-ir6 (above) already ruled is worse than a small target.
   The top bar is one centred flex row with 8px of padding and nothing above or below the chip,
   so the same halo there reaches into padding and costs nothing.
   ⚠ NO PAINT, NO `z-index`, NO `isolation` — an unpainted halo needs none of the three, and
   tools/vh's `pseudoTap()` credits it as the target only BECAUSE it paints nothing.
   ⚠ AND `overflow` HAS TO GO, WHICH IS THE ONE THING THAT WOULD HAVE MADE THIS A LIE. A chip
   carries `overflow:hidden` for its ellipsis (the trap that bit the band above), and an
   absolutely positioned pseudo is clipped by its own containing block — so the halo would have
   COMPUTED 44px, which is all `pseudoTap()` reads, while the hit area stayed 28px and the
   report went green on a target that had not moved. The ellipsis is not load-bearing on these
   two: `.bld-top` holds `#zoompill` ("100%  reset") and `#sizepill` ("≈ 56 KB"), both generated
   short strings, both `white-space:nowrap`, and a nowrap flex item's `min-width:auto` is its
   full text width — it never shrinks, so `text-overflow` never fires. The panel's `.bld-chips`,
   which hold names people chose, keep theirs. Measured, not reasoned: the after-run checks
   `elementFromPoint` 8px above the chip's drawn top and gets the chip. */
@media(pointer:fine){
  .bld-top .bld-chip{position:relative;overflow:visible}
  .bld-top .bld-chip::after{content:"";position:absolute;left:0;right:0;
    top:50%;transform:translateY(-50%);height:var(--tap-min)}
}
.bld-hint{font-size:10.5px;color:var(--faint);line-height:1.45;padding:8px 0}

/* ---- layer list ---- */
/* THE ADD PALETTE. A grid of equal targets rather than a list: every tool is one tap,
   nothing is nested, and the same markup gives 4 columns on a phone and 3 in the narrower
   desktop panel without a second layout. */
.bld-palette{display:grid;grid-template-columns:repeat(auto-fill,minmax(72px,1fr));gap:6px}
.bld-pal{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;
  padding:14px 6px;border:1px solid var(--hair);background:var(--sunk);color:var(--dim);
  cursor:pointer;border-radius:2px}
.bld-pal:hover{border-color:var(--accent);color:var(--ink)}
.bld-pal.on{border-color:var(--accent);color:var(--void);background:var(--accent)}
.bld-pal-l{font-size:9px;letter-spacing:calc(.1em * var(--track));text-transform:var(--caps)}
.bld-layers{display:flex;flex-direction:column;gap:3px}
.bld-layer{display:flex;align-items:center;gap:7px;padding:7px 8px;border:1px solid var(--hair);
  background:var(--sunk);cursor:pointer;color:var(--dim)}
.bld-layer:hover{border-color:var(--hair-hi);color:var(--ink)}
.bld-layer.on{border-color:var(--accent);color:var(--ink);background:var(--raised)}
.bld-layer-n{flex:1;min-width:0;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bld-mini{border:0;background:none;color:var(--faint);cursor:pointer;padding:2px;display:flex}
.bld-mini:hover{color:var(--ink)}
.bld-mini.danger:hover{color:var(--bad)}
/* A LIT MINI IS A STATE, NOT A HOVER — the padlock and the pin are toggles, and the only
   thing that says a layer is locked once the row looks otherwise normal. */
.bld-mini.on{color:var(--accent)}
/* ⚖ THE LAYER THUMBNAIL SITS ON A CHECKERBOARD (forage-t1k.20), and that is not decoration:
   a layer is drawn on TRANSPARENT, so a black square on the panel's dark ground and white
   text on a light skin are both invisible against a flat backing. The chequer is the
   universal "nothing behind this" and it makes both readable in every skin, which is why it
   is a literal grey rather than a var. */
.bld-layer-th{flex:none;width:34px;height:34px;display:flex;align-items:center;justify-content:center;
  border:1px solid var(--hair);color:var(--faint);
  background-color:#8a8a8a;
  background-image:linear-gradient(45deg,#6d6d6d 25%,transparent 25%,transparent 75%,#6d6d6d 75%),
    linear-gradient(45deg,#6d6d6d 25%,transparent 25%,transparent 75%,#6d6d6d 75%);
  background-size:8px 8px;background-position:0 0,4px 4px}
.bld-layer-th img{max-width:100%;max-height:100%;display:block}
/* THE GRIP. `touch-action:none` is load-bearing: without it the panel scrolls under the finger
   instead of the row moving, which on a phone is the entire feature failing. */
.bld-grip{cursor:grab;touch-action:none}
.bld-layers.dragging{cursor:grabbing}
.bld-layers.dragging .bld-layer{transition:none}
.bld-layer.drag{border-color:var(--accent);opacity:.85}
.bld-layer.locked .bld-layer-n{color:var(--faint)}
/* ⚖ THE ARRIVAL FLASH (forage-t1k.71) — THE SAME SHAPE AS THE TAG BRUSH'S `brushhit-ok` FURTHER
   DOWN THIS FILE, deliberately and not by accident: a decaying inset ring, .9s, ease-out, once,
   with the reduced-motion opt-out written directly beside it. This archive has one vocabulary
   for "that landed" and a second one would only teach members to read two.
   ⚠ WHY A ROW AND NOT THE PICTURE: a pasted layer sits exactly on top of the one it came from
   and is pixel-identical, so the canvas cannot show that there are now two of it. The list can.
   ⚠ AND UNDER prefers-reduced-motion THE ROW STILL HAS TO SAY IT — the animation is off, the
   ring is not: it holds for the same .9s and then the class is removed by the same timer. */
@keyframes bld-landed{from{box-shadow:inset 0 0 0 3px var(--accent)}to{box-shadow:inset 0 0 0 3px transparent}}
.bld-layer.hit{animation:bld-landed .9s ease-out 1}
@media(prefers-reduced-motion:reduce){
  .bld-layer.hit{animation:none;box-shadow:inset 0 0 0 3px var(--accent)}}

/* ---- transport (clips) ---- */
/* ⚖ IT WRAPS RATHER THAN CRUSHING ITS OWN WORDS (forage-t1k.9). This was a single unwrapped
   row, and it was ALREADY over-full before `still` was added to it: measured in Chromium at
   320px, `loop` and `preview` were flex-shrunk to ~24px boxes while their labels — which
   nothing clips — kept drawing at full width, so the bar read "LOOPPREVIEW" as one word. A
   seventh control turned that into "LOOPPREVIEWSTILL", which is not a tight layout, it is an
   unreadable one. MEASURED, at every width a phone actually reports: seven labelled controls, a
   scrub bar and a two-part clock come to ~477px of content, so ONE ROW WAS NEVER POSSIBLE ON A
   PHONE — not at 390, not at 430, not even before `still` existed. It only looked possible
   because flexbox shrank the boxes and the words kept their own width and drew over each other.
   So: it wraps below ~450px and stays a single row above it (768px measured: one row, every
   label clear). The cost is one extra ~40px line on a clip build on a phone, and it buys back
   three words nobody could read.
   ⛔ Do NOT "fix" this by dropping the labels at a phone breakpoint. `preview` is a WORD before
   it is a button — the whole of forage-t1k.2 is that the icon alone was read as a mute — and
   docs/DECISIONS.md rules a native `title=` invisible on touch, so icon-only here would delete
   the answer rather than abbreviate it.
   ⚠ THE SCRUB TAKES A FLEX BASIS rather than staying `flex:1` on an 80px floor, so it is the
   item that claims the leftover width and the buttons keep their natural size instead of being
   squeezed under theirs. A 100px scrub over a 4-second clip is 25ms per pixel; it is the
   control that most needs the room. */
.bld-transport{flex:none;display:flex;flex-wrap:wrap;align-items:center;gap:10px;padding:7px 12px;
  border-top:1px solid var(--hair);background:var(--panel)}
.bld-transport:empty{display:none}
/* accent-color: the untrimmed scrubber is a plain native range and was blue — see the note on
   `.bld-range input[type=range]`. `.bld-scrub.trimmed` below drops the native track entirely,
   so this is inert there by design. */
.bld-scrub{flex:1 1 160px;min-width:80px;accent-color:var(--accent)}

/* ---- the filter chips, with a picture of what they would do (forage-t1k.21) ----
   ⚖ A GRID OF EQUAL TARGETS, the same shape and for the same reason as .bld-palette above:
   every preset is one tap, nothing is nested, and one markup gives four columns on a phone and
   three in the narrower desktop panel. minmax(62px) is the 46px thumbnail plus its label —
   small enough that all nine plus a member's own looks are on screen without a sideways scroll,
   which is the discoverability defect the picker's chip row already paid for once. */
.bld-fx-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(62px,1fr));gap:6px}
.bld-fx{display:flex;flex-direction:column;align-items:center;gap:4px;padding:5px 3px;
  border:1px solid var(--hair);background:var(--sunk);color:var(--dim);cursor:pointer;
  border-radius:2px;min-width:0}
.bld-fx:hover{border-color:var(--hair-hi);color:var(--ink)}
.bld-fx.on{border-color:var(--accent);color:var(--ink);background:var(--raised)}
.bld-fx-th{width:46px;height:46px;overflow:hidden;display:block;background:var(--void)}
.bld-fx-th img{width:100%;height:100%;object-fit:cover;display:block}
.bld-fx-l{font-size:9px;letter-spacing:calc(.1em * var(--track));text-transform:var(--caps);
  max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

/* ---- the picker ---- */
.bld-modal{position:fixed;inset:0;z-index:var(--z-modal);background:rgba(0,0,0,.82);display:flex;
  flex-direction:column;align-items:center;justify-content:center;gap:10px;padding:14px}
.bld-pickbar{display:flex;gap:8px;align-items:center;width:min(680px,100%)}
.bld-search{flex:1;font:inherit;font-size:13px;background:var(--sunk);color:var(--ink);
  border:1px solid var(--hair-hi);padding:8px 10px}
.bld-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(84px,1fr));gap:6px;
  max-width:min(680px,100%);max-height:62vh;overflow-y:auto}
.bld-grid img{width:100%;aspect-ratio:1;object-fit:cover;cursor:pointer;border:1px solid var(--hair)}
.bld-grid img:hover{border-color:var(--accent)}
/* ---- the picker's chips, tiles and long-press preview (forage-t1k.26 · .4) ---- */
/* The chip rows wrap: on a phone "archive · mine · favourites · recent" is wider than the
   modal, and a chip row that scrolls sideways hides the chip nobody knew was there — which is
   the exact discoverability defect this pass exists to fix. */
.bld-pickchips{display:flex;flex-wrap:wrap;gap:6px;width:min(680px,100%)}
/* The TILE is the grid child now, not the img: multi-select needs somewhere to hang a lit
   state and a gif needs somewhere to say so. `contain:paint` keeps the badge inside it. */
.bld-tile{position:relative;display:block;padding:0;background:none;border:0;line-height:0;
  cursor:pointer;contain:paint}
.bld-tile:hover img{border-color:var(--accent)}
.bld-tile.on img{border-color:var(--accent);outline:2px solid var(--accent);outline-offset:-2px}
.bld-tile-kind{position:absolute;left:3px;bottom:3px;font-size:9px;line-height:1;padding:2px 4px;
  background:rgba(0,0,0,.72);color:var(--ink);letter-spacing:calc(.08em * var(--track))}
.bld-modal .bld-chip[disabled]{opacity:.4;cursor:default}
/* The long-press look. Covers the modal rather than floating beside a tile, because a phone
   has no room beside anything and the gesture is already "hold to look, release to stop". */
.bld-preview{position:absolute;inset:0;z-index:1;display:flex;flex-direction:column;gap:8px;
  align-items:center;justify-content:center;padding:18px;background:rgba(0,0,0,.94);
  pointer-events:none}
/* ⚠ `width`, NOT `max-width`: a reaction sticker is often 96px square, and the whole gesture is
   "hold it to see it BIGGER". A max-width would have left the small ones exactly as small as
   the tile, which is a preview that previews nothing. `max-height` + `contain` still stop a
   tall one running off the screen. */
.bld-preview img{width:min(560px,92%);max-height:70vh;object-fit:contain;
  border:1px solid var(--hair-hi)}
.bld-preview .bld-hint{max-width:min(560px,92%);text-align:center}
/* BUILD PROGRESS. Deliberately blocking: while a build is in flight there is nothing
   useful to do on the canvas, and an editor that still accepts edits would be inviting
   changes that cannot reach the render already running. */
.bld-progress{position:fixed;inset:0;z-index:var(--z-blocking);display:flex;align-items:center;
  justify-content:center;background:rgba(0,0,0,.72);backdrop-filter:blur(2px)}
.bld-progress-card{min-width:min(340px,86vw);padding:20px 22px;background:var(--panel);
  border:1px solid var(--hair-hi);display:flex;flex-direction:column;gap:12px}
.bld-progress-label{font-size:12px;letter-spacing:calc(.12em * var(--track));
  text-transform:var(--caps);color:var(--ink)}
.bld-progress-track{position:relative;height:8px;background:var(--sunk);
  border:1px solid var(--hair);overflow:hidden}
.bld-progress-fill{height:100%;width:0;background:var(--accent);transition:width .18s linear}
/* INDETERMINATE: used when there is no honest number to show. A stripe that travels says
   "working, length unknown"; a percentage that creeps and stalls says something false. */
.bld-progress-track.indet .bld-progress-fill{width:38%;transition:none;
  animation:bld-indet 1.1s ease-in-out infinite}
@keyframes bld-indet{0%{transform:translateX(-105%)}100%{transform:translateX(300%)}}
.bld-progress-sub{font-size:11px;color:var(--faint);font-variant-numeric:tabular-nums}
@media (prefers-reduced-motion: reduce){
  .bld-progress-track.indet .bld-progress-fill{animation-duration:2.4s}
}
/* .notice is the SHARED one (common.js notify()) — the builder's toast was the only
   floating message in the app, so every other surface that needed one either used a
   page-local banner or, on /queue and /collective, said nothing at all. Same rule, so
   the two cannot look like different components. */
.bld-toast,.notice{position:fixed;left:50%;bottom:96px;transform:translateX(-50%);z-index:var(--z-toast);
  background:var(--raised);border:1px solid var(--hair-hi);color:var(--ink);
  padding:9px 15px;font-size:12px;max-width:80vw;text-align:center}
.notice.bad{border-color:var(--bad);color:var(--bad)}

/* ── THE SHEET HANDLE ────────────────────────────────────────────────────────────────────
   A grabber on the sheet's own top edge: full width so it is unmissable, ~22px tall so it
   costs nothing, and no horizontal room at all — the tabs row keeps every pixel it had.
   ⚖ Hidden on desktop, where the panel is a side column and steals no canvas height. */
.bld-handle{display:none;align-items:center;justify-content:center;flex:none;
  width:100%;height:22px;padding:0;border:0;cursor:pointer;background:transparent;
  -webkit-tap-highlight-color:transparent}
.bld-handle span{display:block;width:42px;height:4px;border-radius:2px;background:var(--dim);
  opacity:.55;transition:opacity .15s ease,transform .15s ease}
.bld-handle:hover span,.bld-handle:focus-visible span{opacity:1}
.bld-panel.down .bld-handle span{transform:scaleX(1.5);opacity:.8}
.bld-handle:focus-visible{outline:2px solid var(--accent,#d9a441);outline-offset:-3px}
/* ⚠ 22px is the GRABBER, not the target. On a touch device this is the mode switch between
   editing and moving, so it gets the full 44px floor (mobiletaps.test.js); on a mouse the thin
   bar is enough and costs the canvas less. */
@media (pointer: coarse){
  /* both dimensions, because the census asks for both — the bar is already full-width, so
     min-width states a fact rather than reserving anything. */
  .bld-handle{ min-width:var(--tap-min); min-height:var(--tap-min) }
}
@media (prefers-reduced-motion: reduce){ .bld-handle span{transition:none} }

/* ═══ THE OUTPUT SHEET (forage-t1k.1 · .10 · .17) ═══════════════════════════════════════════
   What kind of file comes out, decided at the moment of building. Four rules and one new
   component; nothing else in the builder changes shape.
   ⚠ IT SITS HERE, DIRECTLY ABOVE THE PHONE BLOCK, BECAUSE ORDER IS THE CASCADE IN THIS FILE.
   `.bld-sheet` below is the DESKTOP shape; the phone shape is an override inside the one
   `@media (max-width: 860px)` this file already owns. Written at the END of the file instead —
   which is where it started — the desktop rule won at 390px on equal specificity and the sheet
   rendered 420px wide inside a 390px viewport: every section label clipped on the left and the
   build button clipped on the right. That was PHOTOGRAPHED, not reasoned about (the ruling in
   CLAUDE.md: a UI claim needs a picture of the real app).
   ⚠ It is BELOW the 44px chip work above it, which landed the same hour from another hand:
   every rule here with a coarse-pointer twin is written against the `::after` band that block
   introduced rather than against the border it removed. */

/* ⚖ A REFUSED COMBINATION IS SHOWN, NOT HIDDEN — AND IT IS STILL TAPPABLE, because a chip that
   does nothing when pressed teaches nothing. The markup carries `aria-disabled` rather than the
   `disabled` ATTRIBUTE for exactly that reason: `disabled` swallows the click, and the click is
   how the panel gets to say WHY.
   ⚠ THE LOOK IS A DASHED HAIRLINE AND --faint, DELIBERATELY NOT OPACITY. --faint is a 4.5:1
   floor value; `.3` opacity over it would put a refused option back under the contrast floor
   the same day that floor was raised. A refused chip must stay READABLE — it is carrying a
   sentence's worth of meaning. */
.bld-chip.off{border-style:dashed;border-color:var(--hair);background:var(--void);
  color:var(--faint);cursor:default}
.bld-chip.off:hover{border-color:var(--hair);color:var(--faint)}
@media(pointer:coarse){
  /* the drawn chip is the ::after band now, so the refusal is drawn on the band */
  .bld-chip.off,.bld-chip.off:hover{background:none}
  .bld-chip.off::after,.bld-chip.off:hover::after{background:var(--void);
    border-style:dashed;border-color:var(--hair)}
  /* a slider is a drag target and takes the same floor the chips do */
  .bld-out input[type=range]{min-height:var(--tap-min)}
}
/* a greyed CONTROL, same argument: visibly off, with its reason beneath it, rather than gone */
.bld-ctl.off{opacity:1;color:var(--faint)}
.bld-ctl.off input{filter:grayscale(1);opacity:.45;pointer-events:none}
/* THE REASON LINE. One line under the format row does double duty — normally the consequence of
   the lit chip, and a tap on a refused one replaces it with the refusal. The left rule is what
   makes it read as an ANSWER to the tap that just landed instead of as ambient help text. */
.bld-hint.why{border-left:2px solid var(--hair-hi);padding-left:9px;color:var(--dim)}
.bld-hint.why.no{border-left-color:var(--bad)}

/* THE SIZE READOUT. Deliberately not a bare number: the estimate, the word "estimate", and the
   source's own size as a mark on the bar all travel together — this repo already ruled once
   (the build progress bar above) that a confident number it cannot stand behind is worse than
   no number at all. */
.bld-size{margin-top:12px;padding:9px 10px;border:1px solid var(--hair-hi);background:var(--sunk)}
.bld-size-r{display:flex;align-items:baseline;gap:8px}
.bld-size-k{font-size:9px;letter-spacing:calc(.14em * var(--track));text-transform:var(--caps);
  color:var(--faint)}
.bld-size-v{margin-left:auto;font-size:16px;color:var(--bright);font-variant-numeric:tabular-nums}
.bld-size-v.heavy{color:var(--accent)}
.bld-size-sub{font-size:10px;color:var(--faint);line-height:1.45;margin-top:5px}
.bld-size-bar{position:relative;height:4px;margin-top:7px;background:var(--void);
  border:1px solid var(--hair)}
.bld-size-fill{position:absolute;inset:0 auto 0 0;background:var(--accent)}
.bld-size-fill.heavy{background:var(--bad)}
.bld-size-src{position:absolute;top:-3px;bottom:-3px;width:1px;background:var(--dim)}

/* ⚖ THE SHEET IS position:fixed IN ITS OWN Z TIER AND NEVER ENTERS THE GRID (owner 2026-08-11,
   builderdrag.test.js). `.bld-panel` is a PINNED 38vh precisely because a content-sized panel
   resized the stage, which moved the canvas under a live finger and posted layers off the
   artboard. A sheet that joined the grid would be that defect a second time; this one takes no
   height from `.bld-main` at all, so the measured canvas movement on open is 0px.
   ⚠ AND ITS OWN HEIGHT IS FIXED FOR THE SAME REASON AT A SMALLER SCALE. Choosing gif reveals two
   more controls; at `height:auto` the sheet would grow upward and the build button would walk
   out from under the thumb that is about to press it. The body scrolls instead.
   ⚠ A CLOSE-COMMENT MARKER ON THE LINE ABOVE KILLED THE SCRIM (found 2026-08-20, drafts work).
   It ended this comment six lines early, so the paragraph below was parsed as CSS — and a CSS
   parser recovering from garbage consumes tokens up to the next brace block, which ate the
   `.bld-scrim` rule whole. The output sheet shipped with no backdrop at all: the page behind it
   undimmed, and the tap-outside-to-close target a 414x0 box with z-index auto and a transparent
   background (measured in Chromium). Nothing could see it — the rule is present and reads
   correctly, the sheet still opens and still closes by its own button, and no harness asks
   whether a backdrop is painted. cssparts.test.js scans for the unbalanced marker now.
   ⚠ Do not re-cut this comment in two, and do not quote that marker inside a comment.
   ⚖ WHICH SHAPE IS THE DEFAULT IS DECIDED BY THE BREAKPOINT LADDER, NOT BY TASTE. The phone
   shape needs a media query and the desktop shape does not, so the DIALOG is written here as
   the plain rule and the SHEET is an override inside the ONE `max-width:860px` block this file
   already owns — see .bld-panel there. Writing it the other way round would have minted a
   fourteenth breakpoint (`min-width:861px`) for a rung that already exists on the other side,
   which mobilebreakpoints.test.js refuses, correctly. */
.bld-scrim{position:fixed;inset:0;z-index:var(--z-sheet);background:rgba(0,0,0,.72)}
/* ON A MOUSE IT IS A CONTENT-SIZED DIALOG. The pinned height below exists so a control cannot
   walk out from under a FINGER between one tap and the next; a pointer re-aims for free, and a
   half-empty 600px sheet for a still+png is just waste. Same split `.bld-handle` already makes
   (a 22px grabber on a mouse, the 44px floor on a thumb). */
.bld-sheet{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);
  z-index:calc(var(--z-sheet) + 1);width:420px;max-height:min(640px,86vh);
  display:flex;flex-direction:column;background:var(--panel);border:1px solid var(--hair-hi)}
.bld-sheet-top{flex:none;display:flex;align-items:center;gap:8px;padding:9px 12px;
  border-bottom:1px solid var(--hair);background:var(--raised)}
.bld-sheet-t{font-size:11px;letter-spacing:calc(.14em * var(--track));text-transform:var(--caps);
  color:var(--ink)}
.bld-sheet-src{font-size:9px;color:var(--faint);letter-spacing:calc(.08em * var(--track));
  text-transform:var(--caps);margin-left:auto;text-align:right}
.bld-sheet-body{flex:1;min-height:0;overflow-y:auto;padding:10px 12px}
.bld-sheet-foot{flex:none;padding:9px 12px;border-top:1px solid var(--hair);background:var(--raised)}
.bld-sheet-acts{display:flex;align-items:center;gap:10px;margin-top:9px}
.bld-sheet-acts .bld-go{margin-left:auto}
/* THE PINNED ESTIMATE: no top margin, no border of its own — it is PART of the footer, one line
   above the button that spends the bytes, and it cannot scroll away from it. */
.bld-size.pin{margin-top:0;border:0;background:none;padding:0}

/* ⚖ THE LEAVE SHEET IS THE OUTPUT SHEET'S COMPONENT, SHORTENED (forage-t1k.30). Cancel with
   unsaved work asks a three-way question — keep editing · save a draft · discard — and this
   builder already owns exactly one surface for "a question asked over the canvas": .bld-sheet,
   a centred dialog on a mouse and a bottom sheet on a phone. A second component would be a
   second thing to keep in sync for no gain.
   ⚠ `.short` IS THE ONE DIFFERENCE, and it is safe here for the reason the pinned height is not
   safe there: .bld-sheet is 70vh on a phone because CHOOSING GIF REVEALS TWO MORE CONTROLS, so
   a content-sized surface would grow upward and walk the build button out from under the thumb
   about to press it. This sheet's three buttons never change, so nothing can move, and 70vh of
   near-empty panel to ask one question is its own kind of wrong.
   ⚠ Two classes, so it beats the phone block's `.bld-sheet{height:70vh}` on specificity
   wherever it sits in the cascade — this is deliberately NOT relying on source order. */
.bld-sheet.short{height:auto;max-height:min(420px,80vh)}
.bld-sheet-acts.bld-leave{flex-wrap:wrap}
/* the three answers keep a full row each on a narrow sheet rather than eliding their labels:
   "save a draft and leave" is the option nobody guesses, so it must be able to say so. */
.bld-sheet-acts.bld-leave .bld-chip{flex:1 1 auto;text-align:center;overflow:visible}

/* ⚖ THE RECOVERY OFFER IS A BAR (forage-t1k.16) — see offerDraft() in page-build-history.js for
   why it is not a modal and not a silent restore. The CSS half of that ruling is that it must
   cost the layout NOTHING: it is absolutely positioned INSIDE the stage, so #rail, #panel and
   #stage are measured exactly as they are without it, and a returning member's canvas is not a
   different size from a fresh one.
   ⚠ IN THE STAGE, NOT IN THE BODY, and that is the whole reason it needs no magic offset: the
   stage already begins immediately under .bld-top on desktop AND on a phone, so "just below the
   top bar" is `top:8px` here and is correct on both layouts and at any top-bar height. It also
   cannot reach the notch or the home indicator, so it asks nothing of the safe-area insets.
   z-index 3 is INTRA-COMPONENT — its only siblings are the clip layer and the canvas container
   (z-index 1) inside this one stacking context — which is exactly the case zindex.test.js
   leaves as a bare number rather than a global token.
   It sits at the TOP because the bottom of this screen is the tool sheet and the tool rail on a
   phone, the two places a thumb rests, and an offer that appears under a resting thumb is an
   offer that gets answered by accident. */
.bld-recover{position:absolute;left:50%;top:8px;transform:translateX(-50%);z-index:3;
  display:flex;align-items:center;flex-wrap:wrap;justify-content:center;gap:8px;
  max-width:calc(100% - 16px);padding:8px 10px;
  background:var(--raised);border:1px solid var(--accent);
  box-shadow:0 6px 22px rgba(0,0,0,.5)}
.bld-recover-t{font-size:10.5px;color:var(--ink);letter-spacing:calc(.06em * var(--track));
  text-transform:var(--caps)}

/* ================= PHONE ================= */
/* the rail becomes a bottom bar and the panel a sheet — the canvas keeps the rest. */
@media (max-width: 860px) {
  .bld{grid-template-columns:1fr;grid-template-rows:1fr auto auto;
       grid-template-areas:"main" "panel" "rail"}
  .bld-rail{flex-direction:row;overflow-x:auto;border-right:0;border-top:1px solid var(--hair);
    padding:6px 8px;gap:2px}
  .bld-ib{min-width:52px;padding:7px 6px}
  /* ⚑ AND THE DESKTOP COLUMN'S TILING IS UNDONE HERE, WHOLE (forage-qw2w.13.3). This bar is a
     ROW: it tiles horizontally, on its own terms, in the coarse block below, and the column's
     answer — a 4px-taller box with the paint on a pseudo — is wrong in both directions here.
     ⚠ IT HAS TO BE RESTATED, NOT INHERITED. `.bld-rail .bld-ib` outranks the `.bld-ib` rule
     above it AND `.bld-ib:hover` (equal specificity, later in the file), so a narrow window on
     a MOUSE — a desktop browser dragged under 860px, not a rare shape — would otherwise keep
     the taller box and lose its hover ground to a pseudo that this block switches off. */
  .bld-rail .bld-ib{padding-block:7px}
  /* the paint the column handed to a pseudo comes back to the element — but ONLY on a fine
     pointer, because the coarse block below wants it on the pseudo for its own tiling, and
     `.bld-rail .bld-ib:hover` would outrank the `background:none` it sets there. */
  @media(pointer:fine){
    .bld-rail .bld-ib:hover{background:var(--sunk);border-color:var(--hair)}
    .bld-rail .bld-ib.on{background:var(--sunk);border-color:var(--accent-deep)}
    .bld-rail .bld-ib::after{content:none}
  }
  /* ⚖ THE TOOLS TILE ON A THUMB (forage-s37). Measured on a real touch Chromium at 390x844:
     seven 52x52.9 targets with SIX 2.0px gaps. Each button clears the 44px floor on its own,
     so nothing here needs to get bigger — what is wrong is the 2px of no-man's-land between
     them, which is the mis-tap generator `.bld-chip` above was fixed for and which the TAP-GAP
     probe scores against an 8px minimum.
     ⚖ TILING, NOT 8px, AND THE RAIL IS WHY. This bar is `overflow-x:auto` and already runs 2px
     wider than a 390px viewport; six 8px gaps would push it to ~428 and hide `redo` behind a
     swipe on the builder's primary control surface. TAP-GAP accepts either answer — ">= 8px, or
     the two controls tile" — and tiling is the one that costs nothing: every point in the row
     then belongs to exactly one tool, which is a STRONGER promise than 8px of dead space.
     ⚠ SO THE BOX ABSORBS THE GAP AND THE DRAWN BUTTON DOES NOT: gap 0, +2px of box (the gap it
     ate), and the drawn rectangle becomes an ::after at the PADDING box — which, at 1px of
     border, is exactly the 52x52.9 border box it was. `padding-inline` is bumped by the same
     1px a side so a tool whose LABEL outgrows `min-width` tiles on the same terms rather than
     quietly losing 2px of paint.
     ⚠ AND THE RAIL GIVES BACK THE FIRST HALF-GAP (8px of padding -> 7px), WHICH IS THE HALF THAT
     MAKES THIS FREE. Without it every box starts 1px right of where its button was drawn and the
     whole row — bands, icons and labels — walks 1px sideways. With it, each drawn rectangle and
     each icon land on the pixel they were already on, the bar measures the same 392px it always
     did, and the change costs the horizontal scroll nothing at all.
     ⚠ NESTED INSIDE THIS BLOCK ON PURPOSE, not written as `(max-width:860px) and
     (pointer:coarse)`: that would be a SECOND use of 860px and mobilebreakpoints.test.js
     ratchets the count for every stray width. Same argument as the output sheet's, above.
     ⚠ Only the RAIL. `.bld-transport .bld-ib` is the same class in a different bar with its own
     padding, and `.bld-ib.bld-go` is lifted by its own rule at the top of this file. */
  @media(pointer:coarse){
    .bld-rail{gap:0;padding-inline:7px}
    .bld-rail .bld-ib{position:relative;isolation:isolate;min-width:54px;padding-inline:7px;
      background:none;border-color:transparent}
    .bld-rail .bld-ib::after{content:"";position:absolute;z-index:-1;inset-block:-1px;
      inset-inline:0;border:1px solid transparent;border-radius:2px}
    .bld-rail .bld-ib:hover::after{background:var(--sunk);border-color:var(--hair)}
    .bld-rail .bld-ib.on::after{background:var(--sunk);border-color:var(--accent-deep)}
  }
  /* ⚖ A PINNED HEIGHT, NOT A CONTENT-SIZED ONE (owner 2026-08-11). `max-height:40vh` let the
     sheet grow and shrink with whatever the active tab happened to hold, so selecting a layer or
     switching tabs resized it — which moved the stage, which moved the canvas, which is how a
     drag in progress threw layers off the artboard. A fixed height means the tab body scrolls
     inside a sheet that never moves. The one thing that changes it is #handle. */
  .bld-panel{width:auto;height:38vh;border-left:0;border-top:1px solid var(--hair)}
  .bld-panel.down{height:auto}
  .bld-panel.down .bld-tabs,.bld-panel.down .bld-tabbody{display:none}
  .bld-handle{display:flex}
  .bld-tabbody{padding:9px 11px}
  .bld-range input[type=range]{width:84px}
  .bld-top{padding:7px 10px}
  /* finger-sized targets for the small controls */
  .bld-mini{padding:6px}
  .bld-layer{padding:9px 8px}
  /* ⚖ THE OUTPUT SHEET, AS A SHEET (forage-t1k.1/.10/.17 — the component and the rest of its
     reasoning are immediately above this block). Bottom-anchored and FIXED at 70vh for
     the same reason .bld-panel above it is pinned: choosing gif reveals two more controls, and
     a content-sized surface would grow upward and walk the build button out from under the
     thumb about to press it. The body scrolls inside it instead.
     ⚠ It is `position:fixed` in its own z tier and is NOT in the grid, so it takes no height
     from .bld-main and the canvas cannot move when it opens. Measured at 0px. */
  .bld-sheet{left:0;right:auto;top:auto;bottom:0;transform:none;
    width:100%;height:70vh;max-height:none;
    border:0;border-top:1px solid var(--hair-hi)}
}
/* the clip PLAYS behind a transparent canvas — the browser decodes it in its own
   layer, so the editor never repaints a video frame (see build-fabric.html). */
.bld-media{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;
  pointer-events:none}
.bld-media video,.bld-media img{max-width:100%;max-height:100%;object-fit:fill}
.bld-stage{position:relative;touch-action:none}   /* the stage owns pinch/pan, not the browser */
.bld-stage canvas{position:relative;z-index:1}
.bld-media,.bld-stage .canvas-container{transform-origin:center center;will-change:transform}
#zoompill[hidden]{display:none}
.bld-transport{flex:none;display:flex;align-items:center;gap:6px;padding:6px 10px;
  border-top:1px solid var(--hair);background:var(--panel);overflow-x:auto}
.bld-transport:empty{display:none}
.bld-transport .bld-ib{min-width:0;padding:6px 8px}
.bld-transport .bld-ib-l{font-size:8px}
.bld-scrub{flex:1;min-width:100px}
/* THE TRIM WINDOW, drawn on the scrub bar. Two numbers in a side panel cannot be judged
   against the clip you are watching; a shaded band can. The kept region is bright and
   everything cut away is dimmed — the way a cut reads in any editor.
   It must be painted on the TRACK pseudo-element: a background on the input itself is
   drawn over by the native track, so the first version of this showed nothing at all. */
/* ...AND THE CUT MIDDLE IS A HOLE IN THAT BAND (forage-t1k.7). Four stops instead of two: kept,
   cut, kept, and the two ends. --cut-a/--cut-b are both set to the OUT-POINT when nothing is
   cut, which collapses the two middle stops onto each other — a zero-width band is no band, so
   the gradient is pixel-identical to the two-stop one for every trim that has no hole in it. */
.bld-scrub.trimmed{--trim-bg:linear-gradient(to right,
  var(--sunk) 0, var(--sunk) var(--trim-a), var(--accent) var(--trim-a),
  var(--accent) var(--cut-a), var(--sunk) var(--cut-a),
  var(--sunk) var(--cut-b), var(--accent) var(--cut-b),
  var(--accent) var(--trim-b), var(--sunk) var(--trim-b), var(--sunk) 100%)}
.bld-scrub.trimmed{appearance:none;-webkit-appearance:none;background:none;height:18px}
.bld-scrub.trimmed::-moz-range-track{background:var(--trim-bg);height:6px;border-radius:3px}
.bld-scrub.trimmed::-webkit-slider-runnable-track{background:var(--trim-bg);height:6px;border-radius:3px}
.bld-scrub.trimmed::-moz-range-progress{background:transparent}
/* appearance:none removes the native thumb along with the native track, so it has to be
   drawn back — and it is the playhead, so it wants to be findable against both. */
.bld-scrub.trimmed::-moz-range-thumb{width:14px;height:14px;border-radius:50%;border:2px solid #000;
  background:var(--ink)}
.bld-scrub.trimmed::-webkit-slider-thumb{appearance:none;-webkit-appearance:none;margin-top:-4px;
  width:14px;height:14px;border-radius:50%;border:2px solid #000;background:var(--ink)}
/* THE PAD. For a STILL the exposed area is just the canvas background colour, set in
   relayout(). A clip's canvas is transparent — the footage plays behind it — so the pad is
   painted here, UNDER the canvas, in the space the video no longer occupies.
   ⚖ ONE FILL BEHIND THE WHOLE CANVAS, NOT ONE STRIP PER EDGE (forage-t1k.45). This was two
   pseudo-elements, a top strip and a bottom one, and `.canvas-container` has exactly two —
   so `pad.left`/`pad.right` would have needed a third and a fourth. Instead the container is
   filled edge to edge and the MEDIABOX'S FOUR MARGINS (fit(), page-build.js) are what expose
   it: margin + box + margin adds up to the canvas on both axes, so whatever the footage does
   not cover is pad, by construction, on any edge. `::after` is free again.
   ⚑ AND IT IS PAINTED AS **FOUR BORDERS WITH A HOLE IN THE MIDDLE**, NOT AS ONE FILL
   (forage-54g, 2026-08-22). The owner photographed this: *"messing with the band makes the post
   go white."* A band — ANY pad, in fact — turned the whole clip editor white, with only the
   fabric objects left visible on it, and it had been that way on live since forage-t1k.45.
   ⛔ **`z-index:-1` DOES NOT PUT ANYTHING BEHIND `.bld-media`, AND THE PARAGRAPH THAT USED TO
   STAND HERE SAYING IT DID WAS WRONG.** It said: *".bld-media is an absolutely positioned
   sibling painted in step 8; a negative-z child of the canvas container paints in step 3, i.e.
   BEHIND the video."* That is only true while `.canvas-container` is NOT a stacking context —
   and the line directly above gives it `will-change:transform`, which makes one unconditionally,
   and the pinch-zoom then puts a real `transform` on it as well. A negative z-index child cannot
   escape its own stacking context, so the fill paints INSIDE the container, and the container is
   a LATER sibling than `.bld-media` in build.html's own markup. It was always on top of the
   footage. Measured, in the real app, in Chromium: 0% of the picture area was the clip and 100%
   of it was the band colour.
   ⚠ THE OLD SHAPE HID THAT FOR SIX WEEKS BECAUSE IT ONLY EVER COVERED THE PAD. Two strips, one
   the height of the top band and one the height of the bottom, paint over the region the footage
   is not in — the mediabox's `overflow:hidden` and its four margins see to that — so being on
   the wrong side of the video cost exactly nothing. `inset:0` is the same mistake made visible.
   ⚖ SO THE HOLE COMES BACK AND THE ONE PSEUDO-ELEMENT STAYS. `border-width` takes FOUR values,
   so a single `::before` with a transparent middle paints all four pads — t1k.45's ruling
   ("`.canvas-container` has exactly two pseudo-elements and pad.left/right would need a third
   and a fourth") is honoured, `::after` stays free, and the widths are the mediabox's own four
   margins, set from the same numbers in the same pass of fit(). Whatever the footage occupies is
   never painted, by construction, on any edge.
   ⚠ `z-index:-1` STAYS — not to get behind the video, which it cannot do, but to stay behind the
   CANVAS, so a caption dragged into the band is still drawn on top of it. */
.bld-stage.has-band.is-clip .canvas-container::before{content:"";position:absolute;
  inset:0;box-sizing:border-box;background:none;border-style:solid;
  border-color:var(--band-color,#fff);
  border-width:var(--band-h,0) var(--pad-r,0) var(--band-b,0) var(--pad-l,0);
  z-index:-1;pointer-events:none}
.bld-stage .canvas-container{position:relative}
/* ══ THE SPLIT VERB (forage-t1k.55) ═════════════════════════════════════════════════════════
   ⚖ AN EMPTY PANEL IS DRAWN, NOT HIDDEN. A grid you cannot see is a grid you cannot fill on a
   phone, and this archive is mobile-first. Each free cell is a dashed rectangle over a hatch
   with a 44px tap target in it saying what it wants.
   ⚠ IT IS A DOM OVERLAY, NOT A FABRIC OBJECT. A fabric rect would be in toRecipe(), in the
   layers list, in the undo stack and in the finished build. The host is a child of
   .canvas-container and every cell is positioned in PERCENTAGES, so the whole thing rides the
   view transform through a pinch-zoom for free — the crop rectangle's own trick. */
.bld-splitui{position:absolute;inset:0;z-index:4;pointer-events:none}
.bld-cell{position:absolute;pointer-events:auto;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:5px;
  min-width:var(--tap-min);min-height:var(--tap-min);
  border:1px dashed var(--hair-hi);color:var(--faint);cursor:pointer;
  font-size:9px;letter-spacing:calc(.1em * var(--track));text-transform:var(--caps);
  background:repeating-linear-gradient(45deg,rgba(14,17,22,.72) 0 8px,rgba(12,15,20,.72) 8px 16px)}
.bld-cell:hover{border-color:var(--accent);color:var(--ink)}
.bld-cell b{font-size:22px;font-weight:400;line-height:1;color:var(--dim)}
/* the little grid diagram on a split chip — at 9px the words "2-up" do not say which axis, so
   the picture is the label and the word is the confirmation */
.bld-diag{display:grid;gap:2px;width:15px;height:15px;background:var(--hair-hi);flex:none;
  vertical-align:-3px;margin-right:5px}
.bld-diag i{background:var(--dim);display:block}
.bld-chip.on .bld-diag{background:var(--void)}
.bld-chip.on .bld-diag i{background:var(--accent)}
.bld-splitchip{display:inline-flex;align-items:center}
/* the clip is offset inside this box by the crop, so only the kept region is visible */
.bld-mediabox{overflow:hidden;flex:none}
.bld-mediabox video,.bld-mediabox img{display:block;max-width:none;max-height:none}

/* ===== SOUND REVIEW — the audio qualifying screen (owner request 2026-07-27g) =====
   One row per mined candidate: the reuse count first (it is the best signal of whether a
   thing is worth naming), then the audio itself, then name-or-bin. Deliberately plain — this
   is a chore surface, and the job is to get through a list quickly. */
.sr-row{border:1px solid var(--hair);border-radius:4px;padding:10px 12px;margin-bottom:9px;
  background:var(--panel)}
.sr-head{display:flex;align-items:baseline;gap:6px;margin-bottom:2px}
.sr-head b{color:var(--bright);font-size:13px}
.sr-audio{display:block;width:100%;max-width:420px;margin:8px 0 6px;height:32px}
.sr-acts{display:flex;flex-wrap:wrap;align-items:center;gap:7px;margin-top:4px}
.sr-slug,.sr-label{width:auto;flex:0 1 170px;padding:5px 8px;font-size:12px}
.sr-acts .bv-act{padding:6px 11px}
@media(max-width:620px){
  /* phone: the archive is mobile-first, and this list is long — stack the controls so the
     accept button never ends up off-screen beside two inputs. */
  .sr-slug,.sr-label{flex:1 1 100%}
  .sr-acts .bv-act{flex:1 1 45%}
}

/* invite LINK cell (owner request 2026-07-27i) — a readonly input rather than plain text so
   it is selectable and tap-to-select-all, which is the only path that works when
   navigator.clipboard is missing (any plain-http origin, i.e. the whole LAN). */
.inv-link{width:min(340px,52vw);font-family:var(--mono,ui-monospace,monospace);font-size:11px;
  padding:4px 6px;background:var(--sunk);border:1px solid var(--hair);color:var(--dim)}
.inv-link:focus{color:var(--ink);border-color:var(--accent)}
/* the invite DESTINATION picker, on its own row above the link column (2026-08-30). CAPPED:
   `.adminput` is `flex:1`, and a two-entry dropdown stretched across 1,100px reads as a form
   field somebody is required to fill in — which is exactly what it is not. Leaving it alone is
   the default and mints the plain link. */
/* ⚠ TWO CLASSES, NOT ONE. `.adminput` lives in app-viewer.css, which cssparts.test.js pins as
   the LAST part in the cascade — a single-class rule here loses the tie to it by document
   order, silently, and the picker goes back to filling the bar. */
.admin-acts .inv-dest{flex:0 0 auto;width:min(320px,60vw)}
td .inv-link + .btn{margin-left:6px;vertical-align:middle}
/* HOW MANY KEYS (2026-08-31). ⚠ NARROW ON PURPOSE: `.adminput` is `flex:1` and a number box
   that fills the bar reads as the primary control on a row whose primary control is the button
   next to it. Two digits is the whole range. */
.admin-acts .inv-qty{flex:0 0 auto;width:64px;text-align:center}
/* ⚑ THE DESTINATION, SAID OUT LOUD — the summary line under the picker and the one on every
   link row. ⛔ THE DEFECT IT CLOSES IS THAT THE TWO STATES LOOKED IDENTICAL: the copy box cuts
   off at `?next=`, exactly where the destination begins, so a link carrying a club and a plain
   link were the same pixels. The row line is the only thing on this screen that names it, so it
   is COLOURED when there is one and stays visible (never `display:none`) when there is not —
   "no line" and "a line I did not read" look the same at arm's length. */
.inv-dest-row{margin-top:3px;font-size:10px;line-height:1.3;overflow-wrap:anywhere}
.inv-dest-row.on{color:var(--accent)}
.inv-dest-arrow{opacity:.7}
.hint.inv-dest-on{color:var(--accent)}
/* ⚠ PHONE_BP (640), NOT THE 620 THIS FILE ALREADY CARRIES. 620 is a WAIVED stray with n:1 in
   server/tests/mobilebreakpoints.test.js — a ratchet that may fall and never rise — and the
   meme builder's toolbar is the one block allowed to use it. A new rule belongs on the rung
   that already means "phone" (scars.md §5, "inventing a breakpoint"). */
@media(max-width:640px){
  /* ⛔ THE BOX GETS A VIEWPORT SHARE, NEVER `width:100%` — MEASURED, 2026-08-31. `100%` inside a
     six-column table cell resolved to ~110px on a 390px phone and the field showed `https:`,
     which is WORSE than the ~203px it had. A percentage of a cell whose width the table is
     choosing is not a width, it is a request. 64vw ≈ 250px at 390: it buys back what the
     stacked button frees and no more, and the row's own `→ <club>` line below is what actually
     names the destination — a URL tail is always longer than a phone. */
  .inv-link{width:min(340px,64vw)}
  /* the button drops under the box rather than the two splitting one cramped cell */
  td .inv-link + .btn{margin-left:0;margin-top:5px}
}

/* one-time password reveal (owner request 2026-07-28). Loud on purpose: this string is
   visible for a few seconds and then gone forever, so it must not read as ordinary chrome. */
.prof-temp-pw{margin-top:10px;padding:10px 12px;border:1px solid var(--accent);
  border-radius:4px;background:var(--sunk)}
.prof-temp-pw-h{font-size:10px;letter-spacing:calc(.14em * var(--track));text-transform:var(--caps);
  color:var(--accent);margin-bottom:7px}
.prof-temp-pw-row{display:flex;flex-wrap:wrap;gap:7px;align-items:center}
.prof-temp-pw-val{flex:1 1 200px;width:auto;font-family:ui-monospace,Menlo,monospace;
  font-size:14px;letter-spacing:.04em;color:var(--bright);background:var(--void);
  border:1px solid var(--hair-hi);padding:6px 8px}
.prof-temp-pw .hint{margin-top:6px}

/* =====================================================================
   TAG BRUSH v1 (owner ruling 2026-07-28) — the shift/ctrl bulk-curation gesture on the
   search grid. Everything here is driven by ONE body class the JS toggles, so arming the
   mode costs a class flip and no per-tile work: 28 cards light up in a single paint.
   The AFFORDANCE IS THE POINT. A modifier gesture that mutates the archive must announce
   itself before the click, and must announce WHICH of the two it is going to be — the
   quiet badge on every brushable card says "the mode is armed", the loud one under the
   cursor says "this card, this action".
   ===================================================================== */
body.brush-add .tile[data-brush], body.brush-del .tile[data-brush],
body.brush-add .hovercard[data-brush], body.brush-del .hovercard[data-brush]{cursor:crosshair}
/* the mark itself — a centred glyph over the card. ::after on the anchor, so it costs no
   DOM and cannot be clicked (the anchor takes the click; see TagBrush's capture handler). */
body.brush-add .tile[data-brush]::after, body.brush-del .tile[data-brush]::after,
body.brush-add .hovercard[data-brush]::after, body.brush-del .hovercard[data-brush]::after{
  content:"";position:absolute;inset:0;pointer-events:none;z-index:5;
  display:flex;align-items:center;justify-content:center;
  font:600 34px/1 var(--ui);letter-spacing:0;
  /* A DOUBLE shadow, not a bigger one: the tight ring gives the glyph an edge against a
     bright meme, the wide one lifts it off a busy mid-tone. A single soft shadow reads as
     nothing over white — and half this archive is white-background screenshots. */
  text-shadow:0 0 3px rgba(0,0,0,.95),0 2px 12px rgba(0,0,0,.8);
  opacity:.5;transition:opacity .12s linear,background-color .12s linear}
body.brush-add .tile[data-brush]::after, body.brush-add .hovercard[data-brush]::after{
  content:"＋";color:var(--good);box-shadow:inset 0 0 0 2px var(--good)}
body.brush-del .tile[data-brush]::after, body.brush-del .hovercard[data-brush]::after{
  content:"✕";color:var(--bad);box-shadow:inset 0 0 0 2px var(--bad)}
/* the card actually under the cursor: full strength + a wash, so the target is never
   ambiguous in a dense grid. */
body.brush-add .tile[data-brush]:hover::after, body.brush-add .hovercard[data-brush]:hover::after{
  opacity:1;background:rgba(95,196,107,.22)}
body.brush-del .tile[data-brush]:hover::after, body.brush-del .hovercard[data-brush]:hover::after{
  opacity:1;background:rgba(224,85,95,.22)}
/* in flight — the glyph holds, the card dims. Never disabled-looking: the click landed. */
.tile.brush-busy, .hovercard.brush-busy{opacity:.55}
/* the landed flash. Colour says which direction; it decays on its own. */
@keyframes brushhit-ok{from{box-shadow:inset 0 0 0 3px var(--good)}to{box-shadow:inset 0 0 0 3px transparent}}
@keyframes brushhit-no{from{box-shadow:inset 0 0 0 3px var(--bad)}to{box-shadow:inset 0 0 0 3px transparent}}
.tile.brush-hit-add,.hovercard.brush-hit-add{animation:brushhit-ok .9s ease-out 1}
.tile.brush-hit-del,.hovercard.brush-hit-del{animation:brushhit-no .9s ease-out 1}
@media(prefers-reduced-motion:reduce){.tile.brush-hit-add,.hovercard.brush-hit-add,
  .tile.brush-hit-del,.hovercard.brush-hit-del{animation:none}}
/* the result chip — what the server actually did, in its own words ("2 tagged",
   "1 downvoted", "nothing to remove"). Self-removes after ~2s. */
.brushchip{position:absolute;left:6px;bottom:6px;z-index:6;pointer-events:none;
  font-size:9.5px;letter-spacing:calc(.1em * var(--track));text-transform:var(--caps);
  padding:3px 6px;background:var(--void);border:1px solid var(--hair-hi);color:var(--ink);
  max-width:calc(100% - 12px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.brushchip.ok{border-color:var(--good);color:var(--good)}
.brushchip.no{border-color:var(--bad);color:var(--bad)}

/* =====================================================================
   HOTKEY LEGEND (owner request 2026-07-28) — fixed bottom-left, collapsible, desktop only.
   The JS already declines to mount it on a touch pointer; the media queries below are the
   LAYOUT half of that — under the grid breakpoint it would sit on top of the results, and
   a phone must never render it at all even if something else mounts it by mistake.
   ===================================================================== */
.hkbox{position:fixed;left:14px;bottom:14px;z-index:var(--z-bar);width:268px;
  background:var(--panel);border:1px solid var(--hair);
  font-size:10px;line-height:1.5;box-shadow:0 6px 22px rgba(0,0,0,.5)}
.hkbox .hk-head{display:flex;align-items:center;gap:6px;width:100%;padding:6px 8px;
  background:none;border:0;border-bottom:1px solid var(--hair);cursor:pointer;
  color:var(--dim);font:inherit;font-family:var(--ui);
  letter-spacing:calc(.16em * var(--track));text-transform:var(--caps);text-align:left}
.hkbox .hk-head:hover{color:var(--accent)}
.hkbox .hk-tri{display:inline-block;font-size:8px;transform:rotate(90deg);transition:transform .15s ease}
.hkbox.shut .hk-tri{transform:rotate(0deg)}
.hkbox.shut .hk-head{border-bottom:0}
.hkbox.shut .hk-body{display:none}
/* COLLAPSED IS A CHIP, not an empty 268px bar: shrink to the header's own width so the
   dismissed panel takes the least gutter it can. This is what makes it liveable on a
   viewport too narrow for the open panel to clear the grid (see hotkeyLegend's default). */
.hkbox.shut{width:auto}
.hkbox .hk-body{padding:7px 8px;display:flex;flex-direction:column;gap:5px}
.hkbox .hk-row{display:flex;align-items:baseline;gap:6px}
.hkbox .hk-keys{flex:0 0 auto;display:flex;align-items:center;gap:2px;white-space:nowrap}
.hkbox .hk-plus{color:var(--faint);font-size:8px}
.hkbox kbd{font-family:var(--mono);font-size:9px;line-height:1;padding:3px 4px;
  background:var(--sunk);border:1px solid var(--hair-hi);color:var(--ink);
  border-radius:2px;text-transform:none;letter-spacing:0}
.hkbox .hk-row.ok kbd{border-color:var(--good);color:var(--good)}
.hkbox .hk-row.no kbd{border-color:var(--bad);color:var(--bad)}
.hkbox .hk-l{flex:1 1 auto;color:var(--faint)}
@media(prefers-reduced-motion:reduce){.hkbox .hk-tri{transition:none}}
/* WIDTH is the only gate here. The POINTER gate lives in hotkeyLegend(), which refuses to
   build at all unless HOVER_CAPABLE — `(hover:hover) and (pointer:fine)`, which is strictly
   stronger than any `(hover:none),(pointer:coarse)` rule could be, so a second CSS copy of
   it would be dead weight that can only ever disagree with the JS (it did: it hid the panel
   in headless Firefox, where matchMedia is stubbed for the JS but the CSS engine still
   answers coarse). One gate, in the place that can express the condition exactly.
   The width rule is a REAL second condition the JS cannot see: on a narrow desktop window
   the panel would sit on top of the results, and the grid goes narrow well before this. */
@media(max-width:1099px){.hkbox{display:none}}
