/* =========================================================
   GEXI — public/shell/gx-sandbox.css   (PROJECT ARC — PHASE A1, dark GEXI_PROJECT_SANDBOX)
   The sandbox shelf. Inert without the flag-gated markup (sandbox.js mounts nothing flag-off).
   Theme-safe: neutrals ride currentColor/rgba; no hard-coded light-only colours.
   ========================================================= */

/* the same-cell grid sibling: occupies the STAGE cell without touching #gx-stage's own channels */
.gx-sbx { display: contents; }
/* ⭐ WAVE 0.5 C5 — THE CHASSIS: head / body / FOOT, fixed now so no later wave moves a wall.
   The foot is RESERVED and renders literally nothing today — zero height, no border, no placeholder — which
   is why it is not a control and L-1 is untouched. It exists now because of what it costs to add later: the
   piece Box is currently `position:absolute; inset:0`, so it covers the whole panel, and a Box that covers
   the foot means you could never SPEAK TO the piece you have open. Bounding the Box to rows 1-2 is the entire
   structural precondition for WAVE 4's composer, and doing it now costs two lines instead of a re-layout.
   `position: relative` stays — .gx-sbx-lipbtn anchors to it. */
.gx-sbx-panel {
  grid-area: stage;
  position: relative; z-index: 5;                            /* KEPT — .gx-sbx-lipbtn anchors here */
  display: grid;
  grid-template-columns: minmax(0, 1fr);                     /* ONE column. Always. */
  grid-template-rows: auto minmax(0, 1fr) auto;
  grid-template-areas: 'head' 'split' 'foot';                /* every cell NAMED, so nothing auto-places */
  min-width: 0; min-height: 0;
  background: var(--gx-bg, rgba(20, 22, 26, 0.98));
  border-left: 1px solid rgba(127, 127, 127, 0.18);
}
/* ⚠️ THE CATCH-ALL, AND IT COMES FIRST. A panel child with a definite ROW and an auto COLUMN gets
   auto-placed into an IMPLICIT SECOND COLUMN sized to content — which is exactly the shipped bug recorded
   below: 449.78px of children inside a 447.3px panel, 2.5px of overflow, and every heading clipped so the
   workshop read "IE CUT" and "ll needed: footage". Naming the areas fixes today's children; this makes it
   impossible for a newcomer's child too, which is the half that actually prevents a recurrence. */
.gx-sbx-panel > * { grid-column: 1; min-width: 0; }

/* THE SPLIT WRAPPER — one column by default (the phone, and any desktop panel too narrow to hold two). D2
   put this element in place precisely so D5 could be a change of THIS template rather than a re-parenting. */
.gx-sbx-split { grid-area: split; display: grid; grid-template-columns: minmax(0, 1fr); min-width: 0; min-height: 0; }

/* ═══ ⭐ D5 — THE SPLIT: A BENCH AND A WORKSTATION ═════════════════════════════════════════════════════
   The owner's complaint was "how little space we have to work" — and the cause was not the panel's width,
   it was that opening a piece COVERED the cut and the shelf. So the workshop becomes two columns that are
   both permanently true: the BENCH (what the film is made of — the cut's order, the shelf, the rooms) and
   the WORKSTATION (the one thing you are working on, at full size, always there).

   ⚠️ `.is-split` IS WRITTEN BY JS, and that is deliberate — see the long note at SPLIT_MIN in sandbox.js.
   The short version: the split decides what is MOUNTED as well as how it is laid out, and a hidden <video>
   still fetches. One fact, one owner; CSS reacts, it does not also decide.

   THE RATIO. The workstation is the larger side because it holds the thing being watched, while the bench
   holds lists that read fine narrow. MEASURED IN THE BROWSER at the shipped 60/40 shell split — not
   computed here, because the last time this file carried arithmetic done in my head it was wrong by 112px:
     1280 viewport → panel 600  → bench 277 / work 323     1491 → panel 727  → bench  336 / work  391
     1920 viewport → panel 984  → bench 455 / work 529     3440 → panel 2424 → bench 1121 / work 1303
   At every one of those the two tracks sum EXACTLY to the panel — zero overflow, which is the property that
   the C5 implicit-column bug violated and the one worth pinning.

   ⚠️ WHY SPLIT_MIN EXISTS, in numbers: the floors below sum to 540, so a panel narrower than that CANNOT
   hold both tracks and would overflow — 180px of it at a 1000px viewport, where the desktop layout is still
   active and the panel is only 360 wide. JS refuses the split under 580, leaving a 40px margin over the
   arithmetic floor, and the shipped one-column + box-overlay path renders instead. */
.gx-sbx-panel.is-split > .gx-sbx-split {
  grid-template-columns: minmax(260px, 0.86fr) minmax(280px, 1fr);
  gap: 0;
}
.gx-sbx-panel.is-split > .gx-sbx-split > .gx-sbx-body { border-right: 1px solid rgba(127, 127, 127, 0.16); }

/* THE WORKSTATION. Its own scroll, so studying a piece never scrolls the bench away from under you — the
   two columns are independent surfaces, which is the whole point of them both being permanent. */
.gx-sbx-work {
  min-width: 0; min-height: 0;
  display: flex; flex-direction: column;
  overflow-y: auto; overscroll-behavior: contain;
  padding: 0 14px 24px;
}
.gx-sbx-workhead {
  display: flex; align-items: center; gap: 10px; padding: 12px 0;
  position: sticky; top: 0; background: inherit; z-index: 2;
}
.gx-sbx-worktitle {
  flex: 1; min-width: 0; margin: 0; font-size: 0.9375rem; font-weight: 600;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* ⚠️ 44px of touch target on a control that reads as a small glyph — the C12 lesson (a strip that LOOKED
   live and was dead to touch) applies to anything this size, desktop or not. */
.gx-sbx-workclear {
  font: inherit; font-size: 0.875rem; line-height: 1; background: none; border: 0; cursor: pointer;
  color: inherit; opacity: 0.6; border-radius: 8px;
  min-width: 32px; min-height: 32px; display: inline-flex; align-items: center; justify-content: center;
}
.gx-sbx-workclear:hover { opacity: 1; background: rgba(127, 127, 127, 0.12); }
.gx-sbx-workclear:focus-visible { outline: 2px solid var(--gx-brand, #17c07b); outline-offset: 2px; }
#gx-shell.gx-sandboxing #gx-stage { visibility: hidden; }   /* the stage cell yields while the sandbox is open — trees stay mounted */

/* B2 (found by the first LOOK at computed geometry — every DOM-driven check had passed over a 1px
   panel): the IDLE READING COLUMN rule (gx-skin.css §10) collapses the stage track to 0 when nothing
   is producing/staged — it predates the sandbox, which LIVES in that track. When the panel is open,
   the 3-zone split returns. One class more specific than §10's selector, so it wins; inert unless
   .gx-sandboxing exists, which only the flag-gated module ever sets — flag-off byte-inert. */
/* ⚠️ WAVE 0.5 C4 — the `:not([data-mode="project"])` below is PAIRED with the identical addition to
   gx-skin.css §10, and shipping one without the other resurrects the exact B2 bug the comment above
   describes. The arithmetic, because it is the whole reason this file wins:
     appShell.css:16  #gx-shell                                                  (1,0,0)
     gx-skin §10      #gx-shell:not(.gx-producing):not(:has(#gx-stage.gx-staged)) (2,2,0)
                      — :has() carries its most specific argument, #gx-stage.gx-staged
     this rule        + .gx-sandboxing                                            (2,3,0)  ← wins today
   gx-skin.css loads AFTER this file (app-shell.html:28 vs :24), so a TIE goes to gx-skin. Adding the
   :not() to §10 alone lifts it to (2,3,0) — a tie — and the flag-off desktop sandbox would open into a
   0-width track again. Adding it here too lifts this to (2,4,0), so the ordering is restored by margin.
   This rule stays for the CHAT lane (flag-off, and a chat thread's overlay); project mode never sets
   .gx-sandboxing on desktop, because there the workshop is furniture rather than an overlay. */
@media (min-width: 960px) {
  #gx-shell.gx-sandboxing:not([data-mode="project"]):not(.gx-producing):not(:has(#gx-stage.gx-staged)) {
    grid-template-columns: var(--gx-rail-w) minmax(0, var(--gx-chat-w)) minmax(0, 1fr);
  }
}

/* ═══ WAVE 0.5 C4 — THE MODE WEARS THE GRID ═══════════════════════════════════════════════════════════
   In project mode the workshop is not an overlay that opens; it is the third column, permanently. §10 (the
   CHAT idle reading column) no longer matches here, so the only competitor is appShell.css:16 at (1,0,0) —
   this is (1,1,0) and wins. No :has() in this block: the staged-room swap rides z-index instead, so an
   engine without :has() still behaves correctly rather than losing the workshop.
   ⚠️ NO TRANSITION on grid-template-columns, ever. The mode switch is instant — correctness never rides
   paint (rAF and CSS transitions freeze when a pane is not compositing; that family cost us a day). */
@media (min-width: 960px) {
  #gx-shell[data-mode="project"] {
    /* ⭐⭐ THE WORKSHOP LEADS (owner: "you can make the sandbox much bigger than the llm area, because it
       needs it more"). This inverts what the layout assumed. Every earlier version protected the
       conversation's 42rem reading column and gave the workshop whatever was spare — which is backwards for
       a PRO surface: a conversation only ever needs a readable column, while the workshop holds the actual
       WORK (the cut, the shelf, and the editing that lands in it) and can always use more.
       So the split is a RATIO, 60/40 in the workshop's favour, and it scales instead of capping. Measured:
         1280 → chat 400 / work 600 (3 cards)   ·  1491 → chat 484 / work 727 (4 cards)
         1920 → chat 656 / work 984 (6 cards)   ·  2560 → chat 912 / work 1368 (8 cards)
       against a flat 3 cards at every width before, because the old rule capped the workshop at 560px.
       ⚠️ THE 360px FLOOR ON THE CONVERSATION is the safety: when the viewport cannot afford the ratio the
       chat stops shrinking and the workshop yields, so the turn is never squeezed into an unreadable strip.
       The reading column below is intentionally left at 42rem — once the track is narrower than that, the
       `max()` picks the base padding and the text simply fills its column, which is the correct behaviour. */
    --gx-read-w-p: 42rem;
    /* ⚠️ THE CONVERSATION IS CAPPED AT A READING WIDTH — everything beyond it belongs to the work. A plain
       ratio was still wrong on a wide display: on the owner's 3440px screen a 40% share is 1264px for a
       column of text that reads at ~672px, so ~600px would have been pure gutter while the workshop went
       without. Text does not get better past a reading measure; a workbench does.
       So the chat track takes 40% but NEVER more than 46rem, and the workshop takes the remainder:
         1280 → chat 400 / work 600      1491 → chat 484 / work 727
         1920 → chat 656 / work 984      3440 → chat 736 / work 2424
       The 360px floor still protects the turn when the viewport cannot afford the ratio. */
    /* ⚠️ THE 40% IS OF THE SPACE AFTER THE RAIL, and the expression must SAY so. A bare `40%` in a grid
       track resolves against the GRID CONTAINER's inline size — the whole shell — not against what is left
       once the rail is taken. So the shipped rule under-delivered the ruling above by ~112px at 1280 and
       1491 and ~80px at 1920, agreeing with the table only at 3440 where the 46rem cap binds. I had
       computed those numbers in JS and assumed CSS percentages resolved the same way; they do not.
       Measured after the repair: 1280 → 400/600 · 1491 → 484/727 · 1920 → 656/984 · 3440 → 736/2424. */
    grid-template-columns:
      var(--gx-rail-w)
      minmax(360px, min(calc((100% - var(--gx-rail-w)) * 0.4), 46rem))
      minmax(0, 1fr);
  }
  /* the workshop owns the stage cell; the Stage itself is out of sight until a Tools room frames over it */
  #gx-shell[data-mode="project"] > #gx-stage { display: none; }
  #gx-shell[data-mode="project"] > #gx-stage.gx-staged { display: flex; z-index: 7; }   /* > the panel's z-index: 5 */
  #gx-shell[data-mode="project"] .gx-sbx-panel { border-left: 0; }                      /* #gx-chat's border-right is the hairline */
  /* C11 — COLLAPSED: the third track goes to zero and the conversation takes the room. Not a hidden panel
     (the Panel renders null), so nothing is mounted off-screen holding timers or focus. The pill brings it
     back. No transition on the track — the same rule as the mode switch: correctness never rides paint. */
  #gx-shell[data-mode="project"].gx-workshop-collapsed {
    grid-template-columns: var(--gx-rail-w) minmax(0, 1fr) 0;
  }
  /* the conversation keeps a reading column — feel-parity with chat mode, one size down */
  #gx-shell[data-mode="project"] .gx-dock-thread {
    padding-left:  max(var(--space-3), calc((100% - var(--gx-read-w-p)) / 2));
    padding-right: max(var(--space-3), calc((100% - var(--gx-read-w-p)) / 2));
  }
  #gx-shell[data-mode="project"] .cv-composer,
  #gx-shell[data-mode="project"] .cv-disclaimer { max-width: var(--gx-read-w-p); }
}

/* Mobile: ONE sheet on a project thread. Two independent 76px lips (appShell.css's stage peek and this
   file's sandbox lip) can only collide because both surfaces live on every thread; one mode, one sheet. */
@media (max-width: 959px) {
  #gx-shell[data-mode="project"] #gx-stage { display: none; }
}

/* ⚠️ grid-column: 1 IS LOAD-BEARING on all three. .gx-sbx-box claims `grid-column: 1` explicitly, and
   these were left on `auto` — so grid AUTO-PLACEMENT, finding column 1 of rows 1-2 already taken by the
   box, pushed them into an IMPLICIT SECOND COLUMN sized to content (449.78px inside a 447.3px panel).
   That 2.5px overflow is what put a horizontal scrollbar on the workshop and shifted every line left, so
   the panel read "IE CUT", "IE PIECES", "ll needed: footage". They must share ONE cell, because the box is
   meant to OVERLAY them, not sit beside them. */
.gx-sbx-head { grid-area: head; display: flex; align-items: center; gap: 10px; padding: 12px 14px; border-bottom: 1px solid rgba(127, 127, 127, 0.16); }
.gx-sbx-title { font-size: 1rem; font-weight: 600; margin: 0; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gx-sbx-note { font-size: 0.75rem; opacity: 0.75; }
.gx-sbx-x { font: inherit; background: none; border: 0; cursor: pointer; opacity: 0.6; padding: 4px 8px; border-radius: 8px; color: inherit; }
.gx-sbx-x:hover { opacity: 1; background: rgba(127, 127, 127, 0.12); }

.gx-sbx-body { grid-column: 1; min-width: 0; min-height: 0; overflow-y: auto; overscroll-behavior: contain; padding: 10px 14px 24px; }
/* C5 — the RESERVED foot. Renders nothing until WAVE 4 mounts the composer here; an empty grid row is not
   a control, so nothing on this surface promises anything it cannot do. */
.gx-sbx-foot { grid-area: foot; }
.gx-sbx-empty { opacity: 0.7; font-size: 0.875rem; padding: 18px 4px; }

/* WAVE 0 — THE BEGINNING: a brand-new project thread's resting state. Quiet on purpose: it names the
   space and the sayable way to fill it, then gets out of the way of the conversation beside it. Token-only
   colours (inherit + opacity) so it reads correctly in both themes with no palette of its own. */
.gx-sbx-begin { padding: 28px 4px; max-width: 46ch; }
.gx-sbx-begin-lead { font-size: 1rem; font-weight: 600; margin: 0 0 10px; }
.gx-sbx-begin-sub { font-size: 0.875rem; line-height: 1.55; opacity: 0.72; margin: 0 0 18px; }
.gx-sbx-begin-say { font-size: 0.875rem; opacity: 0.9; margin: 0; }

.gx-sbx-sec { margin-bottom: 18px; }
/* W1.3 — the header can now carry a control (the caption file) beside the readiness line, so it WRAPS:
   two controls plus a readiness string will not fit a ~340px column, and squeezing them is worse than a
   second line. `margin-right: auto` on the readiness span pushes any control to the trailing edge. */
.gx-sbx-sechead { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 10px; margin: 8px 0; }
.gx-sbx-ready { margin-right: auto; }
/* the caption file: the SAME quiet control as the film download, one size down — it is a cut-level action,
   not the headline. Reuses the download's tokens rather than inventing a second treatment (L-2). */
.gx-sbx-capdl {
  flex: none; display: inline-flex; align-items: center; gap: 4px;
  min-height: 28px; padding: 0 8px; box-sizing: border-box;
  font: inherit; font-size: 0.6875rem; font-weight: 600; text-decoration: none; white-space: nowrap;
  color: var(--ink); background: var(--card-alt, rgba(127, 127, 127, 0.14));
  border: 0.5px solid var(--line, rgba(127, 127, 127, 0.22)); border-radius: var(--radius, 10px);
  -webkit-tap-highlight-color: transparent; touch-action: manipulation;
  transition: background var(--dur-fast) var(--ease-out);
}
.gx-sbx-capdl:hover { background: var(--card, rgba(127, 127, 127, 0.2)); }
.gx-sbx-capdl:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 2px; }
.gx-sbx-capdl-ic { font-size: 0.8125rem; line-height: 1; opacity: 0.85; }
@media (max-width: 959px) { .gx-sbx-capdl { min-height: 44px; padding: 0 12px; font-size: 0.75rem; } }
.gx-sbx-sechead h3 { font-size: 0.8125rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; opacity: 0.85; margin: 0; }
.gx-sbx-ready, .gx-sbx-count { font-size: 0.75rem; opacity: 0.6; }
.gx-sbx-grouphead { font-size: 0.8125rem; font-weight: 600; opacity: 0.8; margin: 12px 0 6px; }

/* ═══ A3 — THE CUT: the ORDER strip (drag + keyboard reorder), the compile row, the film takes ═══ */
/* One horizontal scrollport of its own (the body's overflow-x would otherwise slide ALL sections);
   fixed tile size in EVERY state — the drop arithmetic assumes constant pitch, and nothing may
   reflow under the cursor. touch-action: manipulation, NEVER none — both native scrolls must keep
   working on the tiles; the drag lifts on long-press, and only then is the pan blocked. */
.gx-sbx-order {
  display: flex; flex-wrap: nowrap; gap: 4px;
  overflow-x: auto; overscroll-behavior-x: contain;
  position: relative; padding: 4px 2px 6px;
}
.gx-sbx-order.is-reordering { cursor: grabbing; }
.gx-sbx-mini {
  flex: 0 0 48px; width: 48px; height: 27px; border-radius: 4px;
  background-color: rgba(127, 127, 127, 0.35); background-size: cover; background-position: center;
  border: 0; padding: 0; cursor: grab; position: relative;
  touch-action: manipulation; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;
  display: inline-flex; align-items: center; justify-content: center; color: inherit;
}
.gx-sbx-mini::after { content: ''; position: absolute; inset: -8px -2px; }   /* touch target past the 27px visual */
.gx-sbx-mini:focus-visible { outline: 2px solid rgba(110, 168, 254, 0.8); outline-offset: 2px; }
.gx-sbx-miniglyph { font-size: 0.6875rem; opacity: 0.55; pointer-events: none; }
.gx-sbx-mini.is-making { background-image: none; background-color: transparent; background: repeating-linear-gradient(45deg, rgba(127,127,127,.25), rgba(127,127,127,.25) 4px, rgba(127,127,127,.45) 4px, rgba(127,127,127,.45) 8px); }
.gx-sbx-mini.is-failed { background-image: none; background-color: rgba(214, 84, 84, 0.55); }
/* the lifted tile stays IN ITS SLOT, dimmed — the committed order renders till the drop lands */
.gx-sbx-mini.is-dragging { opacity: 0.35; outline: 1px dashed currentColor; }
/* the drop caret: ONE absolutely-positioned bar at the insertion boundary — a gap would reflow the
   neighbours under the pointer (it scrolls WITH the strip content; no scrollLeft correction needed) */
.gx-sbx-caret { position: absolute; top: 2px; bottom: 4px; width: 3px; border-radius: 2px; background: rgba(110, 168, 254, 0.9); pointer-events: none; }
.gx-sbx-undo.is-cut { margin: 0 0 0 auto; padding: 3px 10px; font-size: 0.75rem; }
.gx-sbx-cutops { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; }
.gx-sbx-oprow.is-compiling { cursor: default; }
.gx-sbx-filmrow { margin-top: 8px; }
/* W1.2 — the film's identity line and its save control share one row, and WRAP rather than squeeze: the
   summary can run long ("compiled from an earlier cut — the order it used was not recorded") and the panel
   is a ~340px column at its narrowest, so on a phone the control drops to its own line instead of crushing
   the text. `align-items: baseline` keeps them on one optical line when they do fit. */
.gx-sbx-filmfoot { display: flex; flex-wrap: wrap; align-items: baseline; gap: 6px 10px; margin-top: 4px; }
.gx-sbx-filmsum { display: block; font-size: 0.75rem; opacity: 0.7; flex: 1 1 auto; min-width: 0; }
.gx-sbx-filmdl {
  flex: none; display: inline-flex; align-items: center; gap: 5px;
  min-height: 32px; padding: 0 10px; box-sizing: border-box;
  font: inherit; font-size: 0.75rem; font-weight: 600; text-decoration: none; white-space: nowrap;
  color: var(--ink); background: var(--card-alt, rgba(127, 127, 127, 0.14));
  border: 0.5px solid var(--line, rgba(127, 127, 127, 0.22)); border-radius: var(--radius, 10px);
  -webkit-tap-highlight-color: transparent; touch-action: manipulation;
  transition: background var(--dur-fast) var(--ease-out);
}
.gx-sbx-filmdl:hover { background: var(--card, rgba(127, 127, 127, 0.2)); }
.gx-sbx-filmdl:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 2px; }
.gx-sbx-filmdl-ic { font-size: 0.875rem; line-height: 1; opacity: 0.85; }
/* touch floor: 32px reads right beside a 0.75rem line, but a finger needs 44 (A4's rule for every control) */
@media (max-width: 959px) { .gx-sbx-filmdl { min-height: 44px; padding: 0 14px; } }
/* the visually-hidden utility (live region + strip instructions) */
.gx-sbx-vh { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }

/* the shelf grid — stable keys + in-place updates mean scroll NEVER jumps */
.gx-sbx-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(148px, 1fr)); gap: 10px; }
.gx-sbx-card { border-radius: 10px; padding: 4px; cursor: pointer; outline: none; border: 2px solid transparent; }
.gx-sbx-card:hover { background: rgba(127, 127, 127, 0.08); }
.gx-sbx-card:focus-visible { border-color: rgba(110, 168, 254, 0.7); }
.gx-sbx-card.is-selected { border-color: var(--gx-accent, #4c9a72); background: rgba(76, 154, 114, 0.08); }
.gx-sbx-card.is-failed { opacity: 0.85; }

.gx-sbx-thumb { width: 100%; border-radius: 8px; overflow: hidden; background: rgba(127, 127, 127, 0.14); position: relative; display: flex; align-items: center; justify-content: center; }
.gx-sbx-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
.gx-sbx-thumb.is-glyph { font-size: 1.6rem; opacity: 0.5; }
.gx-sbx-thumb.is-making { flex-direction: column; gap: 6px; }
.gx-sbx-prog { width: 72%; height: 4px; border-radius: 2px; background: rgba(127, 127, 127, 0.28); overflow: hidden; }
.gx-sbx-prog-bar { height: 100%; border-radius: 2px; background: var(--gx-accent, #4c9a72); transition: width 0.5s ease; }
.gx-sbx-prog-label { font-size: 0.6875rem; opacity: 0.75; }

.gx-sbx-meta { display: flex; align-items: center; gap: 6px; padding: 6px 2px 2px; }
.gx-sbx-name { flex: 1; min-width: 0; font-size: 0.75rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gx-sbx-badges { display: inline-flex; align-items: center; gap: 4px; flex: 0 0 auto; }
.gx-sbx-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
.gx-sbx-dot.is-failed { background: #d65454; }
.gx-sbx-dot.is-needs { background: #d6a854; }
.gx-sbx-lens { font-style: normal; font-size: 0.6875rem; font-weight: 700; width: 14px; height: 14px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; background: rgba(214, 168, 84, 0.25); color: #d6a854; }
.gx-sbx-takes { font-style: normal; font-size: 0.6875rem; opacity: 0.65; }
.gx-sbx-incut { font-style: normal; font-size: 0.5rem; color: var(--gx-accent, #4c9a72); }

/* ═══ B2.5 — THE INGREDIENT ROOMS (P5): the workshop reads as manifests at rest (the bench stays
   small, the mind's context-window read made visual); a room opens to its items on tap ═══ */
.gx-sbx-workshop { font-size: 0.8125rem; opacity: 0.85; margin: 2px 2px 6px; }
.gx-sbx-gaps { font-size: 0.75rem; margin: 0 2px 10px; color: #d6a854; }
.gx-sbx-room { border-top: 1px solid rgba(127, 127, 127, 0.14); padding: 8px 0 10px; }
.gx-sbx-roomhead { display: flex; align-items: center; gap: 8px; width: 100%; font: inherit; text-align: left;
  background: none; border: 0; color: inherit; cursor: pointer; padding: 2px; border-radius: 8px; }
.gx-sbx-roomhead:hover { background: rgba(127, 127, 127, 0.08); }
.gx-sbx-roomglyph { opacity: 0.7; }
.gx-sbx-roomlabel { font-size: 0.875rem; font-weight: 600; }
.gx-sbx-fresh { color: var(--gx-accent, #4c9a72); font-size: 0.625rem; }
.gx-sbx-roomman { font-size: 0.75rem; opacity: 0.7; margin: 4px 2px 0; padding-left: 22px; }
.gx-sbx-roomverdict { font-size: 0.75rem; color: #d6a854; margin: 4px 2px 0; padding-left: 22px; }
.gx-sbx-items { display: flex; flex-direction: column; gap: 4px; margin-top: 8px; padding-left: 22px; }
.gx-sbx-item { display: flex; align-items: center; gap: 8px; padding: 6px 8px; border-radius: 8px;
  border: 1px solid transparent; cursor: pointer; position: relative; }
.gx-sbx-item:hover { background: rgba(127, 127, 127, 0.08); }
.gx-sbx-item:focus-visible { border-color: rgba(110, 168, 254, 0.7); outline: none; }
.gx-sbx-item.is-selected { border-color: var(--gx-accent, #4c9a72); background: rgba(76, 154, 114, 0.08); }
.gx-sbx-item.is-attention .gx-sbx-itemstate { color: #d6a854; }
.gx-sbx-itemthumb { width: 34px; height: 24px; object-fit: cover; border-radius: 4px; flex: 0 0 auto; }
.gx-sbx-itemname { flex: 1; min-width: 0; font-size: 0.75rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gx-sbx-itemstate, .gx-sbx-itemused { font-size: 0.6875rem; opacity: 0.65; flex: 0 0 auto; }
.gx-sbx-cardstate { font-size: 0.8125rem; margin: 8px 2px 2px; }
.gx-sbx-cardfrom { font-size: 0.75rem; opacity: 0.7; margin: 2px 2px 8px; }
.gx-sbx-cardused { margin-top: 8px; display: flex; flex-direction: column; gap: 4px; align-items: flex-start; }
.gx-sbx-usedlink { font: inherit; font-size: 0.75rem; background: none; border: 0; color: inherit; cursor: pointer;
  padding: 4px 6px; border-radius: 6px; text-align: left; }
.gx-sbx-usedlink:hover { background: rgba(127, 127, 127, 0.1); }
.gx-sbx-playwrap video.gx-sbx-cardplayer { display: block; width: 100%; max-height: 32vh; border-radius: 10px; background: #000; cursor: pointer; }

/* the source */
.gx-sbx-cast { display: flex; flex-wrap: wrap; gap: 8px; }
.gx-sbx-char { border: 1px solid rgba(127, 127, 127, 0.24); border-radius: 999px; padding: 6px 12px; display: inline-flex; gap: 8px; align-items: baseline; cursor: pointer; }
.gx-sbx-char:hover { background: rgba(127, 127, 127, 0.08); }
.gx-sbx-charname { font-size: 0.8125rem; }
.gx-sbx-chstate { font-size: 0.6875rem; opacity: 0.6; }
.gx-sbx-src { display: flex; align-items: baseline; gap: 8px; padding: 4px 2px; }

/* the pill — the only always-visible affordance, flag-gated by mount */
.gx-sbx-pill {
  position: fixed; right: 16px; bottom: 78px; z-index: 70;
  font: inherit; font-size: 0.8125rem; padding: 8px 14px; border-radius: 999px; cursor: pointer;
  border: 1px solid rgba(127, 127, 127, 0.3);
  background: var(--gx-bg, rgba(20, 22, 26, 0.94)); color: inherit;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
}
.gx-sbx-pill:hover { border-color: rgba(127, 127, 127, 0.55); }

/* the head status line — the peek lip's voice; hidden on desktop (the cut header carries it there) */
.gx-sbx-headstatus { display: none; }

/* mobile (A4: the SHARED stage-sheet grammar — same 76px lip, same motion curve, same fall-back-to-
   peek-while-cooking rule; Back peels box→panel via the navStack overlay entry — never out) */
@media (max-width: 959px) {
  .gx-sbx-panel {
    position: fixed; inset: 0; z-index: 80; border-left: 0;
    transform: translateY(100%);                                  /* closed: fully off-screen (mounted while peeking) */
    transition: transform .26s cubic-bezier(.2, .7, .3, 1);       /* the stage sheet's exact motion */
    will-change: transform; overscroll-behavior: contain;
  }
  #gx-shell.gx-sbx-peeking .gx-sbx-panel { transform: translateY(calc(100% - 76px)); }  /* the lip */
  #gx-shell.gx-sandboxing .gx-sbx-panel { transform: translateY(0); }                    /* open */
  /* the grabber — the stage lip's affordance, same geometry */
  #gx-shell.gx-sbx-peeking:not(.gx-sandboxing) .gx-sbx-panel::before { content: ''; position: absolute; top: 7px; left: 50%; transform: translateX(-50%); width: 36px; height: 4px; border-radius: 2px; background: rgba(127, 127, 127, 0.45); z-index: 2; }
  /* in the lip: the ✕ hides (the whole lip is one act — expand); the tap target is the lip button */
  #gx-shell.gx-sbx-peeking:not(.gx-sandboxing) .gx-sbx-x { display: none; }
  #gx-shell.gx-sbx-peeking:not(.gx-sandboxing) .gx-sbx-pill { display: none; }          /* one affordance, not two */
  /* ⭐ the stage grammar's companion BLOCKER fix, converged (appShell.css gx-stage-peek #gx-chat):
     the lip must never occlude the composer — Send sits ABOVE it, always (audit HIGH) */
  #gx-shell.gx-sbx-peeking:not(.gx-sandboxing) #gx-chat { padding-bottom: 76px; }
  /* in the lip the head sits at the BOTTOM of the screen — the notch inset is a top-of-screen fact;
     without this the title/status slides out of the 76px window on notched phones (audit) */
  #gx-shell.gx-sbx-peeking:not(.gx-sandboxing) .gx-sbx-head { padding-top: 10px; }
  .gx-sbx-lipbtn { position: absolute; top: 0; left: 0; right: 0; height: 76px; z-index: 3; background: transparent; border: 0; cursor: pointer; }
  .gx-sbx-headstatus { display: inline; font-size: 0.75rem; opacity: 0.7; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
  .gx-sbx-head { padding-top: calc(10px + env(safe-area-inset-top, 0px)); }
  .gx-sbx-body { padding-bottom: calc(24px + env(safe-area-inset-bottom, 0px)); }
  .gx-sbx-box { padding-bottom: calc(16px + env(safe-area-inset-bottom, 0px)); }
  .gx-sbx-pill { bottom: calc(84px + env(safe-area-inset-bottom, 0px)); }
  /* ═══ C12 — ON A PHONE, PROJECT MODE OPENS FROM THE RIGHT (owner, 2026-07-28: "it opens from bottom, can
     it come from the right"). The workshop LIVES on the right on desktop, so it should arrive from the right
     on a phone too — one spatial idea instead of two.
     ⚠️ SCOPED TO PROJECT MODE ON PURPOSE. The bottom sheet also serves the legacy/chat-thread lane, which
     has shipped for months and is not part of this wave; changing its axis would be an unrequested change to
     working behaviour. Both idioms coexist, each in its own mode.
     The OPEN state is deliberately untouched (still inset:0, full screen), so every audited property of the
     open sheet — safe areas, the box, focus, Back peeling card→box→panel — carries over unchanged. Only the
     ENTRY AXIS and the resting strip differ. */
  #gx-shell[data-mode="project"] .gx-sbx-panel { transform: translateX(100%); }
  #gx-shell[data-mode="project"].gx-sandboxing .gx-sbx-panel { transform: translateX(0); }

  /* ⭐ C13 — THE EDGE TAB (owner: "make it less intrusive, similar to the hamburger on left but not the
     same"). NO RESTING STRIP. A visible slice of the panel — even 44px, even below the header — is a
     permanent column of chrome stealing width from the conversation on the narrowest screen there is, and
     it cost three separate faults to make merely tolerable. A small tab costs none of that: the phone keeps
     its full width until you ask for the workshop.
     A SIBLING OF THE BURGER, NOT ITS TWIN — same idea (a small button opens a drawer), deliberately
     different form: it hugs the RIGHT edge, which is the workshop's side at every screen size, and it is a
     quiet half-rounded tab rather than a floating square. Tokens only; nothing new invented. */
  .gx-sbx-tab {
    position: fixed; z-index: 70;
    right: 0; top: 50%; transform: translateY(-50%);
    width: 26px; height: 56px; padding: 0;
    display: flex; align-items: center; justify-content: center;
    font: inherit; font-size: 14px; line-height: 1;
    /* GEXI GREEN (owner: "give the tab a color the consumer won't miss"). It was a muted 0.72-opacity chip,
       which is the right weight for something you already know is there and the wrong weight for the ONLY
       door into the workshop on a phone. This is the EXACT fg/bg pairing the shipped brand button already
       uses (gx-header.css:50, the header's ＋) rather than a new colour — and --brand-500 carries its own
       light and dark values, so it stays legible in both themes with no palette of its own. */
    /* ⚠️ DARK GLYPH, NOT WHITE. White on this green measures 1.92:1 in dark theme and 2.54:1 in light —
       both under the 3:1 floor for a non-text UI control. `--on-brand` is now DEFINED (core.css + styles.css)
       rather than falling through to its #fff fallback, so this reads from the token and the fallback here
       is the same dark ink rather than white. Not `--ink`: that flips to near-white in dark theme, which is
       the bug wearing a token. Measured 7.50:1 on the light brand, 9.90:1 on the dark one. */
    color: var(--on-brand, #0f0f1a);
    background: var(--brand-500);
    border: 0; border-radius: 10px 0 0 10px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.28);                   /* lifts it off the transcript behind it */
    cursor: pointer;
    -webkit-tap-highlight-color: transparent; touch-action: manipulation;
  }
  .gx-sbx-tab:active { filter: brightness(0.92); }
  .gx-sbx-tab:focus-visible { outline: 2px solid var(--brand-500); outline-offset: 2px; }
  /* clear of the composer's own safe-area, so a short screen never puts it over Send */
  @media (max-height: 560px) { .gx-sbx-tab { height: 44px; } }

  /* the describe row: label + input stack on a phone instead of squeezing on one line */
  .gx-sbx-oprow.is-input { flex-wrap: wrap; }
  .gx-sbx-inputrow { flex: 1 1 100%; }
  .gx-sbx-descinput { flex: 1; min-width: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .gx-sbx-panel { transition: none; }
}

/* A4: the preview is the swipe surface — horizontal = compare takes, vertical stays native scroll.
   Native img drag is disabled both ways (it fired pointercancel before the swipe threshold — audit) */
/* B4.5 — the trim stepper's live length */
.gx-sbx-durnow { font-size: 0.8125rem; font-variant-numeric: tabular-nums; min-width: 3.5em; text-align: center; opacity: 0.85; }

.gx-sbx-preview { touch-action: pan-y; user-select: none; -webkit-user-select: none; position: relative; }
.gx-sbx-preview img { -webkit-user-drag: none; }

/* ═══ B2 — THE PLAYER (tap plays · scrubs · loops; native controls OFF — the swipe machine owns
   horizontal gestures, the slim bar's track seeks on TAP) ═══ */
.gx-sbx-playwrap { width: 100%; }
.gx-sbx-playwrap video.gx-sbx-player { display: block; width: 100%; max-height: 42vh; border-radius: 10px; background: #000; cursor: pointer; }
.gx-sbx-playwrap.is-audio audio.gx-sbx-player { display: none; }   /* audio: the kind glyph is the face, the bar is the control */
.gx-sbx-pbar { display: flex; align-items: center; gap: 8px; padding: 6px 2px 0; }
.gx-sbx-pbtn { font: inherit; font-size: 0.8125rem; line-height: 1; padding: 6px 8px; border-radius: 8px; border: 1px solid rgba(127, 127, 127, 0.28); background: transparent; color: inherit; cursor: pointer; }
.gx-sbx-pbtn:hover { background: rgba(127, 127, 127, 0.1); }
.gx-sbx-ptrack { flex: 1; height: 18px; display: flex; align-items: center; cursor: pointer; position: relative; }
.gx-sbx-ptrack::before { content: ''; position: absolute; left: 0; right: 0; height: 4px; border-radius: 2px; background: rgba(127, 127, 127, 0.28); }
.gx-sbx-pfill { position: relative; height: 4px; border-radius: 2px; background: var(--gx-accent, #4c9a72); z-index: 1; pointer-events: none; }
.gx-sbx-ptrack:focus-visible { outline: 2px solid rgba(110, 168, 254, 0.7); outline-offset: 2px; border-radius: 4px; }
.gx-sbx-ptime { font-size: 0.6875rem; opacity: 0.7; font-variant-numeric: tabular-nums; flex: 0 0 auto; }
.gx-sbx-fstill { position: absolute; right: 8px; bottom: 8px; z-index: 2; font: inherit; font-size: 0.8125rem; padding: 6px 8px; border-radius: 8px; border: 1px solid rgba(127, 127, 127, 0.35); background: rgba(20, 22, 26, 0.7); color: inherit; cursor: pointer; }

.gx-sbx-playwrap video.gx-sbx-filmplayer { display: block; width: 100%; max-height: 32vh; border-radius: 10px; background: #000; cursor: pointer; }
.gx-sbx-filmplay { margin-top: 8px; }
.gx-sbx-playerr { font-size: 0.75rem; opacity: 0.8; margin: 6px 2px 0; }

/* the card's muted hover/long-press preview — same footprint as the thumb, zero reflow */
.gx-sbx-hovervid { width: 100%; height: 100%; object-fit: cover; display: block; }

/* B2: in the cut, media LOST — flagged, never vanished (§5: weak pieces self-identify) */
.gx-sbx-mini.is-missing { outline: 1px dashed #d65454; outline-offset: -1px; opacity: 0.6; }
.gx-sbx-preview:focus-visible { outline: 2px solid rgba(110, 168, 254, 0.7); outline-offset: 2px; border-radius: 8px; }

/* ═══ A2 — THE PIECE BOX (opens OVER the shelf; the shelf stays mounted, scroll survives) ═══ */
.gx-sbx-open {
  position: absolute; right: 8px; bottom: 34px; z-index: 2;
  font: inherit; font-size: 0.6875rem; padding: 4px 10px; border-radius: 999px; cursor: pointer;
  border: 1px solid rgba(127, 127, 127, 0.4); background: var(--gx-bg, rgba(20, 22, 26, 0.92)); color: inherit;
}
.gx-sbx-card { position: relative; }
.gx-sbx-box {
  /* C5 — BOUNDED to rows 1-2 (was `position:absolute; inset:0`). This is the whole precondition for
     WAVE 4: a Box covering the foot would mean you could never speak to the piece you have open. */
  grid-row: 1 / span 2; grid-column: 1; z-index: 6;
  display: flex; flex-direction: column;
  background: var(--gx-bg, rgba(20, 22, 26, 0.99));
  overflow-y: auto; overscroll-behavior: contain; padding: 0 14px 24px;
}
.gx-sbx-boxhead { display: flex; align-items: center; gap: 10px; padding: 12px 0; position: sticky; top: 0; background: inherit; z-index: 2; }
.gx-sbx-back { font: inherit; font-size: 0.8125rem; background: none; border: 0; cursor: pointer; color: inherit; opacity: 0.75; padding: 4px 8px; border-radius: 8px; }
.gx-sbx-back:hover { opacity: 1; background: rgba(127, 127, 127, 0.1); }
.gx-sbx-boxtitle { flex: 1; min-width: 0; font-size: 0.9375rem; font-weight: 600; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gx-sbx-taketag { font-size: 0.75rem; opacity: 0.6; white-space: nowrap; }

.gx-sbx-preview { position: relative; max-width: 420px; margin: 0 auto 12px; width: 100%; }
.gx-sbx-durtag, .gx-sbx-captag {
  position: absolute; bottom: 8px; font-size: 0.6875rem; padding: 2px 8px; border-radius: 999px;
  background: rgba(0, 0, 0, 0.55); color: #fff; pointer-events: none;
}
.gx-sbx-durtag { left: 8px; }
.gx-sbx-captag { right: 8px; }
/* with a player mounted the tags would sit ON the control bar (confirmed visually — audit LOW):
   lift them above the bar's row and keep them click-through */
.gx-sbx-preview:has(.gx-sbx-playwrap) .gx-sbx-durtag,
.gx-sbx-preview:has(.gx-sbx-playwrap) .gx-sbx-captag { bottom: 46px; }

.gx-sbx-takes-row { display: flex; gap: 8px; overflow-x: auto; padding: 4px 0 10px; }
.gx-sbx-takechip {
  flex: 0 0 auto; width: 64px; border-radius: 8px; padding: 3px; cursor: pointer; position: relative;
  border: 2px solid rgba(127, 127, 127, 0.25); background: none; color: inherit;
}
.gx-sbx-takechip img { width: 100%; height: 40px; object-fit: cover; border-radius: 5px; display: block; }
.gx-sbx-takeglyph { display: flex; align-items: center; justify-content: center; height: 40px; opacity: 0.5; font-size: 1.1rem; }
.gx-sbx-takechip.is-current { border-color: var(--gx-accent, #4c9a72); }
.gx-sbx-takechip.is-cooking { opacity: 0.55; cursor: default; }
.gx-sbx-takenum { font-size: 0.6875rem; opacity: 0.7; }

.gx-sbx-undo {
  font: inherit; font-size: 0.8125rem; padding: 6px 14px; border-radius: 999px; cursor: pointer;
  border: 1px solid rgba(127, 127, 127, 0.3); background: none; color: inherit; margin-bottom: 10px;
}
.gx-sbx-undo:hover { background: rgba(127, 127, 127, 0.1); }

.gx-sbx-ops { display: flex; flex-direction: column; gap: 6px; }
.gx-sbx-oprow {
  display: flex; align-items: center; justify-content: space-between; gap: 10px; text-align: left;
  font: inherit; padding: 10px 12px; border-radius: 10px; cursor: pointer;
  border: 1px solid rgba(127, 127, 127, 0.2); background: none; color: inherit; width: 100%;
}
.gx-sbx-oprow:hover { background: rgba(127, 127, 127, 0.07); }
.gx-sbx-oprow.is-input { cursor: default; }
.gx-sbx-opmeta { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.gx-sbx-opname { font-size: 0.875rem; }
.gx-sbx-opreason { font-size: 0.75rem; opacity: 0.6; }
/* the credit chip — ON the control, before the act; free ops simply have none */
.gx-sbx-cost {
  font-style: normal; font-size: 0.6875rem; font-weight: 600; white-space: nowrap;
  padding: 2px 8px; border-radius: 999px;
  background: rgba(76, 154, 114, 0.18); color: var(--gx-accent, #6cc296);
}
.gx-sbx-inputrow { display: flex; gap: 8px; align-items: center; flex: 0 0 auto; }
.gx-sbx-btn {
  font: inherit; font-size: 0.8125rem; padding: 6px 12px; border-radius: 999px; cursor: pointer;
  border: 1px solid rgba(127, 127, 127, 0.3); background: none; color: inherit;
}
.gx-sbx-btn.is-primary { background: var(--gx-accent, #4c9a72); border-color: transparent; color: #fff; display: inline-flex; gap: 6px; align-items: center; }
.gx-sbx-btn:disabled { opacity: 0.45; cursor: default; }
.gx-sbx-descinput {
  font: inherit; font-size: 0.8125rem; padding: 7px 10px; border-radius: 8px; min-width: 180px;
  border: 1px solid rgba(127, 127, 127, 0.3); background: none; color: inherit;
}
