/* ════════════════════════════════════════════════════════════════════════════
   PHOTO BOOTH v2 — the arcade skin (2026-07-30)
   ────────────────────────────────────────────────────────────────────────────
   Built from Mr. Avery's mock. Spec: _specs/2026-07-30-photo-booth-v2-design.md

   ⚠ THIS SKIN IS PHOTO BOOTH ONLY. It deliberately does NOT match Spark Lab's
   v6 language (rounded dark pills, Phosphor chrome, at most two filled buttons)
   and it must never leak outward — Art Studio, Spark Lab, Coloring and By
   Number all share canvas.css's :is() scope, which is exactly why this lives in
   its own file and hangs off .cc-b2 rather than [data-app="canvas"].

   His mock runs five filled colour buttons at once. That is the look; the
   two-filled-buttons discipline from the 2026-07-30 pass is lifted HERE and
   nowhere else.

   TYPE: the pixel faces (fonts.css). Minecraft for display, Pixeloid Sans for
   chrome, Pixeloid Mono for counts. ⚠ Never copy a px size across from the old
   Press Start 2P / VT323 rules — the metrics differ hard. See fonts.css.
   ⚠ Weights are real files: 700 exists, 800/900 do not and get faked.
   ════════════════════════════════════════════════════════════════════════════ */

.cc-booth.cc-b2 {
  --b2-navy:   #0d0a2a;
  --b2-panel:  #16123a;
  --b2-line:   #2e2a5e;
  --b2-edge:   #8b3ff5;   /* the magenta/violet panel border in his mock */
  --b2-cy:     #16E0E0;
  --b2-li:     #C6F542;
  --b2-pk:     #FF2E7E;
  --b2-gold:   #FFB800;
  --b2-ink:    #ffffff;
  --b2-dim:    #9a9ac8;
  --b2-pad:    14px;
  --b2-off:    .38;       /* the one disabled opacity — see INTERACTION below */

  /* ⚠ DEFECT-1 FIX (2026-08-01): this used to be `position:absolute; inset:0`.
     None of the ancestors between here and `.win` (`.cc-mode`, `#canvas-root`,
     `.win-body`) declare a `position`, so that inset:0 resolved its containing
     block all the way up to `.win` itself — the ONE positioned ancestor in the
     chain — and painted this whole booth across the ENTIRE window, title bar
     included. Because an absolutely positioned box's clipping follows its
     CONTAINING BLOCK, not its DOM parent, `.win-body`'s `overflow:hidden` never
     touched it either: this box was escaping straight past it. Result: Prof.
     Byte's "?" button and the window's own − ▢ ✕ controls were visually and
     hit-test-wise buried under this element at every window size, including
     maximized. `.cc-studio` and `.cc-play` (Art Studio / Spark Lab) already
     solve exactly this by using `position:relative; height:100%` instead of an
     inset hack — sibling of `.cc-mode`'s flex column, sized to fill it, never
     reaching past `.win-body`'s box. Do the same here. This does NOT change
     where any of this app's own content renders: `.cc-mode`'s box is pixel-
     identical to the area `.win-body` gives it, so the header/main/dock never
     shift — only the accidental bleed above the title bar is gone. */
  position: relative; height: 100%;
  display: flex; flex-direction: column;
  gap: 10px; padding: var(--b2-pad);
  /* his starfield sits back; ours rendered brighter and the halftone argued
     with the frame. A flat dark overlay layer knocks it back without ever
     touching the asset — background-color must live in the LAST layer. */
  background:
    linear-gradient(rgba(4, 2, 14, .45), rgba(4, 2, 14, .45)),
    url(../img/booth/v2/starfield.webp) center / cover no-repeat,
    var(--b2-navy);
  color: var(--b2-ink);
  font-family: 'Pixeloid Sans', ui-monospace, monospace;
  overflow: hidden;             /* guard, not a layout tool */
}

/* ── HEADER ─────────────────────────────────────────────────────────────── */
.cc-b2-head {
  position: relative;
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 14px;
  min-width: 0;
}
.cc-b2-title { height: 60px; width: auto; display: block; image-rendering: pixelated; }
.cc-b2-titletxt {   /* onerror fallback — art missing */
  font-family: 'Minecraft', monospace; font-weight: 700;
  font-size: 33px; margin: 0; letter-spacing: 1px;
}
.cc-b2-exit {
  flex: 0 0 auto;
  width: 34px; height: 34px; display: grid; place-items: center;
  background: #241f4d; color: var(--b2-ink);
  border: 2px solid var(--b2-line); border-radius: 9px; cursor: pointer;
}
.cc-b2-exit:hover { background: var(--b2-pk); border-color: var(--b2-pk); }
.cc-b2-exit svg { width: 17px; height: 17px; fill: currentColor; }

/* CAMERA │ IMPORT — one segmented control in a trough, not two buttons. */
.cc-b2-srcwrap {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 4px;
  margin: 0 auto;
  padding: 5px; border-radius: 12px;
  background: #120f30; border: 2px solid var(--b2-line);
}
.cc-b2-src {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 9px 18px; border: 2px solid transparent; border-radius: 9px;
  background: transparent; color: var(--b2-dim); cursor: pointer;
  font-family: inherit; font-weight: 700; font-size: 14px; letter-spacing: .5px;
}
.cc-b2-src:hover { color: var(--b2-ink); }
.cc-b2-src.cc-on {
  background: var(--b2-cy); color: #06121a; border-color: #0b6f7a;
  box-shadow: 0 3px 0 #0b6f7a;
}
.cc-b2-src svg { width: 16px; height: 16px; fill: currentColor; }

.cc-b2-headr { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; margin-left: auto; }
.cc-b2-help {
  width: 30px; height: 30px; border-radius: 50%; cursor: pointer;
  background: #241f4d; color: var(--b2-gold);
  border: 2px solid var(--b2-gold);
  font-family: inherit; font-weight: 700; font-size: 14px;
}
.cc-b2-help:hover { background: var(--b2-gold); color: #2a1c00; }
.cc-b2-safe {
  display: inline-flex; align-items: center; gap: 8px;
  padding: 9px 14px; border-radius: 11px;
  background: #1b1240; border: 2px solid var(--b2-pk);
  color: #ffd9e8; font-weight: 700; font-size: 12px; letter-spacing: .6px;
}

/* help card — booth-only, anchored under the header */
.cc-b2-helpcard {
  position: absolute; right: 0; top: calc(100% + 8px); z-index: 40;
  width: min(420px, 86vw); padding: 16px 18px;
  background: var(--b2-panel); border: 2px solid var(--b2-edge); border-radius: 14px;
  box-shadow: 0 14px 40px rgba(0,0,0,.55);
}
.cc-b2-helpcard h3 { margin: 0 0 10px; font-size: 14px; color: var(--b2-cy); letter-spacing: 1px; }
.cc-b2-helpcard ol { margin: 0 0 10px; padding-left: 20px; }
.cc-b2-helpcard li { margin-bottom: 7px; font-size: 12px; line-height: 1.6; color: #d9d9f2; }
.cc-b2-helpcard b { color: var(--b2-li); }
.cc-b2-help-safe { margin: 0 0 12px; font-size: 11px; color: var(--b2-dim); }

/* ── MAIN ───────────────────────────────────────────────────────────────── */
/* ⚠ min-width:0 AND min-height:0 are load-bearing on this whole chain. A flex
   item's automatic minimum size is its CONTENT size, so without them the filter
   rail's min-content width (all 35 tiles end to end = 2602px) inflates the app
   past its own window instead of scrolling — measured, twice. */
.cc-b2-main {
  flex: 1 1 auto;
  display: flex; gap: 12px;
  min-width: 0; min-height: 0;
}
.cc-b2-stage {
  position: relative;
  flex: 1 1 auto;
  min-width: 0; min-height: 0;
  display: flex; align-items: center; justify-content: center;
}

/* ── DECORATIVE PROPS ───────────────────────────────────────────────────── */
/* ⚠ EVERY NUMBER BELOW IS MEASURED OFF HIS SAMPLE, not chosen. They live in
   `_tools/booth-v2/compare.py`'s prop rows; re-run it rather than nudging.
   They are % of THE FRAME's box (these elements are children of .cc-b2-frame),
   because that is what his composition anchors them to — see the markup note
   in booth.js.

   ⚠ AND THEY ARE ROTATED. v2 planted all three upright because build.py
   de-rotates each prop when it cuts it out of his source art, and nothing in
   the pipeline ever recorded the angle it sat at in his composition. A
   measurement of x/y/w/h literally cannot see that error, which is why it
   survived a pass that hit every other box to within 0.6pp.

   ⚠ `left`/`top` place the UNROTATED box; `rotate()` then turns it about its
   own centre. So these are centre-derived — do not "fix" one without the other.

   z-index 2 puts them IN FRONT of the frame: his film can rests over the
   frame's lower-right corner rather than being clipped by it. All are
   pointer-events:none, so nothing is made unclickable. */
.cc-b2-prop {
  position: absolute; z-index: 2;
  pointer-events: none; user-select: none;
  image-rendering: pixelated;
  filter: drop-shadow(0 6px 14px rgba(0,0,0,.5));
}
/* right edge meets the frame's left edge, tilted +8° — his */
.cc-b2-prop-cam { width: 22.91%; left: -24.14%; top: 76.51%; transform: rotate(8deg); }
/* straddles the frame's right edge near the bottom, tilted +13.5° — his */
.cc-b2-prop-can { width: 12.26%; left: 91.97%; top: 72.63%; transform: rotate(13.5deg); }
.cc-b2-noprops .cc-b2-prop { display: none; }
/* The tray brings its own ADD / ⬇ / 💾 / ✕ row, which hangs below the strip and
   lands square on the camera's shoulder — measured 44px of overlap. His
   composition never shows both: the strip occupies that pocket alone. The
   camera is decoration and the tray is the thing being used, so the camera
   stands down for as long as the strip is open. */
.cc-b2-strip-on .cc-b2-prop-cam { display: none; }

/* ── THE FRAME ──────────────────────────────────────────────────────────── */
.cc-b2-framebox {
  position: relative; z-index: 1;
  width: 100%; height: 100%;
  display: flex; align-items: center; justify-content: center;
  min-width: 0; min-height: 0;
  /* Room for the props to sit BESIDE the frame rather than under its edges,
     which is how his mock stages them. Reclaimed the moment props drop out.
     ⚠ ASYMMETRIC AND IN PERCENT, both on purpose. His mock does not centre the
     frame in the stage: the left gutter carries the photo strip AND the camera
     (14.3% of the app) while the right carries only the film can (4.3%), so a
     symmetric padding puts the frame ~15px left of where he drew it. Percent
     rather than px so the gutters keep their proportion as the window is
     dragged — these resolve against the STAGE's width. */
  padding-left: 19%;
  padding-right: 4.5%;
}
.cc-b2-noprops .cc-b2-framebox { padding-inline: 0; }
/* The pocket is a real occupant of the top-left, not an overlay — without this
   whatever's in it (the tray, or its CTA below .cc-bcompact 2026-07-30) sits on
   top of the curtain in his frame art. Only needed once the props (and with
   them the 19% gutter) have dropped out; above that the gutter is already
   wider than the pocket. Unconditional on strip-on: the CTA occupies this same
   spot while the strip is off, so the reservation is needed either way — only
   .cc-bcompact (which hides both) frees the space back up. */
.cc-b2-noprops:not(.cc-bcompact) .cc-b2-framebox { padding-left: 138px; }
/* sized in PIXELS by fitFrame() in booth.js. ⚠ Deliberately not CSS
   aspect-ratio: with a definite width that computes a height, max-height then
   clamps without shrinking the width back, and his art stretches. */
.cc-b2-frame { position: relative; }
/* ⚠ THE ART SITS OVER THE PICTURE, NOT UNDER IT (his call, 2026-07-31).
   The well is a hard rectangle at the measured bbox of the art's transparent
   screen; the art's own opening is rounded and has a printed inner bezel. With
   the well painting last, the picture's square corners covered those rounded
   corners and the photo read as stuck ON the camera instead of showing THROUGH
   it. Same paint order the photo strip already uses: photos first, frame on top.
   Safe because the art is pointer-events:none and genuinely transparent over
   the well — the video, SNAP and the wipe handle all stay live underneath. */
.cc-b2-frameart {
  position: absolute; inset: 0; z-index: 1;
  display: block; width: 100%; height: 100%;
  image-rendering: pixelated; pointer-events: none;
}

/* ⚠ THESE PERCENTAGES ARE MEASURED FROM HIS ART and must agree with ART_W /
   ART_H in booth.js's fitFrame(). Source of truth: the build script's printout
   and _tools/booth-v2/manifest.json. Re-measure if the frame art is ever
   swapped; do NOT eyeball them. */
.cc-b2-well {
  position: absolute; z-index: 0;      /* under .cc-b2-frameart — see above */
  left: 14.39%; top: 15.25%; width: 71.02%; height: 72.07%;
  display: flex; align-items: center; justify-content: center;
  overflow: hidden; background: #05030f;
}
/* ── THE BULB ───────────────────────────────────────────────────────────── */
/* ⚠ THERE IS NO BULB ELEMENT, AND THERE SHOULD NOT BE ONE (his call,
   2026-08-01: "the bulb is baked into the original camera image").
   v2 layered a 3-frame animated overlay (`bulb-sheet.webp`, from raw/bulb1-3)
   on top of the frame art — but the art he drew ALREADY contains a lit bulb, so
   that overlay was a second bulb painted over the first. He asked for the
   animation to stop, then for the whole thing to go. The bulb you see in the
   top-right of the fixture is his, in `booth-frame.webp`, and it needs no CSS,
   no sprite, and no request. The sprite strip is deleted and build.py no longer
   emits it; the raws stay in `_tools/booth-v2/raw/` as the record of what he
   sent. `.cc-b2-counting` went with the countdown on 2026-07-31. */

/* art missing, or the window is too small for the bezel to earn its 50% */
.cc-b2-noart .cc-b2-frameart { display: none; }
.cc-b2-noart .cc-b2-well,
.cc-b2-tight .cc-b2-well {
  position: static; inset: auto;
  width: 100%; height: 100%;
  border: 2px solid var(--b2-line); border-radius: 10px;
}
.cc-b2-tight .cc-b2-frame { width: 100% !important; height: 100% !important; }
.cc-b2-tight .cc-b2-frameart { display: none; }

/* ── THE PRINT ──────────────────────────────────────────────────────────── */
/* ⚠ ONCE A PHOTO EXISTS, THE CAMERA STEPS ASIDE (his call, 2026-07-31):
   "swap to a framed image after snap so that we don't have to letterbox them
   into the camera." The camera screen is a fixed 1.2911 rectangle, so a photo
   of any other shape had to be contain-fitted into it against black — and once
   SNAP started cropping to what the kid framed, IMPORT was the last path still
   showing bars. A print has no fixed shape: the mat hugs the canvas, so the
   picture keeps its OWN aspect and there is nothing left to letterbox. It also
   reads right — you took the photo, now you are holding it.
   The frame ART hides — and the bulb goes with it, being painted into that same
   image. The frame BOX does not: the props, the tray and its CTA are all
   anchored to it (see the note at the top of this file), so collapsing it here
   would scatter the whole composition. */
.cc-b2-printed .cc-b2-frameart { display: none; }
/* The well stops being the camera's screen and becomes the space the print
   floats in — transparent, so his starfield reads behind it, and overflow
   visible so the mat's shadow is not clipped. */
.cc-b2-printed .cc-b2-well {
  left: 0; top: 0; width: 100%; height: 100%;
  background: none; overflow: visible;
}
/* tight/noart give the well a dark bordered box of its own; a print needs neither */
.cc-b2-printed.cc-b2-tight .cc-b2-well,
.cc-b2-printed.cc-b2-noart .cc-b2-well { border: 0; background: none; }
/* ⚠ RETAKE JOINS THE FLOW rather than floating at `bottom: 7%` the way SNAP
   does. Absolute, it needed a fixed reserved band beneath the print, and a band
   big enough for the button at every window size cost a portrait print more
   height than the button actually occupies — measured 44.1% of the app against
   the 46.8% the old letterboxed well gave it, i.e. the change would have made
   the very case it exists to fix slightly smaller. In flow, the reservation is
   exactly the button plus the gap, and layoutStage() MEASURES it.
   ⚠ These paddings are subtracted in layoutStage() too: the canvas is sized in
   JS and cannot discover them from the cascade on its own. */
/* ⚠ PADDING HERE IS IN PX, NEVER %. A percentage padding — even the vertical
   one — resolves against the containing block's WIDTH. A `13%` bottom reserve
   measured 64.7px on a 498px-wide pane and ate 41% of a 158px stage, which is
   how the print collapsed to a 16×24px stamp at a 520×460 window. */
.cc-b2-printed .cc-b2-photo {
  flex-direction: column; justify-content: center; gap: 14px;
  padding: 10px 12px;
}
.cc-b2-printed .cc-b2-retake { position: static; transform: none; flex: none; }
.cc-b2-printed .cc-b2-retake:active { transform: translateY(4px); box-shadow: 0 1px 0 #4a3f8f; }
.cc-b2-print {
  /* --b2-mat is set per-frame-size by setMat() in booth.js and inherited from
     .cc-b2-frame. ⚠ Fallback via var()'s second argument, NOT a local
     declaration — declaring it here would shadow the inherited value. */
  background: #fff;
  padding: var(--b2-mat, 12px) var(--b2-mat, 12px) calc(var(--b2-mat, 12px) * 2.6);
  border-radius: 2px;
  box-shadow: 0 12px 28px rgba(0, 0, 0, .55), 0 2px 0 rgba(0, 0, 0, .3);
  line-height: 0;                     /* kill the descender gap under the canvas */
  flex: none;                         /* the canvas is sized in JS — never let flex shrink the mat onto it */
  max-width: 100%; max-height: 100%;
}
/* both classes land on .cc-booth itself — compound, never descendant */
.cc-b2-printed.cc-bcompact .cc-b2-photo { padding: 6px 8px; gap: 9px; }
.cc-b2-printed.cc-bcompact .cc-b2-retake { font-size: 15px; padding: 8px 20px; }

/* ⚠ BELOW .cc-b2-tight THERE IS NO MAT. Same call the frame art already makes
   two rules up: under a certain size the decoration costs more than it gives.
   The mat + an in-flow RETAKE were taking 89px of a 158px stage, leaving the
   picture SMALLER than the letterboxed well it replaced — the change working
   against its own purpose at the bottom of the ladder. Here the picture takes
   the whole box and RETAKE floats over it, exactly as before the print existed.
   ⚠ Absolute again means layoutStage()'s in-flow sibling loop correctly stops
   subtracting it — that check is what makes this a pure CSS switch. */
.cc-b2-printed.cc-b2-tight .cc-b2-photo { padding: 0; gap: 0; }
.cc-b2-printed.cc-b2-tight .cc-b2-print { padding: 0; background: none; box-shadow: none; }
.cc-b2-printed.cc-b2-tight .cc-b2-retake {
  position: absolute; left: 50%; bottom: 7%; transform: translateX(-50%);
}
.cc-b2-printed.cc-b2-tight .cc-b2-retake:active {
  transform: translateX(-50%) translateY(4px);
}

/* ── WELL CONTENTS ──────────────────────────────────────────────────────── */
.cc-b2-cam, .cc-b2-imp, .cc-b2-photo {
  position: relative;
  width: 100%; height: 100%;
  display: flex; align-items: center; justify-content: center;
}
.cc-b2-cam video {
  width: 100%; height: 100%; object-fit: cover;
  transform: scaleX(-1);            /* selfie mirror — the capture mirrors to match */
  background: #05030f;
}
.cc-b2-photo canvas { display: block; image-rendering: auto; }

/* One of his sample pictures, shown in the screen until the camera delivers
   real frames (and permanently if it never does). Over the video — which is
   still black at this point — and under the wait/ready overlays, which follow
   it in the DOM. It ships pre-cropped to the well's aspect, so `cover` here is
   a belt-and-braces no-op rather than a second, different framing.
   ⚠ Never `srcCanvas`: it drives the tiles and fills the screen, but it is not
   a photo the kid took, so the export verbs stay disabled. */
.cc-b2-demo {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover;
  display: block; pointer-events: none;
}
/* the wait copy now sits over a busy picture rather than over black */
.cc-b2-wait { background: rgba(9, 6, 26, .55); }

.cc-b2-wait, .cc-b2-ready {
  position: absolute; inset: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px;
  text-align: center; padding: 18px; pointer-events: none;
}
/* sits at the TOP of the well — centred, it would land square on a face */
.cc-b2-ready { justify-content: flex-start; padding-top: 16px; }
.cc-b2-readylbl {
  font-family: 'Minecraft', monospace; font-weight: 700;
  font-size: 20px; letter-spacing: 1px; color: #fff;
  text-shadow: 0 2px 0 #000;
}
.cc-b2-waitlbl { font-weight: 700; font-size: 13px; }
.cc-b2 .cc-bmuted { font-size: 11px; color: var(--b2-dim); }
.cc-b2 .cc-bspin {
  width: 22px; height: 22px; border-radius: 50%;
  border: 3px solid rgba(255,255,255,.25); border-top-color: var(--b2-cy);
  animation: cc-b2-spin .8s linear infinite;
}
@keyframes cc-b2-spin { to { transform: rotate(360deg); } }

/* THE SHUTTER — one hard flash, no countdown (his call, 2026-07-31).
   Full white, instant, gone in under a fifth of a second: a real flash fires and
   decays, it does not fade in. The 3-2-1 `.cc-b2-count` overlay and the bulb's
   `.cc-b2-counting` wind-up were removed with it. z-index 1 keeps the flash over
   the picture but under the frame art, so the camera body does not light up. */
.cc-b2-flash { position: absolute; inset: 0; z-index: 1; background: #fff; opacity: 0; pointer-events: none; }
.cc-b2-flash.cc-flash { animation: cc-b2-flash .18s ease-out; }
@keyframes cc-b2-flash { 0%, 28% { opacity: 1; } 100% { opacity: 0; } }

/* SNAP / RETAKE — the one loud button, inside the well like his mock */
.cc-b2-snap {
  position: absolute; left: 50%; bottom: 7%; transform: translateX(-50%);
  padding: 14px 44px; cursor: pointer;
  background: var(--b2-pk); color: #fff;
  border: 3px solid #a8144d; border-radius: 999px;
  box-shadow: 0 5px 0 #a8144d;
  font-family: 'Minecraft', monospace; font-weight: 700;
  font-size: 26px; letter-spacing: 2px;
}
.cc-b2-snap:hover:not(:disabled) { background: #ff4f92; }
.cc-b2-snap:active:not(:disabled) { transform: translateX(-50%) translateY(4px); box-shadow: 0 1px 0 #a8144d; }
.cc-b2-retake { background: #241f4d; border-color: #4a3f8f; box-shadow: 0 5px 0 #4a3f8f; font-size: 19px; padding: 11px 30px; }
.cc-b2-retake:hover { background: #342d68; }
.cc-b2-snap-imp { position: static; transform: none; font-size: 18px; padding: 12px 28px; }
.cc-b2-snap-imp:active { transform: translateY(4px); }

/* IMPORT pane */
.cc-b2-imp {
  flex-direction: column; gap: 12px; padding: 22px; text-align: center;
  border: 3px dashed #453c86; border-radius: 12px; margin: 12px;
  width: calc(100% - 24px); height: calc(100% - 24px);
}
.cc-b2-imp.cc-bdrag { border-color: var(--b2-cy); background: rgba(22,224,224,.08); }
.cc-b2-imp-ico svg { width: 46px; height: 46px; fill: var(--b2-cy); }
.cc-b2-imp-h { margin: 0; font-family: 'Minecraft', monospace; font-weight: 700; font-size: 18px; }
.cc-b2-imp-sub { margin: 0; font-size: 11px; color: var(--b2-dim); line-height: 1.7; }
.cc-b2-imp kbd {
  background: #241f4d; border: 1px solid var(--b2-line); border-radius: 4px;
  padding: 1px 5px; font-family: 'Pixeloid Mono', monospace; font-size: 10px;
}

.cc-b2-err {
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  padding: 16px; text-align: center;
}
/* ⚠ In the CAMERA pane this has to be an overlay, not a flex sibling. The video
   is width:100% of a 510px pane, so a static error card was laid out BESIDE it
   (measured 210px starting at x=405) and half of it fell outside the screen —
   broken before the samples landed, invisible after, since the sample canvas is
   positioned and therefore paints over any static sibling. The scrim keeps the
   sample readable as context behind the message. The IMPORT pane's copy of this
   element is a column child and stays in flow. */
.cc-b2-cam .cc-b2-err {
  position: absolute; inset: 0; z-index: 2;
  justify-content: center; padding: 22px;
  background: rgba(9, 6, 26, .78);
}
.cc-b2-err .cc-berr-ico svg { width: 34px; height: 34px; fill: var(--b2-pk); }
.cc-b2-err .cc-berr-h { font-family: 'Minecraft', monospace; font-weight: 700; font-size: 16px; }
.cc-b2-err .cc-berr-p { font-size: 11px; color: var(--b2-dim); line-height: 1.6; max-width: 34ch; }
.cc-b2 .cc-bbusy {
  position: absolute; inset: 0; z-index: 6; display: grid; place-items: center;
  background: rgba(9,6,26,.82); font-weight: 700; font-size: 13px;
}

/* ── BEFORE / AFTER wipe ────────────────────────────────────────────────── */
.cc-b2 .cc-bcompare { position: absolute; z-index: 4; pointer-events: auto; cursor: ew-resize; }
.cc-b2 .cc-bcmp-line { position: absolute; top: 0; bottom: 0; width: 3px; background: #fff; box-shadow: 0 0 0 1px rgba(0,0,0,.5); }
.cc-b2 .cc-bcmp-handle {
  position: absolute; top: 50%; width: 40px; height: 40px; margin: -20px 0 0 -20px;
  border-radius: 50%; background: #fff; border: 3px solid #0b0716; cursor: ew-resize;
  display: grid; place-items: center; padding: 0;
}
.cc-b2 .cc-bcmp-grip svg { width: 18px; height: 18px; fill: #0b0716; }
.cc-b2 .cc-bcmp-tag {
  position: absolute; top: 10px; padding: 4px 9px; border-radius: 6px;
  background: rgba(9,6,26,.8); font-size: 10px; font-weight: 700; letter-spacing: 1px;
  opacity: 0; transition: opacity .15s;
}
.cc-b2 .cc-bcmp-tagb { left: 10px; color: var(--b2-dim); }
.cc-b2 .cc-bcmp-taga { right: 10px; color: var(--b2-cy); }
.cc-b2 .cc-bshow-b .cc-bcmp-tagb, .cc-b2 .cc-bshow-a .cc-bcmp-taga { opacity: 1; }

/* ── THE STRIP TRAY ─────────────────────────────────────────────────────── */
/* Pocketed top-left of the stage, where his mock drew the emoji strip. */
/* ⚠ Same anchoring and the same measured-not-chosen rule as the props above:
   % of THE FRAME's box, from his sample. v2 had this at `left:0; top:0` of the
   stage, which pinned it under the header — his sits nearly a quarter of the way
   down, directly ABOVE the camera prop, the two touching as one stacked cluster
   in the left gutter. That 12-percentage-point vertical error is most of why the
   left column read as empty. */
.cc-b2-tray {
  position: absolute; left: -23.73%; top: 10.32%; z-index: 3;
  width: 18.14%;
  display: flex; flex-direction: column; gap: 6px;
}
/* −2.5° on the PAPER only, never on .cc-b2-tray — the tray also holds the
   ADD / ⬇ / 💾 / ✕ row, and tilting live buttons is a different thing entirely.
   The art already carries most of its own lean; this is the measured residual
   (compare.py's strip row: his −2.5°, ours was 0°). */
.cc-b2-traypaper { position: relative; width: 100%; transform: rotate(-2.5deg); filter: drop-shadow(0 6px 14px rgba(0,0,0,.55)); }
.cc-b2-tray-art {
  position: absolute; inset: 0; width: 100%; height: 100%;
  display: block; image-rendering: pixelated;
}
.cc-b2-tray-emoji { z-index: 0; }
/* ⚠ pointer-events:none is REQUIRED — the frame sits over every slot button and
   would otherwise eat all four clicks (same trap as the arcade cabinet art). */
.cc-b2-tray-frame { z-index: 2; pointer-events: none; }
.cc-b2-tray-slots { position: absolute; inset: 0; z-index: 1; }

.cc-b2-slot {
  position: absolute; padding: 0; overflow: hidden; cursor: pointer;
  background: transparent; border: 0;
  display: grid; place-items: center;
}
.cc-b2-slot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cc-b2-slot:disabled { cursor: default; }
.cc-b2-slot-add {
  font-family: 'Minecraft', monospace; font-weight: 700; font-size: 26px;
  color: #1b1240; background: var(--b2-li);
  width: 100%; height: 100%; display: grid; place-items: center;
}
.cc-b2-slot.cc-next .cc-b2-slot-add { animation: cc-b2-pulse 1.6s ease-in-out infinite; }
@keyframes cc-b2-pulse { 0%,100% { opacity: 1; } 50% { opacity: .55; } }
.cc-b2-slot-x {
  position: absolute; inset: 0; display: grid; place-items: center;
  background: rgba(9,6,26,.72); color: #fff; font-size: 19px; font-weight: 700;
  opacity: 0; transition: opacity .12s;
}
.cc-b2-slot.cc-filled:hover .cc-b2-slot-x,
.cc-b2-slot.cc-filled:focus-visible .cc-b2-slot-x { opacity: 1; }

/* ADD gets its own full-width row; the three icon actions share the next one.
   All four on one 124px line squeezed the glyphs to unreadable and wrapped the
   ＋ away from its own label. */
.cc-b2-tray-acts { display: flex; flex-wrap: wrap; gap: 4px; }
.cc-b2-tact {
  flex: 1 1 0; padding: 7px 6px; cursor: pointer;
  background: #241f4d; color: var(--b2-ink);
  border: 2px solid var(--b2-line); border-radius: 8px;
  font-family: inherit; font-weight: 700; font-size: 13px; line-height: 1;
}
.cc-b2-tact-go {
  flex: 1 0 100%; background: var(--b2-li); color: #1b1240; border-color: #7d9e12;
  font-size: 11px; letter-spacing: .4px; white-space: nowrap;
}
.cc-b2-tact:hover:not(:disabled) { border-color: var(--b2-cy); }
.cc-b2-tact-clear:hover:not(:disabled) { border-color: var(--b2-pk); color: var(--b2-pk); }

/* ── STRIP TRAY CTA — shown while PHOTO STRIP is OFF ───────────────────────
   His mock draws the emoji strip whether or not it holds any photos, so an
   OFF tray left the pocket bare — a hole in the composition. The approved
   fix (his own idea) is the same two art layers as a clickable "turn it on"
   invite. It occupies the IDENTICAL box as .cc-b2-traypaper — same left/top/
   width here, same aspect (set from the one TRAY source of truth in
   booth-strip.js, via booth.js) — so flipping the strip on never shifts
   anything already on screen; the tray simply appears where this was. */
.cc-b2-stripcta {
  position: absolute; left: -23.73%; top: 10.32%; z-index: 3;
  width: 18.14%; padding: 0; margin: 0; border: 0; background: transparent;
  cursor: pointer;
  /* the same measured −2.5° the tray's paper carries, so the two are identical
     objects in the same pocket — ⚠ the hover state must REPEAT the rotation,
     since `transform` replaces rather than accumulates */
  transform: rotate(-2.5deg);
  filter: drop-shadow(0 6px 14px rgba(0,0,0,.55));
  transition: filter .15s ease, transform .15s ease;
}
.cc-b2-stripcta:hover, .cc-b2-stripcta:focus-visible {
  filter: drop-shadow(0 9px 18px rgba(0,0,0,.6)) brightness(1.15);
  transform: rotate(-2.5deg) translateY(-2px);
}
/* Down here the frame is small enough that 18.14% of it is an unusable target,
   so the pocket goes back to absolute px — sized to sit inside the 138px
   reservation .cc-b2-noprops keeps for it. Paired with the .cc-b2-rail
   .cc-b2-tray rule further down; change both or neither. */
.cc-b2-rail .cc-b2-stripcta { width: 92px; left: -104px; }
/* the frame has taken the whole box and there is no gutter left to sit in */
.cc-b2-tight .cc-b2-stripcta, .cc-b2-tight .cc-b2-tray { display: none; }
/* same reasoning as .cc-bcompact .cc-b2-tray below: down here the pocket
   costs the picture too much room, and the dock STRIP button is the way in. */
.cc-bcompact .cc-b2-stripcta { display: none; }

/* ── SIDE PANEL ─────────────────────────────────────────────────────────── */
.cc-b2-side {
  /* His mock gives the filter panel ~30% of the app's width; at a flat 320px it
     read as a cramped afterthought beside the frame and forced the 3-column
     tiles down to ~91px. Proportional, with a floor so it survives the band
     between .cc-b2-rail (920) and .cc-b2-noprops (1180), and a ceiling so a
     maximised 1080p window does not hand it half the app. */
  flex: 0 0 clamp(320px, 30.5%, 470px);
  display: flex; flex-direction: column; gap: 10px;
  min-width: 0; min-height: 0;
  padding: 12px;
  background: rgba(22,18,58,.93);
  border: 3px solid var(--b2-edge); border-radius: 16px;
}
.cc-b2-sidehead { flex: 0 0 auto; display: flex; align-items: center; gap: 10px; }
.cc-b2-sidetitle {
  font-family: 'Minecraft', monospace; font-weight: 700;
  font-size: 21px; letter-spacing: 1px; color: var(--b2-cy);
}
.cc-b2-adjbtn {
  margin-left: auto; display: inline-flex; align-items: center; gap: 7px;
  padding: 8px 13px; cursor: pointer;
  background: #241f4d; color: var(--b2-ink);
  border: 2px solid #6c4bc4; border-radius: 10px;
  font-family: inherit; font-weight: 700; font-size: 11px; letter-spacing: .5px;
}
.cc-b2-adjbtn svg { width: 14px; height: 14px; fill: currentColor; }
.cc-b2-adjbtn:hover { border-color: var(--b2-cy); }
.cc-b2-adjbtn.cc-on { background: var(--b2-cy); color: #06121a; border-color: #0b6f7a; }

/* the panel body is the ONE scroller — nested scrollers fighting over a 520px
   window is what clipped things in v1 */
.cc-b2-panes { flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden; }

.cc-b2 .cc-bgroups {
  position: sticky; top: 0; z-index: 2;
  display: flex; flex-wrap: wrap; gap: 6px;
  padding: 0 0 9px; margin-bottom: 3px;
  background: linear-gradient(#16123a 76%, rgba(22,18,58,0));
}
.cc-b2 .cc-bgroup {
  padding: 7px 12px; cursor: pointer;
  background: #241f4d; color: var(--b2-ink);
  border: 2px solid #4a3f8f; border-radius: 9px;
  font-family: inherit; font-weight: 700; font-size: 12px; letter-spacing: .4px;
}
.cc-b2 .cc-bgroup:hover { border-color: var(--b2-cy); }
.cc-b2 .cc-bgroup.cc-on { background: var(--b2-li); color: #1b1240; border-color: #7d9e12; }

.cc-b2 .cc-bedit-filmstrip {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
}
.cc-b2 .cc-bfilm-head {
  grid-column: 1 / -1;
  margin: 10px 0 1px;
  font-family: 'Minecraft', monospace; font-weight: 700;
  font-size: 13px; letter-spacing: 1px; color: var(--b2-gold);
}
.cc-b2 .cc-bfilm-tile {
  display: flex; flex-direction: column; gap: 5px; padding: 0;
  background: none; border: 0; cursor: pointer; min-width: 0;
}
.cc-b2 .cc-bfilm-shot {
  position: relative; display: block; width: 100%; aspect-ratio: 1;
  border: 3px solid #4a3f8f; border-radius: 10px; overflow: hidden;
  background: #241f4d;
}
.cc-b2 .cc-bfilm-shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.cc-b2 .cc-bfilm-tile:hover .cc-bfilm-shot { border-color: var(--b2-pk); }
.cc-b2 .cc-bfilm-tile.cc-on .cc-bfilm-shot { border-color: var(--b2-cy); }
/* ⚠ DEFECT-3 FIX (2026-08-01): a bare `<span>` is `display: inline`, and CSS
   overflow (hence text-overflow: ellipsis) never applies to non-replaced
   inline boxes — so the old `overflow:hidden; text-overflow:ellipsis;
   white-space:nowrap` here was silently inert and long names ("WATERCOLOR" 99px,
   "KALEIDOSCOPE" 113px) simply overflowed the ~86-88px tile at every size, not
   just narrow ones. Fix: let it wrap onto its OWN two lines instead of
   truncating a kid-facing filter name into mush, using `-webkit-line-clamp` (a
   block box, so overflow/ellipsis now actually apply) to cap it at 2 lines and
   ellipsis anything still too long. `min-height` reserves that 2-line height on
   every tile regardless of whether ITS OWN label needs one line or two, so the
   35 tiles stay uniform — short one-line labels don't create shorter tiles in
   the same grid row. Doesn't touch tile width, grid-template-columns, or the
   measured .cc-b2-side panel width. */
.cc-b2 .cc-bfilm-lbl {
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
  font-weight: 700; font-size: 11px; letter-spacing: .2px; color: var(--b2-dim);
  line-height: 1.25; min-height: 2.5em;
  text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: normal;
}
.cc-b2 .cc-bfilm-tile.cc-on .cc-bfilm-lbl { color: var(--b2-cy); }
.cc-b2 .cc-bfilm-tick {
  position: absolute; right: 4px; top: 4px;
  width: 20px; height: 20px; border-radius: 50%;
  background: var(--b2-cy); color: #06121a;
  display: grid; place-items: center; font-size: 12px; font-weight: 700;
  opacity: 0; transition: opacity .12s;
}
.cc-b2 .cc-bfilm-tile.cc-on .cc-bfilm-tick { opacity: 1; }
.cc-b2 .cc-bloading .cc-bfilm-shot { background: #241f4d; }

/* MORE FILTERS — a signpost at the seam, drawn as clearly NOT a filter */
.cc-b2-more .cc-bfilm-shot {
  border-style: dashed; border-color: #6c4bc4;
  display: grid; place-items: center; background: transparent;
}
.cc-b2-more:hover .cc-bfilm-shot { border-color: var(--b2-gold); }
/* the one label allowed to wrap — "MORE FILTERS" ellipsised to "MORE FILTE…" */
.cc-b2-more .cc-bfilm-lbl { white-space: normal; line-height: 1.25; color: var(--b2-gold); }
.cc-b2-more-ico { font-size: 26px; color: var(--b2-gold); }
/* grid-column: the filmstrip is a 3-column grid, so without this the message
   becomes a 1-column-wide ransom note */
.cc-b2-nofilters { grid-column: 1 / -1; margin: 6px 2px; font-size: 11px; line-height: 1.7; color: var(--b2-dim); }

/* ADJUST pane + parameter sheet */
.cc-b2 .cc-badjust { display: flex; flex-direction: column; gap: 10px; }
.cc-b2 .cc-bnow {
  display: flex; align-items: center; gap: 8px; padding: 9px 11px; cursor: pointer;
  background: #241f4d; border: 2px solid #4a3f8f; border-radius: 10px;
  color: var(--b2-ink); font-family: inherit; font-size: 11px; font-weight: 700;
}
.cc-b2 .cc-bnow-k { color: var(--b2-dim); letter-spacing: .5px; }
.cc-b2 .cc-bnow-v { color: var(--b2-cy); }
.cc-b2 .cc-bnow-go { margin-left: auto; }
.cc-b2 .cc-bnow-go svg { width: 13px; height: 13px; fill: currentColor; }
.cc-b2 .cc-bnote { margin: 0; font-size: 11px; line-height: 1.6; color: var(--b2-dim); }
.cc-b2 .cc-brow { display: flex; flex-direction: column; gap: 5px; margin-bottom: 11px; }
.cc-b2 .cc-brow-head { display: flex; justify-content: space-between; font-size: 11px; font-weight: 700; }
.cc-b2 .cc-brow-num { font-family: 'Pixeloid Mono', monospace; color: var(--b2-cy); }
.cc-b2 .cc-brow input[type=range] { width: 100%; accent-color: var(--b2-cy); }
.cc-b2 .cc-bghost {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  padding: 9px 13px; cursor: pointer;
  background: #241f4d; color: var(--b2-ink);
  border: 2px solid #4a3f8f; border-radius: 10px;
  font-family: inherit; font-weight: 700; font-size: 11px; letter-spacing: .4px;
}
.cc-b2 .cc-bghost:hover { border-color: var(--b2-cy); }

/* the settings sheet — its own card, so RESET can never fall off the bottom of
   a scrolled list (which is exactly what happened in v1) */
.cc-b2 .cc-bparams {
  flex: 0 0 auto; max-height: 42%;
  display: flex; flex-direction: column; min-height: 0;
  padding: 10px; border-radius: 12px;
  background: #120f30; border: 2px solid var(--b2-line);
}
.cc-b2 .cc-bparams-head { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; margin-bottom: 9px; }
.cc-b2 .cc-bparams-name {
  font-family: 'Minecraft', monospace; font-weight: 700; font-size: 14px; color: var(--b2-gold);
}
.cc-b2 .cc-bparams-head .cc-breset { margin-left: auto; padding: 6px 10px; font-size: 10px; }
.cc-b2 .cc-bparams-rows { flex: 1 1 auto; min-height: 0; overflow-y: auto; }

/* ── DOCK ───────────────────────────────────────────────────────────────── */
/* flex-wrap, because the five buttons plus the tip card exceed a hand-resized
   window long before .cc-bcompact drops their labels at 620 — measured: 965px
   of content in an 856px window, overflowing the app horizontally. */
.cc-b2 .cc-booth-dock {
  position: relative;
  flex: 0 0 auto;
  display: flex; flex-wrap: wrap; align-items: center; gap: 8px;
  min-width: 0;
}
/* the contained darker band behind the five action buttons. His mock frames
   them as one bounded object, separate from the TIP card by a visible gap —
   ours had them floating loose on the starfield. There is no wrapper element
   around the buttons (JS out of scope), so this is a ::before sized to stop
   just short of .cc-b2-tip.
   ⚠ The calc below does NOT depend on how much width the buttons take: since
   .cc-b2-tip is `flex: 0 0 <basis>` with `margin-left: auto`, its left edge
   always sits exactly <basis> in from the dock's own right edge, regardless
   of button content width (auto-margin absorbs all leftover space before a
   fixed-basis trailing flex item). So `right: basis + gap` always lands the
   band's edge one gap short of the tip's left edge, at any window width. */
.cc-b2 .cc-booth-dock::before {
  content: "";
  position: absolute;
  top: -8px; bottom: -8px; left: -8px;
  right: calc(clamp(320px, 30.5%, 470px) + 8px);
  background: rgba(7, 5, 22, .55);
  border: 2px solid var(--b2-line);
  border-radius: 16px;
  z-index: 0;
  pointer-events: none;
}
.cc-b2 .cc-bdock-btn, .cc-b2 .cc-b2-tip { position: relative; z-index: 1; }
.cc-b2 .cc-bdock-btn {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 13px 17px; cursor: pointer;
  background: #1b1642; color: var(--b2-ink);
  border: 3px solid #4a3f8f; border-radius: 13px;
  font-family: inherit; font-weight: 700; font-size: 13px; letter-spacing: .5px;
  white-space: nowrap;
}
.cc-b2 .cc-bdock-btn:hover:not(:disabled) { border-color: var(--b2-cy); }
.cc-b2 .cc-bdock-ico svg { width: 17px; height: 17px; fill: currentColor; }
.cc-b2 .cc-bdock-new { background: var(--b2-li); color: #1b1240; border-color: #7d9e12; }
.cc-b2 .cc-bdock-new:hover:not(:disabled) { background: #d7ff5c; border-color: #7d9e12; }
.cc-b2 .cc-bdock-strip.cc-on { background: var(--b2-cy); color: #06121a; border-color: #0b6f7a; }

/* the ON/OFF switch inside PHOTO STRIP */
.cc-b2-switch {
  display: inline-flex; align-items: center; gap: 6px;
  padding: 3px 8px 3px 3px; border-radius: 999px;
  background: rgba(0,0,0,.42);
  font-family: 'Pixeloid Mono', monospace; font-size: 10px; letter-spacing: .5px;
}
.cc-b2-switch i { width: 13px; height: 13px; border-radius: 50%; background: #8b88b0; display: block; }
.cc-bdock-strip.cc-on .cc-b2-switch i { background: #06121a; }

.cc-b2-tip {
  /* Matches .cc-b2-side's basis exactly so its left edge lines up with the
     FILTERS panel's left edge above it, per his mock — both resolve against
     the same full app width since .cc-b2-main and .cc-booth-dock are equal-
     width siblings in the column flex. */
  flex: 0 0 clamp(320px, 30.5%, 470px); min-width: 0;
  display: flex; align-items: center; gap: 10px;
  padding: 10px 14px; margin-left: auto;
  background: rgba(22,18,58,.9);
  border: 2px solid var(--b2-li); border-radius: 13px;
}
.cc-b2-tip-txt { min-width: 0; font-size: 11px; line-height: 1.5; color: #e4e4f7; }
.cc-b2-tip-txt b { color: var(--b2-li); }
.cc-b2-byte { flex: 0 0 auto; width: 54px; height: auto; margin-left: auto; image-rendering: pixelated; }

/* ── FOCUS — one treatment for the whole mode ───────────────────────────── */
.cc-b2 :focus-visible { outline: 3px solid var(--b2-cy); outline-offset: 2px; }

/* ── INTERACTION — one language for the whole mode ──────────────────────── */
/* Craft pass, 2026-07-31. Three things were inconsistent rather than wrong:
   13 controls carried `cursor:pointer` but only 5 transitioned, so half the UI
   snapped between states and half eased; only SNAP had a **press** state, which
   in a skin whose entire language is chunky offset shadows meant every other
   button changed colour without ever feeling pushed; and "disabled" was drawn
   three different ways (.4 / .35 / .38) for one idea.
   ⚠ Transform / opacity / colour only — deliberately nothing that can change a
   box's size. The frame's scale is height-bound on the header and dock, and it
   is currently within 0.6pp of his sample; a stray padding here would move it.
   The reduced-motion block at the foot of this file already kills these
   transitions, so the press state degrades to an instant one. */
.cc-b2-src, .cc-b2-exit, .cc-b2-help, .cc-b2-adjbtn, .cc-b2-tact,
.cc-b2 .cc-bgroup, .cc-b2 .cc-bdock-btn, .cc-b2 .cc-bghost,
.cc-b2 .cc-bnow, .cc-b2 .cc-bfilm-shot {
  transition: background-color .14s ease, border-color .14s ease,
              color .14s ease, transform .1s ease, box-shadow .1s ease;
}
/* the physical push. SNAP is excluded on purpose: it is centred with a
   translateX and rides a taller 5px shadow, so it keeps its own 4px throw. */
.cc-b2-src:active, .cc-b2-exit:active, .cc-b2-help:active,
.cc-b2-adjbtn:active, .cc-b2 .cc-bgroup:active, .cc-b2 .cc-bghost:active,
.cc-b2 .cc-bnow:active,
.cc-b2-tact:active:not(:disabled),
.cc-b2 .cc-bdock-btn:active:not(:disabled) { transform: translateY(1px); }
.cc-b2-src.cc-on:active { box-shadow: 0 1px 0 #0b6f7a; }
/* one disabled treatment, not three */
.cc-b2 .cc-bdock-btn:disabled, .cc-b2-tact:disabled, .cc-b2-snap:disabled {
  opacity: var(--b2-off); cursor: default;
}
/* a filter tile is a picture, so it lifts rather than dimming on hover */
.cc-b2 .cc-bfilm-tile:hover .cc-bfilm-shot { transform: translateY(-2px); }
.cc-b2 .cc-bfilm-tile:active .cc-bfilm-shot { transform: translateY(0); }

/* ════════════════════════════════════════════════════════════════════════════
   THE NARROWING LADDER
   Classes come from a ResizeObserver on the APP WINDOW in booth.js, never from
   @media. The window is draggable — a media query measures the viewport, which
   is how a 500px window on a 1366px screen slipped through the last pass.
     .cc-b2-noprops (<1180)  props out
     .cc-b2-rail    (<920)   panel becomes a rail under the stage
     .cc-bcompact   (<620)   dock icon-only
     .cc-b2-short   (h<560)  tighter vertical rhythm
   ════════════════════════════════════════════════════════════════════════════ */
/* ⚠ A vertical panel simply MOVED below the stage is not a rail. The first
   attempt did that with max-height:44%, which left the stage ~218px tall — the
   frame then tripped .cc-b2-tight and dropped out altogether. The panel has to
   become genuinely short, which means the tiles run in a ROW. */
.cc-b2-rail .cc-b2-main { flex-direction: column; }
.cc-b2-rail .cc-b2-stage { flex: 1 1 auto; min-height: 0; }
.cc-b2-rail .cc-b2-side {
  flex: 0 0 auto; max-height: none; border-radius: 14px;
  padding: 9px 10px;
}
/* ⚠ DEFECT-2 FIX (2026-08-01): `overflow-y: hidden` was clipping real content —
   `.cc-bgroups` (the ALL/CLASSIC/ART/WILD chip row) sits ABOVE `.cc-bedit-
   filmstrip` (the horizontal tile row) in normal block flow inside this pane,
   so their combined height can exceed the pane's own height at a short window
   (measured scrollHeight 165-205px against a 46-64px clientHeight at 520x460 /
   400x420) — and with vertical overflow hidden and no scrollbar, whatever
   didn't fit was simply unreachable, not just "needs a swipe". The RAIL design
   itself (horizontal tile scrolling) is unchanged and still governed by
   `overflow-x: auto` below; `overflow-y: auto` only adds a vertical scrollbar
   for the cases where the chip row + tile row don't both fit, so every tile
   stays reachable without touching the min-width:0/min-height:0 chain that
   keeps the 2602px-wide filmstrip from inflating the app. */
.cc-b2-rail .cc-b2-panes { overflow-x: auto; overflow-y: auto; }
.cc-b2-rail .cc-bgroups { position: static; background: none; padding-bottom: 7px; }
.cc-b2-rail .cc-bedit-filmstrip { display: flex; gap: 8px; align-items: flex-start; }
.cc-b2-rail .cc-bfilm-tile { flex: 0 0 86px; }
/* the group spine survives as an inline marker between runs of tiles */
.cc-b2-rail .cc-bfilm-head {
  flex: 0 0 auto; align-self: center; margin: 0 2px;
  writing-mode: vertical-rl; transform: rotate(180deg); font-size: 11px;
}
.cc-b2-rail .cc-bparams { max-height: 150px; }
.cc-b2-rail .cc-b2-tray { width: 92px; left: -104px; }
.cc-b2-rail .cc-b2-title { height: 40px; }
.cc-b2-rail .cc-b2-safe { display: none; }
/* the tip is the least load-bearing thing in the dock — it goes before the
   dock is forced to wrap */
.cc-b2-rail .cc-b2-tip { display: none; }
/* with the tip gone, the button band runs the full dock width */
.cc-b2-rail .cc-booth-dock::before { right: -8px; }
/* ⚠ Even with the tip gone the five buttons measured 836px in an 828px dock and
   wrapped to a second row — which cost 59px of HEIGHT and starved the stage
   until the frame tripped .cc-b2-tight. Rather than shave paddings and sit one
   pixel from the same cliff, the three export verbs go icon-only here. The two
   that keep their labels are the two that are colour-coded and carry state. */
.cc-b2-rail .cc-bdock-btn:not(.cc-bdock-new):not(.cc-bdock-strip) span:not(.cc-bdock-ico) { display: none; }
.cc-b2-rail .cc-bdock-btn:not(.cc-bdock-new):not(.cc-bdock-strip) { padding: 13px; gap: 0; }

.cc-bcompact .cc-b2 { padding: 9px; }
.cc-b2.cc-bcompact { padding: 9px; gap: 8px; }
.cc-bcompact .cc-bdock-btn span:not(.cc-bdock-ico):not(.cc-b2-switch) { display: none; }
.cc-bcompact .cc-bdock-btn { padding: 12px; gap: 0; }
.cc-bcompact .cc-b2-tip { display: none; }
.cc-bcompact .cc-b2-src span:not(.cc-bico) { display: none; }
.cc-bcompact .cc-b2-src { padding: 9px 12px; }
.cc-bcompact .cc-b2-title { height: 32px; }
.cc-bcompact .cc-b2-side { flex-basis: auto; max-height: 46%; padding: 9px; }
.cc-bcompact .cc-bedit-filmstrip { grid-template-columns: repeat(auto-fill, minmax(74px, 1fr)); }
/* the tray costs the picture too much room down here — it folds behind its
   toggle and the dock button stays the way back to it */
.cc-bcompact .cc-b2-tray { display: none; }
/* Folded in from a dead `@media (max-width: 560px)` block (2026-08-06,
   CLAUDE.md gotcha #0b) that measured the VIEWPORT beside its own
   ResizeObserver fix — it could never fire for a narrow draggable app window
   on a normal-size screen. .cc-bcompact (<620px, compactRO above) is the
   real "narrow window" signal, so the wrap + centering reset move here. */
.cc-bcompact .cc-b2-head { flex-wrap: wrap; }
.cc-bcompact .cc-b2-srcwrap { margin: 0; }

.cc-b2-short .cc-b2-title { height: 34px; }
.cc-b2-short .cc-bdock-btn { padding: 9px 13px; }
.cc-b2-short .cc-b2-snap { padding: 10px 30px; font-size: 20px; }

@media (prefers-reduced-motion: reduce) {
  .cc-b2 *, .cc-b2 *::before, .cc-b2 *::after { animation: none !important; transition: none !important; }
}

/* ════════════════════════════════════════════════════════════════════════════
   ⚠ THE [hidden] TRAP — LAST RULE IN THE FILE, ON PURPOSE.
   This stylesheet puts an AUTHOR `display` on nearly everything the JS hides
   with the `hidden` attribute: .cc-b2-wait, .cc-b2-ready,
   .cc-b2-err, .cc-badjust, .cc-bparams, .cc-b2-panes, .cc-b2-tray, .cc-bbusy.
   An author display ALWAYS beats the UA's `[hidden] { display: none }`, so
   `el.hidden = true` silently does nothing.

   Observed on the first run, 2026-07-30: the ADJUST sliders rendered underneath
   the filter list, and "Waking up the camera…" never went away after the
   camera came up. Same class of bug as the pet dock (CLAUDE.md gotcha #4) and
   the .cc-pcard specificity trap (gotcha #5b).

   !important rather than a specificity race because these elements are hidden
   from a dozen places and a future rule with one more class would silently
   re-break it.
   ════════════════════════════════════════════════════════════════════════════ */
.cc-b2 [hidden] { display: none !important; }
