/* LATENTLAB — the lab UI v3
   One screen, no scroll. Near black, separation by tone. */

:root {
  --bg: #050505;
  --sunk: #0c0c0c;
  --raise: #141414;
  --raise-2: #1c1c1c;
  --ink: #EDE1D3;
  --muted: #8a847a;
  --dim: #55504a;
  --accent: #EE2324;
  --accent-text: #ff5248;
  --accent-sunk: #1a0f0f;
  --ok: #7ddc4e;
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --btn: 26px;   /* panel button height; the square arrow is one of these wide */
  /* RUN and the agent's send arrow are the two things you PRESS in this
     column, one above the other and both flush to its right edge, so
     they are one width rather than two that happen to be close. */
  --run-w: 70px;
}

*, *::before, *::after { box-sizing: border-box; }

html, body { height: 100%; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: Archivo, system-ui, sans-serif;
  font-size: 14px;
  line-height: 1.45;
}

/* This is an app screen, not a document: dragging across a header or a button
   should move nothing and highlight nothing. Selection is given back to the
   few places with words worth taking - what you type, what the agent says,
   and the prompt a result was made from, which keeps its own rule below. */
body {
  -webkit-user-select: none;
  user-select: none;
}
input, textarea, [contenteditable], .chat-line {
  -webkit-user-select: text;
  user-select: text;
}

img, video { display: block; max-width: 100%; }
p, h1, h2, h3 { margin: 0; }
button { font: inherit; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.disp {
  font-family: Oswald, Archivo, sans-serif;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.tiny {
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--muted);
}

/* ---- the whole broadcast is one screen ---- */
.screen {
  height: 100svh;
  overflow: hidden;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
}

/* ============================ THE CARD ============================ */

/* The header runs on a baseline grid. Every line box in it is --line tall and
   all three columns start at the same top, so each column's lines land on the
   same three rows no matter what size the type is:

     row 1   LATENTLAB              THEME / FORMAT      14:32
     row 2   GENERATIVE AI LAB     ... the theme ...     "
     row 3   LIVE                  values              ELAPSED

   Big type keeps the rhythm by taking a whole multiple of --line, which is why
   the clock is 2 rows rather than a size of its own. Nothing here uses a gap:
   spacing is the line box, or the grid stops being a grid. */
.card {
  display: grid;
  grid-template-columns: minmax(130px, 1fr) auto minmax(130px, 1fr);
  align-items: start;
  column-gap: 20px;
  padding: 12px 16px;

  --line: 19px;   /* the unit everything in the header snaps to */
  --brand: 18px;  /* LATENTLAB, and the bout, are the same size */
}
.card-id { display: flex; flex-direction: column; }
.card-id .disp { font-size: var(--brand); line-height: var(--line); }

.live {
  display: inline-flex; align-items: center; gap: 7px;
  height: var(--line); line-height: var(--line);
  font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent-text);
}
.live::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%;
  background: var(--accent); animation: pulse 3.4s var(--ease) infinite;
}
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.25; } }

/* Three tracks, not a row: the two fr tracks settle at the same width - the
   longer name's - so VS lands on the middle of the card whatever the two
   players are called. Centring the row instead puts VS off the centre line by
   half the difference between the names, which is 11px for these two. */
.card-bout {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: baseline;
  gap: clamp(10px, 1.8vw, 24px);
  padding-top: var(--line); /* row 2: level with the line under the name */
}
.card-bout > :first-child { justify-self: end; }
.card-bout > :last-child { justify-self: start; }
.card-bout span { font-size: var(--brand); line-height: var(--line); letter-spacing: 0; }
/* AND THE CROSS IS SET BIGGER THAN THE NAMES, which is the same correction
   the poster's billing needed and for the same reason: the x in a display
   face is a SMALL MARK IN A LARGE EM, so at an equal font-size it draws
   visibly smaller than the letters either side of it. Babe, 2026-10-20:
   "can we make the x a little bigger between artists at the top."

   THE ROW DOES NOT MOVE. `--line` is a fixed 19px and `.card-bout span`
   pins the line-height to it, so the line box is the same height whatever
   the font-size is - the font makes the mark, the line box makes the gap,
   which is the division the poster's own rule already draws. 22 against the
   brand's 18 is the ratio used there. */
.card-bout .vs { color: var(--accent); font-size: 22px; }

/* EITHER NAME IS A DOOR INTO THAT SEAT, the same door the camera tile is,
   and only the one you are not already in is pressable. The header sits in
   neither column, so `pointer-events: none` never reached it and nothing
   has to be switched back on here. */
body[data-role="a"] [data-seat="b"],
body[data-role="b"] [data-seat="a"],
body[data-role="spectator"] [data-seat] { cursor: pointer; }
body[data-role="a"] [data-seat="b"]:hover,
body[data-role="b"] [data-seat="a"]:hover,
body[data-role="spectator"] [data-seat]:hover { color: var(--accent-text); }

.card-right { display: flex; align-items: center; justify-content: flex-end; gap: clamp(14px, 2vw, 28px); }

/* ---- the way into what you set once ----
   NO SLAB AND NO OUTLINE AT REST, the way every other control in a header
   row here behaves: it lifts under the pointer instead. */
.card-gear {
  position: relative;
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  /* THE HEADER'S OWN LINE, NOT THE BUTTON'S NATURAL SIZE. `.card` is
     `align-items: start` and its height is whatever its tallest cell is,
     so a 30px gear took the solo header from 43px to 54 - and 43 is the
     height Babe asked for and kept ("i like this new height"). 19px is
     `--line`, the unit everything in this row already snaps to. */
  width: 26px; height: 19px;
  padding: 0; border: 0; background: none;
  color: var(--muted); cursor: pointer;
  transition: color 150ms var(--ease);
}
/* AND THE PRESS IS BIGGER THAN THE DRAWING. A 19px row is the right
   LAYOUT height and a poor target, so the hit area is a transparent
   overlay that reaches past it - fix the SHAPE rather than the size,
   which is what the keyframe diamond on the clip bench already does
   where two controls had to share one point. Nothing else is up here
   to collide with. */
.card-gear::before { content: ""; position: absolute; inset: -7px -6px; }
.card-gear:hover { color: var(--ink); }
.card-gear svg {
  width: 17px; height: 17px;
  fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
}
/* NOTHING CONNECTED YET, SAID IN COLOUR. The rail used to carry "Not
   connected" permanently and this is what replaces it - a door cannot
   report through itself. A dot rather than a movement, because this machine
   asks for reduced motion and a thing that needs noticing has to survive
   that; the title says the same in words. */
.card-gear.is-off::after {
  content: "";
  position: absolute; top: 3px; right: 3px;
  width: 6px; height: 6px;
  border-radius: 50%;
  background: var(--accent);
}
/* a dl ships with margin: 1em 0, which pushed these a whole 14px off row 1
   and inflated the card by 28px. The grid cannot work with it. */
.card-meta { display: flex; gap: clamp(12px, 1.6vw, 24px); margin: 0; }
.card-meta dt {
  font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--dim);
  line-height: var(--line); /* row 1, level with LATENTLAB */
}
.card-meta dd {
  margin: 0; font-size: 11px; text-transform: uppercase;
  line-height: var(--line); /* rows 2 and 3 */
}
/* the theme is the whole point of the round, so it reads at headline size */
.card-meta dd.disp { font-size: var(--brand); letter-spacing: 0; }

/* label on row 1, value on row 2 - the same shape as a Theme/Format pair */
.clock { display: flex; flex-direction: column; text-align: right; }
.clock .tiny { line-height: var(--line); }
.clock .disp {
  font-size: var(--brand);
  line-height: var(--line);
  font-variant-numeric: tabular-nums;
}
.clock .tiny { color: var(--dim); }

/* ============================ THE FLOOR ============================ */

.floor {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px;
  min-height: 0;
}

.side {
  min-width: 0;
  min-height: 0;
  background: var(--sunk);
  padding: 12px;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  gap: 4px;
}

.side-a .who { color: var(--accent); }

/* ---- the work, filling whatever is left ---- */

.work {
  position: relative;
  min-height: 0;
  background: #000;
  overflow: hidden;
}
/* Contain, not cover: the window is where you JUDGE a result, and a 2x2 sheet
   with its bottom row cropped off is not the result. Bars on the sides are the
   honest version - the floor behind is already black. */
.work > img, .work > video { width: 100%; height: 100%; object-fit: contain; }

.work-prog { position: absolute; left: 0; right: 0; bottom: 0; height: 3px; background: rgba(255,255,255,0.1); }
.work-prog i { display: block; height: 100%; background: var(--accent); }

/* The feed keeps its 16:9 and the name sits in its own row underneath, rather
   than floating over the player's face. */
/* The camera is the column's STRETCHER. Four cells of a fixed height sit
   under it, so whatever the panel has left over becomes picture rather
   than a hole at the floor - and the hole was 34px. The 16/9 is still the
   base size, which is what it falls back to where the column has no
   definite height of its own (the 2x2 at narrow widths, the wide view). */
.cam {
  position: relative;
  width: 100%;
  background: #0f0f0f;
  overflow: hidden;
  flex: 1 1 auto; min-height: 0;
  display: flex; flex-direction: column;
}
.cam img,
.cam video {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  flex: 1 1 auto; min-height: 0;
  object-fit: cover;
  object-position: center 22%;
}
/* A STILL FACE AND A CLIP ARE TWO ELEMENTS IN ONE BOX, so one of them is
   always `hidden` - and `hidden` is a UA `display: none`, which the rule
   directly above beats. Without this the tile stacks both. */
.cam img[hidden],
.cam video[hidden] { display: none !important; }
/* THE ACCOUNT CELL IS A BUTTON THE WHOLE WAY ACROSS, the way the theme cell
   is: a cog beside a word that is already the thing you want to change is a
   control for nothing. */
.cell-acct-btn,
.cell-invite-btn {
  display: flex; flex-direction: column; align-items: flex-start;
  gap: 2px; width: 100%;
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.cell-acct-btn:hover .rail-v,
.cell-invite-btn:hover .rail-v { color: var(--ink); }
/* THE LINK IS SELECTED ON THE WAY IN, so the selection is part of this
   panel rather than something a hand happened to do - and the browser's own
   blue is the one colour this screen does not have. Sand on near-black is
   the highlight the rest of the page already reads as "lit". */
#invite [data-inv-link]::selection {
  background: var(--ink); color: #0a0a0a;
}
.acct-on,
.inv-on { color: var(--accent-text); }
.acct-bad,
.inv-bad { color: var(--accent); }
.cell-acct .rail-v.is-refused { color: var(--accent); }

.cam-row {
  display: flex; align-items: center;
  background: #0a0a0a;
}
/* THE CAMERA LIVES INSIDE .cells, which is the thing the inert column
   switches off - so the door has to be switched back on explicitly or it
   is dead by inheritance and nothing says why. */
body[data-role="a"] .side-b .cam,
body[data-role="b"] .side-a .cam,
body[data-role="spectator"] .cam { pointer-events: auto; }

/* the other artist's camera is a door into their seat, so it says so */
body[data-role="a"] .side-b .cam,
body[data-role="b"] .side-a .cam,
body[data-role="spectator"] .cam { cursor: pointer; }
body[data-role="a"] .side-b .cam:hover .cam-tag,
body[data-role="b"] .side-a .cam:hover .cam-tag,
body[data-role="spectator"] .cam:hover .cam-tag { color: var(--ink); }
.cam-tag {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  line-height: 17px;
  font-size: 9px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted);
}

/* WHICH SEAT THIS TAB IS SITTING IN. With two windows open on one room,
   the two screens are the same screen until something says which is which
   - so the label under your own camera takes the colour this page uses for
   the thing that is live. The other side's hover stays --ink, a step short
   of it, or hovering their name would read as sitting in it. A spectator is
   in neither, and nothing lights. */
body[data-role="a"] .side-a .cam-tag,
body[data-role="b"] .side-b .cam-tag { color: var(--accent-text); }

/* Your own camera, on your own side - nobody gets a switch for anybody
   else's. Live, the feed is mirrored, the way a self-view always is. */
.cam-on {
  flex: 0 0 auto;
  display: inline-flex; align-items: center; justify-content: center;
  width: 22px; height: 17px; padding: 0;
  border: 0; background: none; cursor: pointer;
  color: var(--dim);
  transition: color 160ms var(--ease);
}
.cam-on svg { width: 11px; height: 11px; fill: none; stroke: currentColor; stroke-width: 1.8; }
.cam-on:hover { color: var(--ink); }
.cam.is-live .cam-on { color: var(--accent-text); }
.cam.is-live video { transform: scaleX(-1); }
/* refused, or no camera on the machine: the switch says so and stops saying it */
.cam.is-denied .cam-on { color: var(--accent); }

/* ---- tools ---- */

.tools {
  display: grid;
  grid-template-columns: minmax(112px, 148px) 1fr;
  gap: 4px;
  height: 292px;
  min-height: 0;
}

.cells { display: flex; flex-direction: column; gap: 3px; min-height: 0; height: 100%; }
.cell {
  display: flex;
  align-items: center;
  gap: 9px;
  padding: 8px 10px;
  /* A cell is a fixed height and the camera above takes the remainder. It
     used to be flex: 1 1 0, which split the whole column between four of
     them and turned the lit one into a slab; 40px then left the column
     ending short of the panel floor, so the four take a little more and
     the camera takes the rest. */
  /* 34px, AND THE TWO LAYOUTS WANT OPPOSITE NUMBERS. Here the cells are a
     column and the camera takes the remainder, so SHORTER cells make it
     taller: five at 44 left it 57px, which is what Babe caught - "i also
     noticed the web-cam view got reduced in height". Five at 34 give it
     107px, taller than the 104 it had when there were only four.

     In the narrow layout below, the camera SPANS the cell rows instead, so
     the arithmetic reverses and shorter cells would shrink it. That rule
     keeps 44. Babe, on which way to err: "lets make it taller... will be
     funner for the social aspect to see more of your collab partner" -
     which is the whole premise of the branch, so the camera wins ties. */
  /* 32px AND A SMALLER ICON, which is what paid for the sixth cell.
     Babe, 2026-10-21: "can we make the icons a little smaller and shave
     off like 2 px per button". Measured at 1280: six at 34 leave the
     camera 70px against the 107 five had; six at 32 with a 13px icon
     leave it 82. The arithmetic is 6px of camera per pixel off a cell. */
  flex: 0 0 auto; height: 32px;
  background: var(--raise);
  color: var(--muted);
  cursor: pointer;
  text-align: left;
  border: 0;
  transition: background 180ms var(--ease), color 180ms var(--ease);
}
.cell:hover { background: var(--raise-2); color: var(--ink); }

/* A MESH IN A THUMBNAIL, for the same reason and in the same shape: what a
   GLB has instead of a picture is a SHAPE, and the one thing every mesh on
   the staging bench has in common is the bounding box it gets framed to. A
   mesh became an asset on 2026-10-21, when the upload door stopped refusing
   anything that was not an image, a clip or a sound. */
.thumb-mesh {
  width: 100%; height: 100%;
  display: flex; align-items: center; justify-content: center;
  background: var(--raise); color: var(--muted);
}
.thumb-mesh svg {
  width: 40%; height: 40%;
  stroke: currentColor; fill: none; stroke-width: 1.4;
  stroke-linejoin: round;
}

/* A SOUND IN A THUMBNAIL. The reel and the result pane draw a mark
   rather than a picture, because an mp3 has none - the same waveform
   the asset grid has always used, so one sound reads one way
   everywhere. It fills its square like a picture would. */
.thumb-audio {
  width: 100%; height: 100%;
  display: flex; align-items: center; justify-content: center;
  background: var(--raise); color: var(--muted);
}
.thumb-audio svg {
  width: 38%; height: 38%;
  stroke: currentColor; fill: none; stroke-width: 1.6;
  stroke-linecap: round;
}
.cell svg { width: 13px; height: 13px; stroke: currentColor; fill: none; stroke-width: 1.6; flex: 0 0 auto; }
.cell-name { font-size: 10px; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; }
.cell-n { margin-left: auto; font-size: 10px; color: var(--dim); font-variant-numeric: tabular-nums; }
.cell.is-on { background: var(--accent); color: #0a0a0a; }
.cell.is-on .cell-n { color: rgba(0,0,0,0.55); }

/* The panel's own background is what every 3px gutter inside it shows -
   between tiles, between rounds, between the header cells - so it is the
   column's colour, not a lift. The cells are the lift; the space between
   them is the column showing through. */
.pipe { background: var(--sunk); min-width: 0; display: flex; flex-direction: column; min-height: 0; }


/* The header is a ROW DIVIDED INTO CELLS, like the rail along the floor -
   not a padded bar with buttons floating inside it. Each control is a cell
   of the row at its full height, and the 3px gap shows the seam behind. */
.pipe-head {
  display: flex; align-items: stretch; gap: 3px;
  padding: 0; min-height: 46px;
  background: var(--sunk);
  border-bottom: 2px solid var(--sunk);  /* the column, not a black seam */
  flex: 0 0 auto;
}
.pipe-head .filters { align-self: center; }
/* The chips share the row with four controls, and below about 1400px they
   do not all fit: they scroll inside their own cell rather than pushing RUN
   off the end of the panel, which is where they put it. */
.pipe-head { overflow: hidden; }
.pipe-head .filters { flex: 0 1 auto; min-width: 0; overflow-x: auto; scrollbar-width: none; }
.pipe-head .filters::-webkit-scrollbar { height: 0; display: none; }
/* The title says where you are AND is the way back out, because the header
   row has no space for a button that only ever means "up" and the title was
   already sitting there naming the place. The chevron is the whole of the
   affordance, so it only appears where there is somewhere to go. */
.pipe-title {
  /* the first cell of the row; 10px keeps the inset the header's own
     padding used to give it */
  padding: 0 10px;
  font-size: 14px; letter-spacing: 0.08em;
  display: inline-flex; align-items: center;
  background: none; border: 0;
  color: inherit; text-align: left;
  cursor: default;
}
/* Collapsed to nothing where there is no way back, so Rounds starts at the
   padding like every other header rather than sitting behind a hole. */
.title-back {
  width: 0; height: 13px; flex: 0 0 auto;
  fill: none; stroke: currentColor; stroke-width: 2;
  stroke-linecap: round; stroke-linejoin: round;
  opacity: 0;
}
.pipe[data-back] .pipe-title { cursor: pointer; }
.pipe[data-back] .title-back {
  width: 13px; margin: 0 5px 0 -2px; opacity: 0.5;
}
.pipe[data-back] .pipe-title:hover .title-back,
.pipe[data-back] .pipe-title:focus-visible .title-back { opacity: 1; }
.pipe-body { padding: 0; min-height: 0; flex: 1 1 auto; overflow: hidden; }

.pipe-head .spacer { flex: 1 1 auto; }
/* The four controls are the fixed part of this row, and the filters are the
   part that gives. Without this the assets view simply ran out of width: the
   spacer collapsed, there was still too much, and Run was SQUEEZED from 70px
   to 41 - which moved Assets 28.7px left of where it sits everywhere else.
   Run going dim rather than away was only half of holding the row still. */
.pipe-head > .pipe-btn { flex: 0 0 auto; }

/* ---- the window before anything is in it ----
   A PLACEHOLDER THAT GETS OUT OF THE WAY BY ITSELF. Five paths put real
   media into `.work` and only some clear it first, so the question is asked
   from the other side: if anything in here is not this image, this image is
   not needed. One rule, and no insert site has to remember it.

   AT FULL STRENGTH. It shipped at 0.5 on the argument that scenery should
   sit behind a result - Babe: "yeah make it 1". On a frame this dark the
   dim read as murky rather than as provisional, which is the wrong half of
   the message: the window is meant to look like a window. */
/* IT FILLS RATHER THAN FITS, which is the one place on this screen that is
   right. Every other picture in this window is WORK and must be seen whole -
   `object-fit: contain` exists for exactly that. This one is scenery, and
   measured at Babe's wide window the 1.877:1 frame in a 934x507 box drew
   934x497 and left a 5px black bar top and bottom: "i'm also seeing a thin
   black bar on the top and bottom... maybe we can just crop it a little bit
   for this splash image". Cover IS that crop, at every shape rather than
   at one. */
/* `.work > img.work-temp`, NOT `.work-temp`: the fit above it is
   `.work > img`, which scores 0,1,1 against a bare class's 0,1,0 and
   wins however it is ordered. Measured rather than assumed - the first
   go at this looked identical at 960 because the override never
   applied. Same family as `input.stream` losing to `input[type=text]`. */
.work > img.work-temp { object-fit: cover; }
.work:has(> img:not(.work-temp)) > .work-temp,
.work:has(> video) > .work-temp { display: none; }

.chain { display: flex; align-items: stretch; gap: 3px; }

/* A round is a square the result fills edge to edge. The number and the time
   sit on top of it, over a scrim, so nothing crops the picture. */
.link {
  position: relative;
  /* THREE ACROSS, AT ANY WIDTH - a SHARE, not a floor. Everything a side
     makes lands here now, so `flex: 1 1 0` with `min-width: 0` would make
     the tenth generation one tenth of a column wide; the first fix was a
     118px floor, then 110, then 138, each one measured against Babe's 960
     window. All three were wrong in the same way: a floor only decides what
     happens once the row is FULL, so on a wider window `1 1 0` let a fourth
     tile share the row and the count changed with the window. Babe, looking
     at a wide one: "can we go back to 3 selects. not 4".

     A fixed basis of one visible third settles it - 2 gaps inside the three,
     hence the 6px - and .chain is already overflow-x: auto, so the fourth
     and everything after it scroll. The tiles grow with the column the way
     the workflow tiles above them do, which is also where the wider aspect
     ratio comes from that Babe asked to keep. */
  flex: 0 0 calc((100% - 6px) / 3);
  display: flex; flex-direction: column;
  cursor: pointer;
}
/* the picture is its own square inside the round, so what the round IS can be
   written underneath it rather than over the top of it */
/* The frame IS the round now - it takes the whole height of the strip and
   the lines sit in its foot, so the row is three squares rather than three
   squares each with a caption hanging under it. */
/* A ROUND IS A CARD, the same card a workflow tile is. It used to be one
   square with the words written over its foot on a scrim, which is
   readable on a dark frame and unreadable on a bright one - and the
   brighter the result, the more a player wants to read what it was. The
   tile already solved this: picture inset by the same 10px the words are,
   words underneath, one grey box holding both. */
.link-frame {
  position: relative;
  flex: 1 1 auto; min-height: 0;
  background: var(--raise-2);
  border-radius: 5px;
  padding: 10px;
  display: flex; flex-direction: column; gap: 9px;
  overflow: hidden;
}
/* hover: a breath of light over the frame, nothing more */
.link-frame::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: rgba(237, 225, 211, 0.11);
  opacity: 0;
  transition: opacity 150ms var(--ease);
}
.link:hover .link-frame::before { opacity: 1; }

/* The top scrim went with the number it was there to keep readable: the
   round says which round it is in words now, under the picture. */

/* TWO LINES under the square, and they are the TILE'S two lines. A round is
   the same card a workflow tile is, so it says what it is the same way: the
   name in the lockup's face, one line of detail under it in the smaller,
   wider-tracked one. The third line - the wall-clock time it finished - went
   with the simplification: it was the fact nobody was reading, and dropping
   it buys the picture 8px it can use. */
.link-meta {
  display: flex; flex-direction: column; gap: 2px; min-width: 0;
}
/* THE TEXT BLOCK IS A FIXED HEIGHT, so every picture in the row is the same
   picture. Left to itself a card with a one-row receipt grew its thumbnail
   and the card beside it did not, and a strip of three staggered - the
   baselines wandered by 13px, which is the one thing a row of equal squares
   cannot do. THREE rows now, not four: one of kind, two of receipt. The
   header that took the other two said "SELECT 1", which numbers a slot in a
   game nobody is playing - Babe: "remove the select 1 2 3 headers so the
   dark image area is taller". */
.link-meta { min-height: 41px; }
/* WHAT IS IN THE SQUARE, in one small line: PNG, MP4, or the state while
   there is no file yet. This was the tile's 15px poster face carrying
   "SELECT 2 : QWEN EDIT 2509" over two rows - twenty pixels of picture
   spent on a slot number. The pair under the square is one size now and
   the hierarchy is COLOUR rather than scale: this line is the cream, the
   receipt under it is the grey. */
.link-what {
  font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
  line-height: 1.3; color: var(--ink);
  font-variant-numeric: tabular-nums;
  /* one line: the kind of a thing is one word, and anything longer here is
     a state word that reads fine clipped */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The name line stays the light cream whatever the round is doing. It used
   to turn red the moment you sent it, which is the receipt line's job - the
   word SUBMISSION is already down there, and a card saying it twice, once in
   a colour, is the thing to cut rather than keep. */
/* THE RECEIPT, set exactly as a tile's second line is - and allowed to wrap
   to two rows the same way, because "SUBMISSION : PNG 10/16/2026 3:16PM" is
   simply longer than a card is wide and the date is the half that would be
   lost to an ellipsis. */
.link-spec {
  font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
  line-height: 1.3; color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: normal; overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}

.link-thumb {
  position: relative; flex: 1 1 0; min-height: 0;
  background: #000; border-radius: 3px; overflow: hidden;
  display: grid; place-items: center;
}
.link-thumb img, .link-thumb video { width: 100%; height: 100%; object-fit: cover; }
.link-thumb span { font-size: 9px; color: #3f3b36; letter-spacing: 0.06em; text-transform: uppercase; }

/* All that is left up here is the cross, so it sits in the card's corner
   rather than in a row with a number that no longer exists. */
.link-top {
  position: absolute; z-index: 3; top: 13px; right: 13px;
  display: flex; align-items: flex-start; gap: 6px;
}
.link-name { display: none; }
/* Throwing a round away is a testing tool, so it is quiet until you go for it:
   the cross only arrives under the pointer, and it is red when it does. */
.link-x {
  margin-left: auto;
  background: none; border: 0; padding: 0; cursor: pointer;
  width: 16px; height: 16px; display: grid; place-items: center;
  color: var(--muted); opacity: 0;
  transition: opacity 150ms var(--ease), color 150ms var(--ease);
}
.link-x svg {
  width: 12px; height: 12px;
  fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round;
}
.link:hover .link-x { opacity: 1; }
.link-x:hover { color: var(--accent-text); }
.link-x:focus-visible { opacity: 1; }
.link-name {
  font-family: Oswald, Archivo, sans-serif;
  font-weight: 600;
  font-size: 15px;
  line-height: 1;
  letter-spacing: 0.04em;
  font-variant-numeric: tabular-nums;
}
/* the clock reads in the lines under the square now, not over the picture */
.link-t { color: var(--muted); font-variant-numeric: tabular-nums; }

.link.is-live .link-t { color: var(--accent-text); }

/* nothing to open yet, so it does not invite a click */
.link.is-queued { opacity: 0.4; cursor: default; }
.link.is-queued:hover::before { opacity: 0; }

/* the round currently showing in the window above */
.link.is-playing .link-frame { outline: 2px solid var(--accent); outline-offset: -2px; }
.link-x { background: rgba(5, 5, 5, 0.66); }

/* ============================ THE RAIL ============================ */

/* The rail is cells, on the same 4px grid as the floor above it: the game
   master, the commentary, the count. Anything new along the bottom takes a
   cell of its own rather than crowding in beside what is already there. */
/* Everything along the rail is one voice - one font, one size, labels and
   what they label and the commentary alike. Only the tone separates them. */
.rail {
  display: flex; align-items: stretch; gap: 4px;
  flex-wrap: nowrap; overflow: hidden;
  /* NO PADDING. The 4px up here was the floor's own grid gap carried into
     the rail - invisible, because the gap and the cells are the same black,
     so all it did was sit the words 2px below the middle of the band
     everybody reads as the footer. The cells ARE the footer now. */
  padding: 0;
  font: 500 11px/1.4 Oswald, sans-serif; letter-spacing: 0.08em;
}
/* the rail is the floor's black edge, the same black the header sits on - the
   cells divide it for whatever moves in later, they are not meant to be seen */
.rail-cell {
  display: flex; align-items: center; gap: 10px;
  flex: 0 0 auto; min-width: 0;
  height: 40px; padding: 0 14px;
  background: var(--bg);
}
.rail-k {
  flex: 0 0 auto;
  text-transform: uppercase;
  color: var(--dim);
}
.rail-v {
  color: var(--muted); white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.cell-say { flex: 1 1 auto; }
.cell-deal.is-working .rail-v { color: var(--accent-text); }
.btn {
  border: 0;
  background: var(--raise);
  color: var(--ink);
  padding: 9px 16px;
  font-size: 10.5px; letter-spacing: 0.08em; text-transform: uppercase;
  cursor: pointer;
  transition: background 180ms var(--ease);
}
.btn:hover { background: var(--raise-2); }
.btn-accent { background: var(--accent); color: #0a0a0a; }
.btn-accent:hover { background: #ff3b30; }

/* ====================== THE SCREEN ARRIVING ====================== */

/* Held until the cascade's first frame. Opacity rather than visibility or
   display, so everything that measures itself on load - the ticker measures
   its own line against its cell - still has a page to measure. */
html.is-intro .screen,
html.is-intro .modal { opacity: 0; }
/* Everything behind this query is off on a machine that asks for less motion
   - which is most of what this page does: the cascade, the modal, the ticker
   slide. `?intro=1` puts .wants-motion on the root and overrides the lot, so
   there is one switch for seeing the page as it is meant to move rather than
   a setting per effect. */
@media (prefers-reduced-motion: reduce) {
  html.is-intro:not(.wants-motion) .screen,
  html.is-intro:not(.wants-motion) .modal { opacity: 1; }
}

/* ============================ FALLBACKS ============================ */

/* short or narrow windows give up on one-screen and scroll instead */
@media (max-height: 700px), (max-width: 900px) {
  .screen { height: auto; overflow: visible; }
  .work { min-height: 240px; aspect-ratio: 16 / 9; }
}

@media (max-width: 1180px) {
  /* One column, and two rows that are exactly what they hold: with the
     rows left to share the 292px the cells row took more than its
     content and left a hole under the last cell. */
  .tools { grid-template-columns: 1fr; grid-template-rows: auto minmax(0, 1fr); }
  /* FIVE CELLS, IN THE SAME TWO ROWS FOUR USED. This was a 2x2 beside the
     camera, and the note here already recorded what a fifth did to it: it
     wrapped into a third row and the column clipped - measured again when
     TOOLS arrived, 91px of rail becoming 138px and the tile blurbs losing
     their bottom line. A THIRD COLUMN costs nothing instead: six slots for
     five cells, the camera still spans the pair, and the rail is the 91px
     it always was. The names are short enough - Image, Video, Edit, Post,
     Tools - that 119px a cell reads the same as 178px did. */
  .cells { display: grid; grid-template-columns: 92px 1fr 1fr 1fr; grid-auto-rows: 44px;   /* the camera SPANS these, so taller rows are a taller camera */ align-items: stretch; }
  /* AND IN SOLO THE FIRST COLUMN GOES WITH THE CAMERA THAT RESERVED IT.
     Hiding `.cam` left a 92px slot standing empty and the six cells flowed
     4 + 2 into a grid meant to be 3 + 3 beside a camera. Three equal
     columns is six slots for six cells, and each one is wider for it.

     THE ROWS STAY 44. Nothing is span-ning them any more, but the block
     itself is only 91px tall here - two rows and a gap, which is the rail
     height this layout exists to hold. There is no slack at this width to
     spend, which is the opposite of the wide one and the reason both are
     written out rather than shared. */
  body.is-solo .cells { grid-template-columns: repeat(3, 1fr); }
  /* the flex rule above is meaningless in a grid, and `stretch` is what
     fills the row - so it is put back explicitly rather than inherited */
  body.is-solo .cell { height: auto; }
  .cam { width: auto; grid-column: 1; grid-row: 1 / span 2; }
  .cell { flex: initial; min-width: 0; }
  .cell-name { overflow: hidden; text-overflow: ellipsis; }
}

@media (max-width: 900px) {
  .card { grid-template-columns: 1fr; gap: 10px; }
  .card-bout { display: flex; flex-wrap: wrap; }
  .card-bout, .card-right { justify-content: flex-start; flex-wrap: wrap; }
  .floor { grid-template-columns: 1fr; }
}

@media (prefers-reduced-motion: reduce) {
  html:not(.wants-motion) *,
  html:not(.wants-motion) *::before,
  html:not(.wants-motion) *::after { animation-duration: 1ms !important; transition-duration: 1ms !important; }
}


/* ============================ ASSETS TRAY ============================ */

.assets { margin-top: 3px; background: var(--raise); padding: 7px 8px 8px; }
.assets .tiny { font-size: 9px; margin-bottom: 5px; display: block; }
.assets-row { display: flex; gap: 3px; }
.assets-row img {
  width: 38px; height: 38px; object-fit: cover; background: #000;
}
/* A file being dragged over somewhere it can be dropped. The whole grid takes
   a drop, not only the + tile, because aiming at a 60px square with a file in
   hand is worse than the dialog this exists to avoid. */
.asset-grid.is-over,
.asset-slot.is-over {
  outline: 1px solid var(--accent);
  outline-offset: -1px;
  background: var(--accent-sunk);
}

.asset-slot {
  width: 100%;
  aspect-ratio: 1 / 1;
  background: #232323;
  display: grid; place-items: center;
  /* THE + IS THE ONLY INVITATION ON AN EMPTY GRID, so it is drawn like one
     - Babe, 2026-10-20: "can we make the plus bigger and bold for the asset
     upload icon." At 22px regular it was a faint mark in the middle of a
     dark square, which reads as a placeholder rather than as a thing to
     press, and on a fresh column it is the ONLY thing to press. The tile
     does not change size: the mark inside it does.

     THE WEIGHT IS SAFE TO SET HERE because the one other thing this square
     ever says - the failed-upload line - carries a `font` SHORTHAND, which
     resets the weight and the size both. */
  color: var(--muted); font-size: 30px; font-weight: 700; line-height: 1;
  cursor: pointer;
  transition: background 150ms var(--ease), color 150ms var(--ease);
}
.asset-slot:hover { background: #2e2e2e; color: var(--ink); }
/* a file on its way up, and a file that did not make it */
.asset-slot.is-busy { background: #2e2e2e; color: var(--ink); }
.asset-slot.is-bad {
  color: var(--accent);
  font: 500 10px/1.3 "Oswald", sans-serif;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: center;
  padding: 6px;
}

/* ============================ THE BOX, AS A MODAL ============================ */

.modal {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: grid;
  place-items: center;
  padding: 24px;
  /* No dim: the room behind stays readable, which matters when the thing
     being announced landed on both columns at the same second. The panel
     lifts off it with a shadow instead. */
  background: transparent;
  animation: modal-in 260ms var(--ease) both;
}
.modal.is-gone { display: none; }

/* The lockup is a button, and has to stop looking like one. */
.card-home {
  border: 0; background: none; padding: 0; margin: 0;
  color: inherit; text-align: left; line-height: inherit;
  cursor: pointer;
}
.card-home:hover { color: var(--accent-text); }
/* EXCEPT ALONE, WHERE THE ACCENT IS ALREADY SPOKEN FOR. Babe: "remove
   the red hover of latentlab on the logo". In solo the wordmark reads
   LATENTLAB SOLO with SOLO in that exact colour, so hovering turns the
   name the same red as the mode and the two collapse into one word -
   the hover destroys the only distinction the line is making. The
   cursor stays, so the door still says it is a door. */
body.is-solo .card-home:hover { color: inherit; }

/* ---- the theme, and its door ----
   The whole cell is the button: the word you want to change is already
   sitting there, so a cog beside it would be a control for nothing. It
   wears no button chrome at rest and lifts under the pointer, the way a
   cell in the floor does. */

.cell-theme {
  display: flex; align-items: center; gap: 10px;
  flex: 1 1 auto; min-width: 0;
  height: 100%; padding: 0; margin: 0 -4px 0 0;
  border: 0; background: none; color: inherit; font: inherit;
  text-align: left; cursor: pointer;
  transition: color 150ms var(--ease);
}
.cell-theme:hover .rail-k { color: var(--muted); }
.cell-theme:hover .said { color: var(--ink); }

/* WHERE YOU WRITE ONE INSTEAD OF GENERATING IT, and it needs air above it.
   `.modal-copy` has `margin: 0`, so the sentence offering the choice sat
   hard against the box that is the second half of it - Babe: "add some
   space below the 'if you already know'". 16px is the gap the kicker above
   the seats already uses, so the note has one spacing step rather than
   two. */
.theme-write { display: block; margin: 16px 0 14px; }
.theme-write input {
  width: 100%; height: 38px; padding: 0 12px;
  border: 0; background: #232323; color: var(--ink);
  font: 400 13px/1 Archivo, sans-serif;
}
.theme-write input::placeholder { color: var(--muted); }
.theme-write input:focus { outline: 2px solid var(--accent); outline-offset: -2px; }

.cell-deal { gap: 6px; }
.cell-deal button {
  border: 0; cursor: pointer; height: 26px; padding: 0 12px;
  background: var(--raise-2); color: var(--muted);
  font: 500 10px/1 Oswald, sans-serif; letter-spacing: 0.08em; text-transform: uppercase;
  white-space: nowrap;
  transition: background 150ms var(--ease), color 150ms var(--ease);
}
.cell-deal button:hover:not(:disabled) { background: var(--accent); color: #0a0a0a; }
.cell-deal button:disabled { color: #3f3b36; cursor: default; }


/* ---- the empty seat ----
   THE COLUMN NOBODY IS SITTING IN SHOWS YOUR WORK instead of a set of
   controls that cannot be pressed. It takes the hidden side's grid slot, so
   both columns keep the width every panel was tuned at - which is the whole
   reason this is a swap rather than a layout. */
.solo {
  min-width: 0; min-height: 0;
  background: var(--sunk);
  /* NO PADDING ON TOP, because the head belongs on the column's own edge
     the way the panel's header does - measured 55px against the section's
     43 and reading as though it had slipped down. The inset the
     conversation wants is given to the conversation. */
  padding: 0 12px 12px;
  display: flex; flex-direction: column; gap: 4px;
}
/* `.side` and `.solo` both set a real `display`, and an author style beats
   the UA sheet's `[hidden]` whatever the specificity. */
.solo[hidden], .side[hidden] { display: none !important; }

/* ---- the head on the empty seat ----
   A ROW, DIVIDED: what this column is on the left, who is answering on the
   right. Two cells rather than a label and a control crowded together, which
   is the rule the rail and the panel header both follow. */
.solo-head {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 4px;
  /* THE PANEL HEADER'S OWN HEIGHT, so the two columns start on one line
     rather than nearly on one - which is the kind of 3px that reads as a
     mistake rather than as a difference. */
  min-height: 46px;
}
.solo-who {
  flex: 0 0 auto; padding: 0 2px;
  display: inline-flex; align-items: center;
  /* THE PANEL'S OWN FONT, A STEP UP IN SIZE. `.pipe-btn` is 10px/700 in a
     row holding five controls; this is the head of a column holding one
     thing, so it can say so. Babe: "make the agent bigger with the icon
     like in the other panel. sme font." */
  font: 700 13px/1 Oswald, sans-serif;
  letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--ink);
}
/* AND THE ICON AT THE SCALE OF THE WORD, which is the rule the panel
   already states for the same mark: with no slab behind it the icon is half
   of what makes this read as the agent rather than as a label. */
.solo-who svg {
  width: 15px; height: 15px; margin-right: 7px;
  fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
}
.agent-pick { flex: 1 1 auto; min-width: 0; display: flex; }

/* IT IS NOT `.model-btn`, AND THAT IS DELIBERATE. The look is the same on
   purpose - one menu grammar on this screen - but the panel's handlers all
   resolve their row with `closest('[data-model]')` and would throw on null
   for a button of this class living outside one. Twenty lines of CSS is a
   better trade than a picker that reaches into the panel's menu logic. */
.agent-btn {
  flex: 1 1 auto; min-width: 0;
  display: flex; align-items: center; gap: 7px;
  padding: 6px 7px;
  background: none; border: 0; cursor: pointer;
  /* THE SAME SIZE AS THE WORD BESIDE IT, with the ACCENT carrying the
     difference. Babe: "make the model the same size font and in orange
     color". At 13 and 11 the name read as a caption on the label; at one
     size they are two facts of equal weight - what this column is, and who
     is in it - which is the reel card's rule exactly: hierarchy by colour
     rather than by scale. */
  font: 700 13px/1 Oswald, sans-serif;
  letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--accent-text); text-align: left;
  transition: color 150ms var(--ease);
}
/* AND HOVER IS THE AFFORDANCE, now that the triangle is gone: it goes to
   full ink rather than to the accent it already wears. */
.agent-btn:hover { color: var(--ink); }
.agent-val {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* NO TRIANGLE. Babe: "we can prob remove the arrow". It earns its place on
   SELECT MODEL, where opening the list IS what the row is for; here the row
   exists to SAY who is answering and the press is the rarer act, so a
   permanent marker spends the head's one piece of punctuation on the less
   important half. The hover going to accent is the affordance. */

.agent-list {
  position: fixed; z-index: 60;
  display: flex; flex-direction: column; gap: 1px;
  background: var(--sunk);
  padding: 1px;
  overflow-y: auto; scrollbar-width: none;
  box-shadow: 0 10px 30px rgba(0,0,0,0.6);
}
.agent-list::-webkit-scrollbar { width: 0; height: 0; display: none; }
.agent-list[hidden] { display: none; }
.agent-opt {
  display: flex; align-items: center; gap: 8px;
  padding: 8px 9px;
  background: var(--btn); border: 0; cursor: pointer;
  font: 500 11px/1 Oswald, sans-serif;
  letter-spacing: 0.06em;
  color: var(--muted); text-align: left;
}
.agent-opt:hover { background: var(--raise-2); color: var(--ink); }
.agent-opt.is-on { color: var(--ink); }
.agent-opt-name {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  text-transform: none;
}
/* OPEN OR BILLED, which is the fact that decides whether a question costs
   anything - and the reason the open-weights row is worth naming at all in
   a build somebody else is running. */
.agent-opt-v {
  flex: 0 0 auto;
  font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--dim);
}
.agent-opt:hover .agent-opt-v, .agent-opt.is-on .agent-opt-v { color: var(--muted); }

/* THE ASSISTANT TAKES THE REST OF THE COLUMN. In its own column it is `absolute;
   inset: 0` over the window, which is what `.side.is-asking .chat` is
   written for; standing here it is simply the rest of the column. */
.solo-chat { flex: 1 1 auto; min-height: 0; display: flex; }
.solo .chat {
  position: static; display: flex;
  flex: 1 1 auto; min-width: 0; min-height: 0;
}

/* WHILE YOU ARE ALONE THE HEADER LOSES ITS BILL AND ITS CLOCK. Two names
   across the top name nobody, and an elapsed hour is a rule about a match
   rather than about working. Both come back with the second artist. */
body.is-solo .card-bout { display: none; }

/* AND THE RIGHT-HAND CELL STAYS IN THE THIRD COLUMN. `.card` is three
   columns and the BILL is the middle one, so hiding the bill let that
   cell fall into the gap and the gear sat dead centre of a 1600px
   header. Grid placement is implicit until something is missing, which
   is exactly when it stops being what you meant.

   THIS RULE USED TO BE THE SECOND HALF OF THE ONE ABOVE, and editing it
   alone LEFT THE COMMA: `.card-bout` silently became a selector for
   `grid-column: 3` and stopped being hidden at all, so the bill came
   back on a solo screen and the header went 43px to 94. Nothing threw,
   and a measurement of the GEAR looked merely odd. Split a
   comma-separated selector deliberately or not at all. */
body.is-solo .card-right { grid-column: 3; }

/* THE CLOCK GOES, NOT THE CELL IT IS IN. This hid the whole right-hand
   cell, which was right while the clock was the only thing in it - the
   gear lives there now and has to outlive the mode that needs it most. */
body.is-solo .card-right .clock { display: none; }

/* AND THE WORDMARK STANDS DOWN TO ONE LINE. Babe: "just keep the logo / one
   line too... latentlab solo in another color or something." The accent is
   the TEXT weight of it rather than the signal red: this names what the
   screen is, it does not announce anything. */
.card-solo { display: none; }
body.is-solo .card-full { display: none; }
body.is-solo .card-solo { display: inline; }
.card-solo em { font-style: normal; color: var(--accent-text); }
/* LIVE IS AN ANNOUNCEMENT, and there is nobody it is being made to. */
body.is-solo .card-id .live { display: none; }

/* AND THE FLOOR GOES WITH THEM, FOR NOW. Babe, 2026-10-23: "lets remove the
   bottom footer for now". Two of its three cells are about somebody who is
   not coming in this mode - INVITE hands out the other seat and THEME deals
   a round to two people - and ACCOUNT is a readout of a key `/start` already
   takes at the door. HIDDEN rather than deleted, because collab wants all
   three back and this is the rule that has to undo itself. */
body.is-solo .rail { display: none; }

/* AND THE PANEL STOPS OFFERING A DOOR TO A ROOM YOU ARE STANDING IN. Babe:
   "remove it from the other panel actually." With the assistant in a column
   of its own it is permanently on screen, so AGENT in the panel's header
   opens what is already open. Solo only: in collab the chat lives inside
   `.work` and that button is the only way into it. */
body.is-solo .pipe-ask { display: none !important; }

/* AND THERE IS NO SHELF TO BORROW FROM. Babe: "remove artist b from assets
   panel". The pair exists so you can browse the OTHER artist's library
   without moving their screen - with nobody in that seat it names a person
   who is not there, and this file's own rule for the watcher case covers
   why that is worse than absent: a chip reporting nothing reads as a broken
   screen rather than as a private one. The separator goes with it, or the
   kind chips end on a gap to nowhere. */
body.is-solo .chip-src,
body.is-solo .chip-gap { display: none !important; }

/* AND THE CAMERA GOES, BECAUSE IT IS ADDRESSED TO SOMEBODY WHO IS NOT
   THERE. Babe: "we dont really need web-cam icon for solo mode... we dont
   really even need the artist / cam spot tbh".

   A self-view is the one control here whose whole audience is other people:
   the other artist's column and the capture URL. Alone it is a mirror - and
   it is a mirror that costs a webcam PERMISSION, which in a tool somebody
   else is self-hosting is a real ask paid for nothing. The tag goes with
   it: "Artist A" distinguishes a seat from the other seat, and there is no
   other seat.

   HIDDEN, not deleted, and the stream is never started anyway - `cam-on` is
   the press that asks for the device, so with no button there is no prompt.
   Collab and the event both want all of this back. */
body.is-solo .cam { display: none !important; }

/* AND THE CELLS TAKE THE ROOM IT WAS HOLDING. Babe: "right so now we can
   make the buttons bigger / taller basically... a little bigger on the icon
   and font".

   MEASURED: with the camera gone there were 85px of dead column under TOOLS
   at every width from 1280 up, because a cell is a fixed 32px and the
   camera was what took the remainder. Handing the column back to the cells
   is what this did BEFORE the camera needed it - and the "turned the lit
   one into a slab" note that ended that arrangement was FOUR cells
   splitting the space, where six at 46px is simply a comfortable button. */
body.is-solo .cell { flex: 1 1 0; height: auto; min-height: 0; }

/* A BIGGER MARK AND A BIGGER WORD, now that there is a button under them to
   carry it. 13 to 16 and 10 to 12 - the same step the agent head took, and
   for the same reason: at this size the icon is half of what the control
   is, rather than a decoration beside a label. */
body.is-solo .cell svg { width: 16px; height: 16px; }
/* 14px, WHICH IS THE PANEL TITLE'S SIZE, AND THAT IS THE WHOLE REASON.
   Babe: "should prob see if we can line up the first button with the
   header. its close". Measured: the BOXES already agreed to the pixel -
   first cell 596 to 642, `.pipe-head` 596 to 642 - and the WORDS did not,
   625 against 628. Two different sizes centred in the same height put
   their baselines in different places, so matching the size aligns them by
   construction rather than by a nudge somebody has to maintain. */
body.is-solo .cell-name { font: 600 14px/1.45 Oswald, sans-serif;
  letter-spacing: 0.08em; }

/* THE TITLE STAYS BRIGHT, AND BABE'S REASON IS BETTER THAN MINE WAS.
   Asked whether it could take the cells' colour, it could - measured, the
   two were already identical in every respect but that one: Oswald, 14px,
   600, 1.12px tracking, uppercase. Tried, and reverted: "i think its
   better the other way sorry... because the hovers light up the text and i
   like the lighter headers on the sections".

   WHICH IS THE ARGUMENT. A cell goes to full ink on hover, so a title
   sitting at the cells' RESTING colour means the thing under your pointer
   outshines the heading of the section it is in - the hierarchy inverts
   exactly when you are using it. A section header has to start above the
   controls it is heading.

   WHAT WAS ACTUALLY WRONG WAS THE WORD. Babe: "i think its just the word
   OUTPUT that maybe bugs me... or it should have a forward slash before it
   or something" - so the title is MARKED as a heading rather than dimmed
   into one. The slash is punctuation, in the quiet colour, and the word
   keeps the light it had. It is a `::before`, so nothing can write it into
   the title text that live.js replaces with a workflow name. */
body.is-solo .pipe-title::before {
  content: "/";
  margin-right: 6px;
  color: var(--dim);
  font-weight: 400;
}

/* AND ONLY ON THE RESTING ONE. Babe: "remove the / from the other
   button headers". The mark says THIS IS A HEADING, which is true of
   OUTPUT and false the moment you have navigated into something - there
   the title is a path with a chevron that already means "back", so a
   slash in front of it is a second piece of punctuation competing to
   say where you are. `[data-back]` is the attribute that already knows
   there is somewhere to go, so the exception keys on the same fact the
   chevron does rather than on a list of titles. */
body.is-solo .pipe[data-back] .pipe-title::before { content: none; }

/* ---- taking a seat, from the note you land on ---- */

.join {
  display: flex; flex-wrap: wrap; gap: 4px; justify-content: center;
  margin-top: 10px;
}
.join-seat { flex: 1 1 auto; font-size: 9.5px; padding: 12px 10px; }
/* AN ANCHOR IS NOT A BUTTON, and the tell is a word sitting high in its
   box. A button centres its label in its content box; an anchor with the
   same padding puts a LINE BOX there instead, so the gap under the word is
   the line's descender space and the gap above it is not - which is
   exactly the two or three pixels that made these read as too tall. A
   flex box centres it the way the button does, and the height is stated
   so a link and a button in one row cannot differ at all. */
a.join-seat {
  display: inline-flex; align-items: center; justify-content: center;
  text-decoration: none; text-align: center; line-height: 1;
}
.join-seat { min-height: 38px; }  /* one height for a link and a button alike */
/* The note now has two rows of buttons that do different things - take a
   seat, or look at a screen - and two unlabelled rows is a guess. */
.join-k {
  margin: 16px 0 0;
  font: 600 9px/1 Archivo, sans-serif; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--dim);
}
.join-k + .join { margin-top: 7px; }
/* Six screens in two even rows. The seats row stays a flex row - four
   items of different word lengths look right sharing a line; six in a
   wrapping row do not, because the last one stretches across the bottom
   on its own. */
.join-grid {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 4px;
}
@keyframes modal-in { from { opacity: 0; } to { opacity: 1; } }  /* the ground it scales up from */

.modal-panel {
  width: min(440px, 100%);
  box-shadow:
    0 0 0 1px rgba(255, 255, 255, 0.05),
    0 30px 70px -20px rgba(0, 0, 0, 0.95),
    0 0 170px 70px rgba(0, 0, 0, 0.8);
  background: #101010;
  padding: 20px;
  text-align: center;
  /* It scales up into place rather than popping on: 0.985 over 320ms was so
     close to no move at all that it read as an appearance, which is the one
     thing a panel landing on a live round should not do. */
  animation: panel-in 440ms var(--ease) both;
}
@keyframes panel-in {
  from { opacity: 0; transform: scale(0.86); }
  to   { opacity: 1; transform: scale(1); }
}

.modal-label { color: var(--accent-text); margin-bottom: 14px; }
.modal-art { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; background: #000; }
.modal-title { font-size: 26px; line-height: 1.02; margin: 16px 0 10px; }
/* the note is the first thing read, so it takes the panel's full width and
   sits a step up from the muted tone the rest of the UI talks in */
.modal-copy { font-size: 13px; color: #b5aea3; margin: 0; }
.modal-acts { display: flex; gap: 3px; margin: 20px 0 14px; }
.modal-acts .btn { flex: 1 1 0; padding: 15px 10px; }
.modal-foot { color: var(--dim); font-size: 9.5px; }

/* ---- the vote ----
   Two frames at the size a comparison needs, side by side, each one a
   button. No tally anywhere on it: the only thing that ever comes back is
   that the press was counted, so the screen's whole job is the pair. */

.vote-panel { width: min(760px, 100%); }
.vote-pair {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 3px; margin: 18px 0 14px;
}
.vote-pick {
  display: flex; flex-direction: column; gap: 4px;
  border: 0; cursor: pointer; text-align: left;
  background: var(--raise-2); color: var(--ink);
  padding: 10px;
  transition: background 150ms var(--ease);
}
.vote-pick img {
  width: 100%; aspect-ratio: 16 / 9; object-fit: cover;
  background: #000; border-radius: 3px;
  outline: 2px solid transparent; outline-offset: -2px;
  transition: outline-color 150ms var(--ease);
}
.vote-pick:hover { background: #232323; }
/* the light stroke, so hovering and HAVING VOTED do not say the same
   thing in the same colour */
.vote-pick:hover img { outline-color: var(--ink); }
/* THE NAME IS THE ONLY WORD ON THE CARD. What it was made with is a
   question for the scorecard; here the question is which picture, and a
   second line of model names under two photographs is just something else
   to read while you decide. It sits in a band of its own under the
   picture, centred in it, so the two cards agree whatever the name
   lengths are. */
.vote-who {
  flex: 1 1 auto;
  display: flex; align-items: center; justify-content: center;
  min-height: 42px; padding: 0 4px;
  font: 600 20px/1.1 Oswald, sans-serif; letter-spacing: 0.03em;
  text-transform: uppercase; text-align: center;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* THE ONE YOU PRESSED IS THE WHOLE CELL. An outline round the picture is
   a hover in a different colour; a card that has gone solid is a thing you
   DID. The name goes dark on it and gets bigger, and the model name goes
   altogether - once you have voted, what it was made with is not what the
   card is saying any more. This mark is for YOU: it is what you did, never
   what anybody else did. */
.vote-pick.is-picked { background: var(--accent); }
.vote-pick.is-picked img { outline-color: transparent; }
.vote-pick.is-picked .vote-who { color: #141414; }
/* ONCE IT IS CAST THE PAIR STOPS INVITING A PRESS - and the card you
   pressed is exempt, because your pointer is STILL ON IT the moment you
   vote. Written without the :not() these three outrank the picked rules on
   ID specificity and paint the hover straight back over them: the cell
   loses its orange, the outline comes back, and the only thing left of
   the vote is a dark name on a dark card. It looks right to a test that
   clicks without a cursor and wrong to every human being. */
#vote.is-done .vote-pick { cursor: default; }
#vote.is-done .vote-pick:not(.is-picked):hover { background: var(--raise-2); }
#vote.is-done .vote-pick:not(.is-picked):hover img { outline-color: transparent; }
#vote.is-done .vote-pick.is-picked,
#vote.is-done .vote-pick.is-picked:hover { background: var(--accent); }
#vote.is-done .vote-pick.is-picked img,
#vote.is-done .vote-pick.is-picked:hover img { outline-color: transparent; }

/* the rail's cell, which appears for the length of a vote and says the
   least a cell can say */
.rail-vote {
  border: 0; cursor: pointer; height: 24px; padding: 0 12px;
  background: var(--raise-2); color: var(--ink);
  font: 500 10px/1 Archivo, sans-serif; letter-spacing: 0.06em;
  text-transform: uppercase; white-space: nowrap;
  transition: background 150ms var(--ease), color 150ms var(--ease);
}
.rail-vote:hover { background: var(--ink); color: #0a0a0a; }
.rail-cell[hidden] { display: none !important; }

/* ---- the echo ----
   A watcher sees WHICH control was pressed by the control saying so. It is
   a 700ms wash of the accent over whatever it is put on - a cell, a model
   row, an input square - and it is colour only, no movement, so it reads
   the same for somebody who has asked for less of that. The class is
   removed and re-added by the page so the same control pressed twice
   flashes twice. */
.is-echo {
  animation: echo 700ms var(--ease) 1;
}
@keyframes echo {
  0%   { box-shadow: inset 0 0 0 2px var(--accent); }
  70%  { box-shadow: inset 0 0 0 2px var(--accent); }
  100% { box-shadow: inset 0 0 0 2px transparent; }
}
/* THEY ARE WRITING, and that is the whole message. The words themselves
   reach the room when Run is pressed - a live prompt box would hand the
   other player the round. */
.run-prompt.is-writing::placeholder { color: var(--accent-text); }

/* ---- watching, not playing ----
   Two controls go altogether, because they would invite a press that can
   never happen: the button that spends a move, and the tile that brings a
   file in. */
body[data-role="spectator"] .pipe-run,
body[data-role="spectator"] .pipe-submit,
body[data-role="spectator"] .asset-slot,
body[data-role="spectator"] .cam-on { display: none !important; }

/* The rest STAYS, because a watcher is watching the tool being used - the
   cells, the panel, the grid are what the player is doing and that is the
   show. They simply stop answering: no hover, no cursor, no press. The
   capture guard in the page is the other half of this, for the handlers
   that live on the document; this half is what stops a dead control
   lighting up under the pointer.

   The ROUNDS strip is deliberately not in this list. It is the one thing a
   spectator may press, because looking at a round is watching. */
body[data-role="spectator"] .cells,
body[data-role="spectator"] .pipe-head,
body[data-role="spectator"] .cam-row,
body[data-role="spectator"] .asset-grid,
body[data-role="spectator"] .list,
body[data-role="spectator"] .run,
body[data-role="spectator"] .chat {
  pointer-events: none;
}

/* ---- the other side of the room ----
   On the competitive branch a player was blind to the other column, so its
   controls were hiding nothing and reporting nothing, and they were taken
   away. Here the mirror runs both ways: EDIT lights up over there, the model
   goes selected, the square fills. The chrome follows the mirror, so it all
   comes back - a control that is showing you something has earned its place.

   What it does not do is answer a press. That column is a REFLECTION, and
   driving it from here would move the picture without moving the person, so
   the next thing they did would snap it back. The capture guard in the page
   is the half that stops the press; this is the half that stops a dead
   control lighting up under the pointer.

   Live over there is only what you TAKE from: the rounds, their asset grid,
   and the result itself - a frame you can drag, use or save into your own
   work the moment it lands. */
body[data-role="a"] .side-b .cells,
body[data-role="b"] .side-a .cells,
body[data-role="a"] .side-b .pipe-head,
body[data-role="b"] .side-a .pipe-head,
body[data-role="a"] .side-b .run-form,
body[data-role="b"] .side-a .run-form,
body[data-role="a"] .side-b .chat,
body[data-role="b"] .side-a .chat {
  pointer-events: none;
}

/* ---- A REFLECTION LOOKS LIKE ONE ----
   This block used to HIDE the other column's three header buttons. Babe
   asked for that as a preview - "can you just hide the others buttons for
   now so i can preview what it looks like" - and sitting in seat B found
   the thing it was always going to produce: TWO TREATMENTS FOR ONE
   SITUATION. "i can't click on their assets button", and "also their agent
   button disappeared". Both were deliberate, both were correct, and that is
   exactly why it read as broken: half the dead controls ignored you and the
   other half were not there.

   SO THERE IS ONE TREATMENT NOW, AND IT IS A VOCABULARY THE PAGE ALREADY
   SPEAKS. `.pipe[data-needs-model] .pipe-run` has said "here, but not for
   pressing" since RUN shipped present and dim - dim ink, no accent, a
   default cursor - so a dead control over there is drawn in the words this
   screen already uses rather than in a second idiom invented for it.

   AND THE OUTLINE THIS WAS ASKED FOR WAS BUILT AND LOST TO A SCREENSHOT.
   A 1px hairline round each dead button - cream at 0.12, an `outline` so
   the 2px never reached the layout - and photographed from seat B at 960 it
   put THREE BOXES back in a row this screen had already taken them out of:
   "the row was divided the way the rail along the floor is... which gave
   three words three boxes and made the header the busiest strip on the
   screen", forty lines further up. It made the column you do NOT own the
   loudest thing on the page, which is backwards, and it read as a disabled
   form rather than as somebody else's screen.

   SO IT IS COLOUR, which is the rule that row already states in its own
   words: "every state that was a slab is a colour instead". `--dim` against
   the live `#a8a199` is one clear step down, it survives reduced motion,
   and it adds nothing to a row that was deliberately emptied.
   ref/seat/inert-head.png is the outline, inert-head-colour.png is this.

   THE RED SLAB GOES. `.pipe-run svg` carries `background: var(--accent)`,
   and on this branch red is the colour of the operation - a red arrow on
   somebody else's screen is an invitation to press the one control that
   spends a job. It is the dim grey the rest of the row is.

   The TITLE stays, because it names what they are looking at, and so do the
   kind chips, because they say what that grid is filtered to. */
body[data-role="a"] .side-b .pipe-head .pipe-btn,
body[data-role="b"] .side-a .pipe-head .pipe-btn {
  color: var(--dim);
  cursor: default;
}
body[data-role="a"] .side-b .pipe-head .pipe-run svg,
body[data-role="b"] .side-a .pipe-head .pipe-run svg {
  background: var(--raise-2);
  fill: var(--dim); stroke: var(--dim);
}

/* AND THE CAMERA SWITCH, WHICH NEVER ACTUALLY HID. LATENTLAB_SEAT has always
   set `hidden` on it in the column you do not own - and `.cam-on` sets
   `display: inline-flex`, which beats a UA display:none, so the button has
   been sitting in the other artist's camera row the whole time offering to
   put your face on their tile. The spectator rule two blocks up already
   carries `!important` for exactly this reason; the per-column one was
   written as a property and never checked against the cascade. Found by
   switching the harness from `el.hidden` to computed display - the property
   was true the whole time, which is why it read as working. */
body[data-role="a"] .side-b .cam-on,
body[data-role="b"] .side-a .cam-on { display: none !important; }

/* ---- the scorecard ----
   One row per round: the two frames across it, the round and the split down
   the middle. The frames are small on purpose - this is a SCORECARD, read
   in one look, and the round you want to see again is one press away in the
   strip underneath. Seven rounds has to fit the same window three does, so
   the rows scroll rather than the frames shrinking to nothing. */

.score-panel { width: min(860px, 100%); text-align: left; }
.score-head {
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  gap: 12px; margin: 4px 0 14px;
}
.score-side {
  font: 600 15px/1.1 Oswald, sans-serif; letter-spacing: 0.03em;
  text-transform: uppercase; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.score-side-b { text-align: right; }
.score-line {
  font: 600 22px/1 Oswald, sans-serif; letter-spacing: 0.04em;
  color: var(--muted); font-variant-numeric: tabular-nums;
}
.score-line b { color: var(--ink); font-weight: 600; }

.score-rows {
  display: flex; flex-direction: column; gap: 3px;
  max-height: 52vh; overflow-y: auto;
}
.score-row {
  display: grid; grid-template-columns: 1fr 86px 1fr; align-items: center;
  gap: 10px; padding: 8px 10px;
  background: var(--raise-2);
}
.score-cell {
  display: flex; align-items: center; gap: 10px; min-width: 0;
}
.score-cell-b { flex-direction: row-reverse; text-align: right; }
.score-pic {
  flex: 0 0 auto;
  width: 136px; height: 77px; object-fit: cover;
  background: #000; border-radius: 3px;
  outline: 2px solid transparent; outline-offset: -2px;
}
/* THE ROUND THE AUDIENCE GAVE YOU. Red is the colour of the thing that
   happened, and on this screen the thing that happened is this. */
.score-cell.is-won .score-pic { outline-color: var(--accent); }
.score-cell.is-won .score-pct { color: var(--accent-text); }
.score-meta { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
.score-wf {
  font: 500 10.5px/1.3 Archivo, sans-serif; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--muted);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.score-pct {
  font: 600 14px/1 Oswald, sans-serif; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.score-n {
  text-align: center;
  font: 500 10.5px/1.3 Archivo, sans-serif; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--dim);
}

/* A row lands when it is revealed. Under a reduced-motion request it is
   simply there - the one thing this must never do is hold a result back
   from somebody who asked for less movement. */
.score-row { opacity: 0; transform: translateY(6px); }
.score-row.is-in {
  opacity: 1; transform: none;
  transition: opacity 320ms var(--ease), transform 320ms var(--ease);
}
@media (prefers-reduced-motion: reduce) {
  html:not(.wants-motion) .score-row { opacity: 1; transform: none; transition: none; }
}
.score-panel .modal-acts { justify-content: flex-end; margin: 16px 0 0; }
.score-panel .modal-acts .btn {
  flex: 0 0 auto; padding: 13px 24px;
  font: 500 10.5px/1 Archivo, sans-serif; letter-spacing: 0.06em;
  text-transform: uppercase;
}
.score-panel .modal-acts .btn-accent { background: var(--ink); color: #0a0a0a; }

/* ---- the winner ----
   The poster, at the end of the match. A 16:9 still: the winning frame, a
   scrim, and the name at a size Anton is actually for. */

.win-panel {
  position: relative;
  width: min(820px, 100%); aspect-ratio: 16 / 9;
  background: #050505; overflow: hidden;
  box-shadow:
    0 0 0 1px rgba(255, 255, 255, 0.05),
    0 30px 70px -20px rgba(0, 0, 0, 0.95),
    0 0 170px 70px rgba(0, 0, 0, 0.8);
  animation: panel-in 440ms var(--ease) both;
}
.win-art {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover;
  /* the frame is the ground the name is written on, so it is held back far
     enough that 90px of Anton reads over any picture that could win */
  opacity: 0.34; filter: grayscale(0.3);
}
.win-type {
  position: absolute; left: 40px; right: 40px; bottom: 36px; z-index: 2;
}
.win-k {
  margin: 0 0 6px;
  font: 600 11px/1 Archivo, sans-serif; letter-spacing: 0.22em;
  text-transform: uppercase; color: var(--accent);
}
/* The app's OWN display face, at poster size. Anton is the landing page's
   and the house rule is 300px+ for it, which a handle in a modal is never
   going to be - so the poster voice here comes from the composition (black,
   the frame held back, one red word, the name enormous) rather than from
   importing a second face for one word. The handle is capped where it is
   typed, at the lobby, because no size saves a 30-character name. */
.win-name {
  margin: 0;
  font-size: clamp(54px, 9.5vw, 104px); line-height: 0.92;
  letter-spacing: 0.01em; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.win-sub {
  margin: 10px 0 0;
  font: 500 11.5px/1.4 Archivo, sans-serif; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--muted);
}
.win-sub b { color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; }
.win-acts {
  position: absolute; right: 16px; top: 16px; z-index: 3;
  display: flex; gap: 3px;
}
.win-acts .btn {
  padding: 10px 16px;
  font: 500 10px/1 Archivo, sans-serif; letter-spacing: 0.06em;
  text-transform: uppercase;
  background: rgba(5, 5, 5, 0.66);
}

/* ---- the admin's set-up panel ----
   Wider than the note, and in two columns, because the two halves are two
   decisions: the match on the left, the one thing that happens TO it on the
   right. Everything in it is cells on the same grid the floor is - a row is
   a label and the choice, and the choice is always the same control. */

/* The rail's own label, turned into the door. It has to go on reading as a
   label: the rail is a row of words and a button that looks like a button
   down there would be the loudest thing on the floor. */
.rail-open {
  border: 0; background: none; padding: 0; margin: 0;
  font: inherit; letter-spacing: inherit; text-transform: uppercase;
  color: var(--dim); cursor: pointer;
  transition: color 150ms var(--ease);
}
.rail-open:hover { color: var(--accent-text); }

.set-panel { width: min(760px, 100%); text-align: left; }
/* The note's two buttons split its width because there are only two things
   to say to a note. A panel this wide would make the first one a billboard,
   so here they take the width of their own words and sit at the end of the
   row - in the body face, and in the same sand the lit option wears rather
   than in the accent. */
.set-panel .modal-acts { justify-content: flex-end; margin: 16px 0 12px; }
.set-panel .modal-acts .btn {
  flex: 0 0 auto; padding: 13px 24px;
  font: 500 10.5px/1 Archivo, sans-serif; letter-spacing: 0.06em;
  text-transform: uppercase;
}
.set-panel .modal-acts .btn-accent { background: var(--ink); color: #0a0a0a; }
.set-panel .modal-acts .btn-accent:hover { background: #fff; }
.set-cols {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 12px; margin: 18px 0 0;
}
.set-col { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
/* THE PANEL TALKS IN THE BODY FACE, not the condensed one. Oswald is the
   app's voice everywhere a word is a LABEL on something you are looking at
   - a tile, a cell, a round. A settings panel is the one screen you are
   READING, at 10px, and condensed caps at 10px are the hardest thing on
   the page to read. So every word in here is Archivo, with the tracking
   eased off to match. */
.set-h {
  margin: 0 0 5px;
  font: 600 10px/1 Archivo, sans-serif; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--muted);
}
.set-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  min-height: 40px; padding: 6px 12px; min-width: 0;
  background: var(--raise-2);
}
.set-stack { padding: 6px; }
.set-k {
  flex: 0 0 auto;
  font: 500 10.5px/1.3 Archivo, sans-serif; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--ink);
}
.set-in {
  width: 100%;
  border: 0; background: #232323; color: var(--ink);
  padding: 10px 10px;
  font: 400 12.5px/1.3 Archivo, sans-serif;
}
.set-in::placeholder { color: var(--muted); }
.set-in:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }

/* One control, everywhere: the options side by side, the one you are on in
   the accent. The same button the game master's console is made of. */
.seg { display: flex; flex-wrap: wrap; gap: 2px; justify-content: flex-end; min-width: 0; }
.seg button {
  border: 0; cursor: pointer; height: 27px; padding: 0 11px;
  background: var(--raise); color: var(--muted);
  font: 500 10.5px/1 Archivo, sans-serif; letter-spacing: 0.05em; text-transform: uppercase;
  white-space: nowrap;
  transition: background 150ms var(--ease), color 150ms var(--ease);
}
.seg button:hover { background: #2e2e2e; color: var(--ink); }
/* THE OPTION YOU ARE ON IS LIT, NOT ALARMED. Red is the colour of the thing
   happening - the clock running out, the round that was sent, the live
   match - and a panel of eight settings in it was eight alarms before
   anything had started. Lit is the sand the type is already in: as plain as
   a highlight gets, and nothing else on the screen wears it. */
.seg button.is-on { background: var(--ink); color: #0a0a0a; }
.seg button[hidden] { display: none; }

/* The picture the box will hold: ground you can drop on, dashed so it reads
   as waiting rather than empty. A 16:9 of this column is 208px, which made
   one setting taller than the four on the other side - so it is held to the
   height of three rows instead. */
.set-drop {
  display: grid; place-items: center;
  min-height: 118px;
  background: var(--raise-2);
  outline: 2px dashed #2e2e2e; outline-offset: -2px;
  padding: 14px; text-align: center;
}
.set-drop-k {
  font: 500 10.5px/1.4 Archivo, sans-serif; letter-spacing: 0.05em;
  text-transform: uppercase; color: var(--dim);
}
.set-drop img, .set-drop video { width: 100%; height: 100%; object-fit: cover; }

/* A row that only exists for one choice. `hidden` is a UA display:none and
   every row here is a real display, so it needs saying with weight - the
   same trap .pipe-run[hidden] and .grp[hidden] are already written for. */
.set-row[hidden], .set-drop[hidden] { display: none !important; }

/* At a narrow window the two columns are one, because a half of 760 that
   cannot have 760 is two columns of nothing. */
@media (max-width: 700px) {
  .set-cols { grid-template-columns: minmax(0, 1fr); }
}


@media (prefers-reduced-motion: reduce) {
  html:not(.wants-motion) .modal,
  html:not(.wants-motion) .modal-panel { animation: none; }
}


/* ---- panel actions ---- */

.pipe-btn {
  border: 0;
  background: var(--raise);
  /* a step brighter than --muted: these are live controls, not labels */
  color: #a8a199;
  align-self: stretch;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 14px;
  /* 10px/700: at this size and letter-spacing, 600 reads thin against the
     icons beside it - Babe asked for more weight on the controls and on the
     four cells both. */
  font-size: 10px; font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  white-space: nowrap;
  cursor: pointer;
  transition: background 160ms var(--ease), color 160ms var(--ease);
}
.pipe-btn:hover { background: none; color: var(--ink); }
/* ON is the lift the ALL chip and the run icons already use - the state is
   carried by the label going to full ink, not by a red slab in the row. */
.pipe-btn.is-on { background: none; color: var(--ink); }

/* FLOATING, NOT CELLS. The row was divided the way the rail along the floor
   is - each control a cell of its own at full height, with the 3px seam
   showing between them - which gave three words three boxes and made the
   header the busiest strip on the screen. They sit straight on the row now,
   the way .pipe-title always has, so the only thing dividing them is the
   space between. Every state that was a slab is a colour instead: hover and
   ON both just take the label to full ink. */
.pipe-btn { background: none; }

/* The same sparkle the Prompt header carries, so the two ways to the agent
   are plainly the one thing. */
/* Every control on this row carries its mark, not just the agent: three
   words in a row at 10px are three words, and an icon apiece is what makes
   them read as three different things at a glance. */
.pipe-btn svg {
  /* 13, not 12: with the slab gone the icon is half of what makes these read
     as controls rather than labels, so it sits at the scale of the word. */
  width: 13px; height: 13px; margin-right: 6px;
  fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
}
/* the play is a solid triangle - an outlined one at 12px is a wireframe */
/* and RUN keeps one box, because it is the one control that DOES something
   rather than changing what you are looking at: a small rounded square under
   the triangle, which is weight enough to tell an action from a view now
   that nothing else on the row carries a ground. */
.pipe-run svg {
  fill: currentColor; stroke-width: 1.2;
  width: 9px; height: 9px;
  box-sizing: content-box;
  padding: 4px;
  margin-right: 7px;
  border-radius: 5px;
  background: var(--raise-2);
}

/* The way into the assistant reads as a toggle: grey while it is shut, orange
   while it is open, the same as the Assets button beside it. Keyed off the
   side's own state so the button and the panel cannot disagree. */
.side.is-asking .pipe-ask { background: none; color: var(--accent-text); }
.side.is-asking .pipe-ask:hover { background: none; color: #ff8178; }

/* ---- the panel's two views ---- */

.asset-grid { display: none; }
.pipe.show-assets .chain { display: none; }
.pipe.show-assets .asset-grid {
  display: grid;
  /* Bigger squares, fewer to a row: at 78px the wide panel fitted nine
     across and three rows of thumbnails you could not read. 120px gives
     about six in the wide view and three in the column, which lands as
     two rows in the height the panel actually has. */
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 3px;
  align-content: start;
  height: 100%;
  overflow-y: auto;
}
.asset-grid img,
.asset-grid video {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  background: #000;
}


/* ============================ PANEL VIEWS ============================ */

.pipe .chain,
.pipe .asset-grid,
.pipe .view-list,
.pipe .view-run { display: none; }

.pipe[data-view="chain"]  .chain { display: flex; height: 100%; align-items: stretch; overflow-x: auto; }
.pipe[data-view="assets"] .asset-grid {
  display: grid;
  /* Bigger squares, fewer to a row: at 78px the wide panel fitted nine
     across and three rows of thumbnails you could not read. 120px gives
     about six in the wide view and three in the column, which lands as
     two rows in the height the panel actually has. */
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 3px;
  align-content: start;
  height: 100%;
  overflow-y: auto;
}
/* The rows SHARE the area rather than sitting at the top of it: every group
   is padded to three, so the grid is always one row of three, and a fixed row
   height left 116px of nothing under them in the wide view. `1fr` over a
   floor means they grow into whatever there is and still scroll - rather than
   squash - in the 246px column, or if a group ever runs past ten. */
.pipe[data-view="list"] .view-list {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-auto-rows: minmax(120px, 1fr);
  gap: 3px;
  height: 100%;
  overflow-y: auto;
}
.pipe[data-view="run"]  .view-run  { display: block; height: 100%; min-height: 0; }
/* ---- the assistant, over the work area ---- */

/* It takes the main area because a prompt is something you read, and the
   composer is pinned to the bottom of it rather than floating. */
/* THE SHEET IS THE WINDOW, one layer up. Same trick .chat uses - absolute
   against .work, which is the only positioned ancestor - so it covers the
   picture exactly and costs the layout nothing. z-index 2 puts it over the
   media and under the assistant. */
.boxer {
  position: absolute; inset: 0; z-index: 2;
  display: none;
  cursor: crosshair;
  touch-action: none;   /* or a drag on a touchscreen scrolls the page */
  /* THE EDGE IS WHAT SAYS "THIS IS THE CANVAS". Babe: "maybe even give
     the panel a subtle outline. it has to be clear that is the canvas
     area." An outline rather than a border, because a border would take
     2px out of the window and shift the picture inside it by a pixel -
     and the box coordinates are measured against that picture. */
  /* --line IS 19px, NOT A COLOUR - it is the header's grid unit, so
     `solid var(--line)` was an invalid shorthand and drew nothing at all.
     The tell was the COMPUTED value: 3px, which is `medium`, the width a
     dropped outline falls back to. Same family as the second `var THUMBS`
     this file already caught - grep a name before borrowing it. */
  outline: 1px solid var(--dim);
  outline-offset: -1px;
}
/* The empty canvas says what to do with it, centred and out of the way. */
.boxer-hint {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  margin: 0; padding: 0 14%;
  pointer-events: none;
  text-align: center;
  font-family: Archivo, system-ui, sans-serif;
  font-size: 12px; line-height: 1.5; color: var(--muted);
}
.side.is-boxing .boxer { display: block; }
/* AND THE SAME SHEET TAKES DOTS, armed by the MODEL rather than by the
   tile - `sam3-inpaint` masks what SAM3 is pointed at. Two classes rather
   than one because the two gestures are exclusive: a drag that appends a
   rectangle means nothing to a model that reads none, and the press
   handler branches on this one before it reaches the drag. */
.side.is-tapping .boxer { display: block; }

/* ---- THE CLIP BENCH ----
   The same bargain the box sheet makes one rule above: the window becomes
   the surface a tool works on while that tool is open, and goes back to
   being a window when it is not. The difference is that the bench brings
   its own picture - a box is drawn OVER the artist's media, while a clip
   being trimmed IS the media - so this one is opaque and covers what was
   in the window rather than sitting transparently on it.

   BELOW .chat IN THE STACK, like the sheet, so pressing Agent still covers
   it the way Agent covers everything else. */
.bench {
  position: absolute; inset: 0; z-index: 2;
  display: none;
  background: #000;
}
.side.is-cutting .bench { display: flex; }

/* ---- SCENE EDIT, the staging bench ------------------------------------
 *
 * The third surface in this window, on the same terms as the two above: it
 * is absolutely placed over the whole of .work and shown by the class the
 * open tile hangs. What is different is that it keeps a WebGL context, so it
 * is mounted once and then merely hidden - which is why this is display:none
 * rather than anything that would tear the canvas down.
 */
.blockout {
  position: absolute; inset: 0; z-index: 2;
  display: none;
  background: #07080b;   /* the renderer's own clear colour, so no flash */
  overflow: hidden;
}
.side.is-staging .blockout { display: block; }
.blockout canvas { display: block; width: 100%; height: 100%; }

/* A DROP LANDS ON THE VIEWPORT, because the viewport is where the mesh goes
   and this tile has no square to aim at. The accent marks the operation, the
   same rule the box and the tile pictures follow. */
.blockout.is-over { outline: 2px solid var(--accent); outline-offset: -2px; }

/* THE TOOLS ARE A BAR ACROSS THE TOP, which is the node's own shape and
   what Babe asked for. It is a BAR rather than floating chips because these
   are controls you reach for over and over, where the readout in the corner
   below is a thing you glance at - and the canvas sits under it rather than
   beside it, so the viewport loses no width. */
/* A SOLID ROW, NOT A FADE. Babe, 2026-10-20: "can we make a solid grey row
   for the top buttons, lose the squares on them." The gradient was there to
   keep the words legible over whatever the viewport happened to be showing -
   which is the right instinct and the wrong mechanism, because a fade makes
   the row's own edge depend on the picture under it. A solid ground gives
   the bar one edge at every scene. It was the reel card's #1c1c1c for an
   hour; Babe: "i think we can go a bit darker on the panel rows." Which is
   right for where these sit - the chrome in the PANEL has a panel behind
   it, and these two have a nearly black viewport, so the same grey reads
   heavier here than it does there. */
.bo-bar {
  position: absolute; left: 0; right: 0; top: 0; z-index: 3;
  display: flex; align-items: center; flex-wrap: wrap; gap: 2px;
  padding: 5px 8px;
  background: #121212;
}
.bo-grp { display: flex; gap: 2px; }
/* A DIVIDER RATHER THAN A GAP, because the groups are different kinds of
   thing - what the gizmo does, what the view does, what leaves the scene -
   and a wider gap alone reads as a layout accident at this size. */
.bo-sep {
  width: 1px; align-self: stretch; margin: 2px 5px;
  background: rgba(237, 225, 211, 0.16);
}

/* AND THE BUTTONS LOSE THEIR SQUARES. Each one carried a border and a
   ground of its own, which is a box inside a box the moment the row has one
   - seven outlines saying the same thing the row already said. What is left
   is the word, and the row is what holds them together. Hover is a ground
   rather than a brighter edge, for the same reason: the edge is gone. */
.bo-tool {
  appearance: none;
  display: inline-flex; align-items: center; gap: 5px;
  border: 0;
  background: transparent;
  color: var(--muted);
  /* ARCHIVO RATHER THAN OSWALD - Babe, 2026-10-20: "can we change the fonts
     on the top panel to the easier to read one." Oswald is CONDENSED, which
     is what the panel's labels want where a word has to fit a 46px column;
     this row has the whole window and is read at a glance mid-gesture, so
     the narrow face was buying width nothing needed and costing legibility.
     The tracking comes down with it, because letter-spacing exists to open
     up a condensed face and a normal one is already open. */
  font-family: Archivo, system-ui, sans-serif;
  font-size: 10px; letter-spacing: .04em; text-transform: uppercase;
  padding: 4px 7px;
  cursor: pointer;
  line-height: 1;
}
.bo-tool:hover { background: #2a2a2a; color: var(--ink); }
/* THE LIT TOOL IS COLOUR, NEVER MOVEMENT - this machine asks for reduced
   motion, so anything behind that query has to still say what happened. */
.bo-tool.is-on { color: var(--accent-text); }
/* THE KEY IS PRINTED BESIDE THE WORD, which is the node's doing and worth
   keeping: a shortcut nobody is told about is a shortcut nobody uses. Dim,
   because it is a note on the control rather than part of its name. */
.bo-key {
  color: var(--dim);
  font-size: 9px; letter-spacing: .04em;
}

/* ---- the kit, which lives in the PANEL ---- */

/* THE KIT STACKS, AND IT HAS TO SAY SO. `.grp-tall[open] > .grp-body` is
   `display: flex` with the default ROW direction - which is right for the
   prompt it was written for, because that group has one child. This one has
   two, so they were laid out SIDE BY SIDE: the six primitives wrapped into a
   159px column and the import row became a 48px item printed over the top of
   them. Measured rather than guessed - "row 2: width 48" named the cause.

   AND `.grp` IS IN THE SELECTOR ON PURPOSE. `.grp-kit[open] > .grp-body`
   scores exactly the same as `.grp-tall[open] > .grp-body`, so it would win
   on ORDER alone - which is the fault this file has now recorded four times
   in one session. */
.grp.grp-kit[open] > .grp-body { flex-direction: column; }

/* AND NOTHING HERE FIGHTS `.grp-acts`, WHICH IS STICKY ON PURPOSE. The
   import row looked clipped and the first explanation was wrong: it was not
   the `::details-content` wrapper stranding height - a rule for that was
   written, measured with and without, and changed NOTHING, so it was taken
   back out rather than left in doing nothing. `.grp-acts` is
   `position: sticky; bottom: 0`, deliberately, so the groups above scroll
   UNDER it - and at 960 this form wanted 175px of a 151px column, so the
   last row sat beneath the bar until you scrolled. The fix was to need less
   height, not to fight the bar. */

/* THREE TO A ROW, AS A SHARE RATHER THAN A FLOOR. These were `flex: 1 1
   auto` in a wrapping row, which is the fault the reel's `.link` already
   paid for from the other side: a floor only decides what happens once the
   row is FULL, so five fitted on one line and PLANE wrapped alone and then
   GREW to the whole width - one button four times the size of its
   neighbours, for no reason a reader could see. A share gives two tidy rows
   of three at every width. */
.kit-row {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 3px;
}
/* IMPORT SITS IN THE HEADER, right-aligned, the way the prompt group's
   sparkle does - `grp-fixed` makes the summary unpressable, so the button
   has to take its pointer events back. */
.kit-imp {
  margin-left: auto;
  pointer-events: auto;
  appearance: none; border: 0; cursor: pointer;
  background: #262626; color: var(--muted);
  font-family: Oswald, system-ui, sans-serif;
  font-size: 9px; letter-spacing: .06em; text-transform: uppercase;
  padding: 3px 7px; line-height: 1;
}
.kit-imp:hover { background: #343434; color: var(--ink); }
.kit-b {
  appearance: none;
  flex: 1 1 auto;
  /* `--line` IS 19px, NOT A COLOUR - it is the header's grid unit, and a
     border of `1px solid 19px` is simply dropped, which would have read as
     a styling choice rather than a mistake. The panel's own buttons carry
     no border at all and sit on #2e2e2e; these are the same family one step
     quieter, because a row of six is a kit rather than an action. */
  border: 0;
  /* BRIGHTER THAN THE PANEL - Babe, 2026-10-20: "can we make the button
     colors a little bit brighter here." These sat at #262626, which is two
     steps off the group's own ground and reads as a disabled row rather
     than a row of things to press. The cells beside them moved with them,
     because an OBJECT and an ASPECT are the same kind of press. */
  background: #303030;
  color: var(--ink);
  font-family: Oswald, system-ui, sans-serif;
  /* BIGGER THAN A KIT LABEL, because this row stopped being a kit. These
     were six primitives at 9px, where the word was a caption under a thing
     you recognised by shape; the five that are left are ASPECTS, where the
     word is the whole control and "16:9" at 9px is the smallest type in the
     panel for one of the things you press most. Babe: "can we make the text
     bigger on the aspect ratio buttons." */
  font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
  padding: 6px 4px;
  cursor: pointer;
  line-height: 1;
}
.kit-b:hover { background: #3e3e3e; }
.kit-b.is-on { color: var(--accent-text); }
/* THE OBJECT CELLS ARE SQUARE AND CARRY A DRAWING, which is what lets six
   of them sit in a grid without the row reading as a paragraph. The name is
   on the title, which is the only thing a 9px word was doing.

   SIX ACROSS, NOT THREE BY TWO, AND THE NUMBER DECIDED IT. Square cells in
   3 columns measure 67px each at Babe's 960, so two rows cost 134 of the
   151px this panel has and leave nothing for ASPECT RATIO - measured 236
   needed against 151. One row of six is 32px square at 960 and 39 at 1280,
   and the whole panel comes in at 132. A layout that changes with the
   window is the bug this file keeps finding, so it is six everywhere. */
.obj-grid { display: grid; grid-template-columns: repeat(6, 1fr); gap: 3px; }
.obj-b {
  appearance: none; border: 0; cursor: pointer;
  aspect-ratio: 1 / 1;
  display: grid; place-items: center;
  background: #303030; color: var(--muted);
  border-radius: 2px;
}
.obj-b:hover { background: #3e3e3e; color: var(--ink); }
.obj-b svg {
  width: 56%; height: 56%;
  fill: none; stroke: currentColor;
  stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round;
}
/* THE SIXTH IS NOT A SHAPE, but it is not DIMMER either - Babe, 2026-10-21:
   "can you make this import icon the same color as the shape icons." It was
   `--dim` against the row's `--muted`, which is the tone this app uses for
   a control that cannot be pressed, so the one cell in the row that opens a
   file picker read as the one cell that was switched off. Being a different
   KIND of thing is not the same as being unavailable, and the hover below
   still says which kind it is. */
.obj-in:hover { color: var(--accent-text); background: #3e3e3e; }

/* FIVE ACROSS, which is what they were asked to be and what fits: at 960
   the panel gives these 39px each and "16:9" needs 26 of it. The duration
   stepper that used to sit under them has moved to the transport, where the
   clock it changes the right-hand half of already lives. */
.asp-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 3px; }

/* WHICH EYE YOU ARE LOOKING THROUGH, in the corner of the window it is a
   fact about. Babe, 2026-10-20: "maybe we put the camera / viewport toggle
   in the corner of the viewport." Top left, under the bar and opposite the
   sentence, so the three things pinned to this picture never meet.

   STACKED, because two 22px cells side by side read as one wide control
   with a seam; one above the other reads as a choice between two. */
.bo-eye {
  position: absolute; left: 8px; z-index: 3;
  display: flex; flex-direction: column; gap: 3px;
}
/* NO SQUARE, AND NO RED - Babe, 2026-10-20: "lets keep them out of the
   square button. and we only really need to highlight it when clicked. it
   can be a light grey for both states as a toggle."
 *
 * The chrome was saying what the icon already says. This sits ON the
 * picture rather than in a row of controls, so a filled square reads as a
 * button parked over the shot, where the mark alone reads as a readout you
 * can press - which is what it is. The 22px box stays as the HIT AREA: the
 * background goes, the target does not.
 *
 * AND THE STATE IS THE ICON, NOT THE COLOUR. It ships as a camera while you
 * look through one and a screen while you are outside, so a second channel
 * saying the same thing in red was spending the accent on a fact already on
 * screen - and the accent means irreversible everywhere else here. One
 * light grey in both states; the highlight is the PRESS. */
.bo-eye-b {
  appearance: none; border: 0; cursor: pointer;
  width: 22px; height: 22px; padding: 0;
  display: grid; place-items: center;
  background: none;
  color: var(--muted);
}
/* COLOUR, NEVER MOVEMENT - this machine asks for reduced motion, so the
   press has to say so standing still. */
.bo-eye-b:active { color: var(--ink); }
/* AND THE TARGET LIGHTS WHERE THE EYE DELIBERATELY DOES NOT. The toggle's
   two states are EQUAL - in the shot or outside it - which is why Babe had
   its colour taken off: it was saying what the icon already said. These are
   not equal. Off is resting; armed is waiting for a press; locked is a
   thing the camera is doing to every frame you render, and a control that
   is doing something has to look like it. */
.bo-eye-b[data-target].is-on { color: var(--ink); }
/* AND THE OUTLINER'S BUTTON LIGHTS THE SAME WAY, for the same reason: open
   is a thing the window is doing, not one of two equal states. */
.bo-eye-b[data-outliner].is-on { color: var(--ink); }

/* ---- THE OUTLINER ------------------------------------------------------
 *
 * Babe, 2026-10-20: "prob makes more sense to add an outliner that you can
 * hide in the right side of the viewport."
 *
 * DOCKED RIGHT AND CLOSED BY DEFAULT. It is a lookup you open - what is on
 * the stage, take me to it - rather than something you watch while framing,
 * so it costs the picture nothing until it is asked for. The top and bottom
 * are written by `placeEye()` against the two rows' MEASURED heights, both
 * of which wrap, which is the same arithmetic the guideline is fitted with.
 *
 * `min(160px, 40%)` BECAUSE THE COLUMN IS NARROW. At Babe's 960 the
 * viewport is 454 wide, so a fixed 160 is a third of the picture; the
 * percentage is what stops it eating a narrow window, and the pixel cap is
 * what stops it sprawling on a wide one. */
.bo-out-b { position: absolute; right: 8px; z-index: 3; }
.bo-outl {
  position: absolute; right: 0; z-index: 4;
  width: min(160px, 40%);
  display: flex; flex-direction: column;
  background: rgba(14, 14, 14, 0.94);
  border-left: 1px solid rgba(237, 225, 211, 0.12);
}
.bo-outl[hidden] { display: none; }
.bo-out-top {
  flex: 0 0 auto;
  display: flex; align-items: center; justify-content: space-between;
  padding: 7px 6px 7px 9px;
  border-bottom: 1px solid rgba(237, 225, 211, 0.1);
  color: var(--muted);
  font-family: Oswald, system-ui, sans-serif;
  font-size: 10px; letter-spacing: .08em; text-transform: uppercase;
}
.bo-out-x {
  appearance: none; border: 0; background: none; cursor: pointer;
  padding: 0; width: 16px; height: 16px;
  display: grid; place-items: center; color: var(--muted);
}
.bo-out-x:hover { color: var(--ink); }
.bo-out-x svg {
  width: 11px; height: 11px;
  fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round;
}
.bo-out-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }

/* A ROW IS A COLOUR AND A NAME, and nothing else. The node's carries an
   eye, a lock and an expander on every line because it is a scene editor;
   this answers two questions and stops. */
.bo-row {
  appearance: none; border: 0; background: none; cursor: pointer;
  width: 100%; text-align: left;
  display: flex; align-items: center; gap: 7px;
  padding: 5px 9px;
  color: var(--muted);
  font-family: Oswald, system-ui, sans-serif;
  font-size: 11px; letter-spacing: .03em;
}
.bo-row:hover { background: rgba(237, 225, 211, 0.06); color: var(--ink); }
/* THE ONE YOU ARE HOLDING. Colour and a ground, never movement - this
   machine asks for reduced motion. */
.bo-row.is-on { background: rgba(237, 225, 211, 0.1); color: var(--ink); }
.bo-row-sw {
  flex: 0 0 auto; width: 9px; height: 9px; border-radius: 1px;
}
.bo-row-i {
  flex: 0 0 auto; width: 11px; height: 11px;
  fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
}
/* A DROPPED FILE'S NAME IS LONG AND THE DRAWER IS NOT. It ends in an
   ellipsis rather than wrapping, because two lines of filename would push
   the thing under it off a list whose whole job is to be scanned. */
.bo-row-n {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.bo-eye-b svg {
  width: 14px; height: 14px;
  fill: none; stroke: currentColor;
  stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round;
}

/* WHAT THE STAGE SAYS ABOUT ITSELF, in the words a prompt would use. Bottom
   right, opposite the tools, so a long line runs away from them rather than
   into them. */
.bo-say {
  position: absolute; right: 8px; bottom: 8px; z-index: 3;
  max-width: 60%;
  color: var(--dim);
  font-family: Oswald, system-ui, sans-serif;
  font-size: 10px; letter-spacing: .06em; text-transform: uppercase;
  text-align: right;
  pointer-events: none;
  text-overflow: ellipsis; overflow: hidden; white-space: nowrap;
}
/* THE ASPECT GUIDELINE, which is the shape the output is cut to. A border
   and nothing else: what falls outside is still drawn, so you can see what
   you are losing, and the box itself takes no pointer events because it sits
   over the thing you orbit. */
.bo-frame {
  position: absolute; z-index: 2;
  pointer-events: none;
  /* DIMMER THAN IT WAS - Babe, 2026-10-20: "can we make the aspect ratio
     guide box have less opacity... dimmer." It is a guide to what is being
     cut off, so it should be the quietest line on the picture: at 0.5 it
     was the brightest thing in an empty stage and read as part of the shot
     rather than as a note about it. The dimming OUTSIDE it does the real
     work of saying where the frame is, and that is untouched. */
  border: 1px solid rgba(237, 225, 211, 0.22);
  /* AND THE OUTSIDE IS BLACK NOW, not a 35% scrim - Babe, 2026-10-21: "we
     should prob add black bars for aspect ratios." At 0.35 the grid went on
     reading straight through the mask, so a 1:1 shot in a wide window had
     its frame drawn in one hairline and nothing else; the picture did not
     look cut to anything.
     IT COSTS NOTHING TO SEE PAST IT ANY MORE, which is why this can be
     opaque when it could not be before. "What falls outside is still drawn,
     so you can see what you are losing" was the rule while this window was
     the only picture there was - you staged THROUGH the shot. Staging
     happens in the free view now, where this mask is hidden entirely, so
     the only time these bars are on screen is when the screen IS the shot,
     and a letterbox is the honest thing to draw there. */
  box-shadow: 0 0 0 9999px #000;
}

/* ---- the transport ----------------------------------------------------
 *
 * Laid out from Babe's own screenshot of the app: the clock above a row of
 * round controls, with the track running off to their right and the keys
 * sitting ON it as diamonds. It is a BAR across the bottom for the same
 * reason the tools are one across the top - these are controls, not a
 * readout nailed to a picture.
 */
/* AND THE TRACK STARTS WHERE THE PANEL'S HEADER BAR DOES - Babe,
 * 2026-10-21: "the start side of the timeline bar should line up with the
 * SCENE EDIT header bar". It is the one vertical line this column can
 * actually draw: the camera tile is 148px wide with a 4px gutter, so
 * `.pipe-head` begins 164px into the side and the track now begins there
 * too, with the five controls living in the width the camera tile occupies
 * below them.
 *
 * THAT 136px IS ALSO WHAT CAPS THE BUTTONS. Five squares and four gaps have
 * to come to exactly it, which is 24px and 4px - so "a little bigger" is as
 * big as they get while the line holds, and the height Babe asked for went
 * into the TRACK instead, where it is also what makes a diamond easier to
 * grab. Measured: the whole bar 53px -> 64px.
 *
 * `auto` WOULD HAVE BEEN WRONG even at the right number, because the column
 * would then be whatever the buttons happened to measure and the line would
 * move the first time one of them changed. */
.bo-trans {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 3;
  padding: 8px 8px 9px;
  display: grid;
  /* 140 + the 4px column gap is the 144 that puts the track's left edge on
     164, which is where `.pipe-head` begins - see below. The number moved
     when the buttons grew; the LINE did not. */
  grid-template-columns: 140px 1fr;
  grid-template-areas: "clock clock" "keys track";
  align-items: center;
  /* 4, NOT 8: this gap is half of what puts the track on 164 - the column
     is 140 and the gap is 4. Changing one without the other moved the line
     by four pixels while every number in the comment above still read
     correctly, which is the way a measured layout goes wrong. */
  gap: 3px 4px;
  /* THE SAME SOLID ROW THE TOOLS WEAR, and for the same reason: two bars
     bracketing one viewport have to be the same material, or the window
     reads as having a floor and no ceiling. */
  background: #121212;
}
/* THE CLOCK SITS AT THE FAR END OF ITS OWN ROW, SMALL AND PLAIN - Babe,
   2026-10-21: "can we make the duration a smaller normal font (above the
   transport buttons) and move it all the way to the right side."
 *
 * PLAIN MEANS THE BODY FACE. Oswald is this app's control type and the
 * clock is not a control - it is the one readout on a row of buttons, and
 * in condensed caps with letter-spacing it was competing with them. Archivo
 * at 9px reads as a number rather than as a label.
 *
 * AND TABULAR FIGURES, because this one counts: proportional digits change
 * width as the tenths tick over, and a right-aligned number that shuffles
 * its own left edge sixty times a second is worse than a left-aligned one
 * that does not. */
.bo-clock {
  grid-area: clock; justify-self: end;
  color: var(--dim);
  font-family: Archivo, system-ui, sans-serif;
  font-size: 9px; letter-spacing: 0;
  font-variant-numeric: tabular-nums;
}
/* THEY TOUCH - Babe, 2026-10-21: "can we make the transport buttons a
   little bit bigger so they are the same height as the timeline bar? we can
   make them closer together if needed to keep the alignment right." That is
   the whole trade, and the arithmetic is unforgiving: the track has to
   start at 164, the row's content starts at 20, so five squares and their
   gaps have to come to 140. At 24px with 4px gaps that was 136; at 28px
   there is nothing left for gaps at all.
 *
 * SO THEY BECOME A STRIP, which is the shape the SETTINGS row had just
 * grown: five segments reading as one control rather than five chips with
 * air between them. The rounding goes on the ENDS for the same reason. */
.bo-tbar { grid-area: keys; display: flex; gap: 0; }

/* ROUNDED SQUARES, NOT CIRCLES - Babe, 2026-10-20. A circle reads as a
   media-player chrome from somewhere else; a rounded square is the shape
   every other pressable thing on this page already is, one size down. */
.bo-t {
  appearance: none; border: 0; cursor: pointer;
  /* 28, WHICH IS THE TRACK'S OWN HEIGHT - the two now measure the same and
     the row reads as one band rather than as buttons floating beside a
     strip. */
  width: 28px; height: 28px; padding: 0;
  display: grid; place-items: center;
  background: #2a2a2a; color: var(--ink);
  /* A HAIRLINE BETWEEN SEGMENTS, not a gap: with no space between them the
     five would read as one long button. */
  border-right: 1px solid #121212;
}
.bo-t:first-child { border-radius: 4px 0 0 4px; }
.bo-t:last-child { border-radius: 0 4px 4px 0; border-right: 0; }
.bo-t:hover { background: #3a3a3a; }
.bo-t svg { width: 15px; height: 15px; fill: currentColor; stroke: none; }
/* THE PLUS IS A STROKE, AND A HEAVIER ONE. Babe, 2026-10-20: "make the
   plus a little bolder." It is the one press on this row that MAKES
   something - everything else moves the playhead - and at the same weight
   as the cross beside it, it read as another piece of chrome. The cross is
   gone now, so this rule carries the plus alone. */
.bo-t svg path[d^="M12 4"] {
  fill: none; stroke: currentColor; stroke-width: 3; stroke-linecap: round;
}
/* RECORD CARRIES THE RED RATHER THAN BEING IT. Babe: "the red circle should
   be inside of it the side of the triangle like play." A button that IS a
   red dot reads as a different kind of control; one wearing a red mark in
   the same square reads as the same control doing the one thing worth
   marking - which is also the rule the accent follows everywhere else here,
   marking the operation rather than the chrome.

   r=6 in a 24 box against the triangle's 11x14, so the two read the same
   weight at 13px. */
.bo-rec svg { fill: var(--accent); }
.bo-rec:hover svg { fill: var(--accent-text); }
.bo-trans.is-playing .bo-rec svg { opacity: .4; }
/* the shutter's lens is a hole in the body rather than a second shape */
/* `.bo-hole` went with the shutter, 2026-10-21 - it was the lens in the
   camera icon and the camera icon is under the preview now, where it is a
   word rather than a drawing. */

/* THE CLOCK IS A READOUT AGAIN. The length stepper that sat beside it has
   gone to the panel's SETTINGS group, where the aspect already lives - a
   length is a property of the shot, and the transport does things TO a
   shot rather than saying what one is. */
.bo-clock { display: flex; align-items: center; gap: 8px; }

/* ---- SETTINGS, in the panel ---- */

/* ONE ROW, TWO EQUAL HALVES. `1fr 1fr` rather than `auto auto` so the two
   steppers start at the same two x positions whatever their numbers happen
   to be - 5 and 30 are different widths, and a row that shifts when you
   press + is a row you press twice by accident. */
/* THREE BOXES PACKED LEFT, AND THEY WRAP RATHER THAN SQUEEZE.
 *
 * As a grid of three `auto` columns the widest box was shrunk below its own
 * content the moment the three came to more than the row: the colour cell
 * came out reading "# 07" with the rest of the hex clipped inside it. That
 * is the fault this file already records from the other side, where a flex
 * child took a row's missing pixel out of a WORD rather than overflowing -
 * "if this ever does not fit it has to SHOW that, not hide it inside the
 * glyphs."
 *
 * THE THREE COME TO 365 of the 371 this row has at 1920, and to far more
 * than the 207 it has at 960 - so at a narrow window one drops to a second
 * line and stays whole. A box is a fixed thing now, so nothing inside it
 * can shift when you press it. */
/* THEY SPREAD TO FILL THE ROW - Babe: "maybe more space between the groups
   to fill the panel." The three boxes come to 337 of the 371 this row has
   at 1920, and a fixed gap would have left the slack in one lump at the
   right-hand end, reading as a row that stopped early. `space-between`
   puts it between them instead, where it is the thing that tells three
   controls apart.
 *
 * THE GAP IS THE FLOOR, not the spacing: it is what survives when the row
 * is too narrow to spread - at 960 the boxes come to more than the 207 it
 * has there and one drops to a second line, where `space-between` has
 * nothing to give and the 10px is what keeps them apart. */
.num-row {
  display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between;
  gap: 8px 10px;
}
.num-row > * { flex: 0 0 auto; }
/* AND NOTHING IN A HALF SHRINKS. `flex-shrink: 1` is the default, so a row
   one pixel over its width takes the pixel out of a WORD rather than
   overflowing - which is how a 15px line came out 6px one surface over, and
   how these steppers read as 121px wide when they are 64. If this ever does
   not fit it has to SHOW that, not hide it inside the glyphs. */
/* A CONTROL IS A BOX WITH ITS NAME IN IT - Babe, 2026-10-21: "can we sort
   of contain these in their own box. so [FPS][30][up|down]". Three
   segments, told apart by tone rather than by rules: the name on the
   panel's own key colour, the number on the raised ground every other
   readout here sits on, and the pair that changes it.
 *
 * THE BOX IS THE OUTLINE, so nothing inside it carries one - a border on
 * each segment would draw five lines where the eye needs one. */
/* AND IT IS GIVEN A HEIGHT - Babe, 2026-10-21: "it can all be bigger you
   have space." The box was as tall as the 9px word inside it, because
   nothing in a row of inline parts has a height of its own; 32px is what
   there is room for, measured rather than picked: the group went 83 -> 56
   when the stacked labels came inside, and the form still has to come to
   246 of its 246. */
.num-box {
  display: flex; align-items: stretch; min-width: 0;
  height: 32px;
  border-radius: 3px; overflow: hidden;
  background: var(--raise);
  /* A BORDER, NOT AN INSET SHADOW - Babe, 2026-10-21: "can you make the
     outline in the color cell the same it looks dim on the color swatch
     side." An inset shadow is part of the PARENT's background painting,
     and a child paints over it: the label cell has a ground of its own and
     covered that edge, while the swatch cell is transparent around its
     square and let it show - so one half of the same hairline read
     brighter than the other. A border is outside the padding box, where no
     child can reach it, so it is one line all the way round. */
  border: 1px solid rgba(237, 225, 211, 0.1);
}
.num-box > * { flex: 0 0 auto; }
/* THE DIVIDER IS THE CELL WALL - Babe: "COLOR and the swatch can be
   contained in divided cell". One line between every pair of segments,
   drawn once here rather than on each of them, so a box of two reads the
   same as a box of three. */
.num-box > * + * { border-left: 1px solid rgba(237, 225, 211, 0.1); }
.num-box .num-k {
  display: flex; align-items: center;
  padding: 0 5px;
  font-size: 10px;
  background: rgba(237, 225, 211, 0.05);
}
.num-box b {
  display: flex; align-items: center; justify-content: center;
}
/* THE PANEL'S OWN SETTING TYPE, NOT A THIRD ONE. Babe: "make the length and
   rate more like a regular panel typeface, idk what we are doing on others."
   What the others do is `.pick-k` for the word and `.pick-s`/`.pick-n` for a
   stepper, so these take those exact values - 9px/600/.09em on `--dim` for
   the key, and a #232323 button at 11px on `--muted` beside a tabular
   number. The classes are not REUSED, because `.pick` is a full-width row
   with a 46px key column and two of those do not fit side by side; the type
   is what was asked for and the type is what is shared. */
.num-k {
  font-size: 9px; font-weight: 600; letter-spacing: 0.09em;
  /* THE LABEL IS ONE LINE AND IS GIVEN ONE LINE. A 9px word in a 1.2
     line-box is 11px of row for 9px of word, and this row is counted. */
  line-height: 1;
  text-transform: uppercase;
  color: var(--dim);
}
/* SIDE BY SIDE, SO EACH ONE IS THE HEIGHT OF THE BOX - Babe: "the arrows
   can be side by side so they are bigger." Stacked they were 7px of arrow
   in half a row each; across, each gets the full 32 and a 19px column, and
   the pair is still one segment of the box. UP FIRST, which is the order
   Babe's own sketch wrote them in: [FPS][30][up|down]. */
.num-arr { display: flex; }
.num-arr button {
  appearance: none; border: 0; cursor: pointer;
  display: grid; place-items: center;
  /* 22, NOT 17 - Babe, 2026-10-21: "make the arrow buttons wider". They
     were sized when the three boxes came to 372 of a 371px row and every
     pixel was spoken for; the hex cell coming out of the colour box gave
     76 of them back. */
  width: 22px; padding: 0;
  background: rgba(237, 225, 211, 0.05); color: var(--muted);
  transition: background 150ms var(--ease), color 150ms var(--ease);
}
.num-arr button + button { border-left: 1px solid rgba(237, 225, 211, 0.1); }
.num-arr button:hover { background: rgba(237, 225, 211, 0.14); color: var(--ink); }
.num-arr svg {
  width: 10px; height: 6px;
  fill: none; stroke: currentColor; stroke-width: 1.5;
  stroke-linecap: round; stroke-linejoin: round;
}
/* THE NUMBER IS PINNED SO THE + DOES NOT WALK. 12 to 60 is two digits
   either way, but 5 to 20 is not, and a button that moves when you press it
   is a button you press twice by accident. Tabular figures for the same
   reason the row above uses them: 1 and 8 have to be one width. */
/* AS WIDE AS TWO DIGITS, ALWAYS - Babe, 2026-10-21: "if you make the
   duration 2 digits it breaks trying to figure that out." At 10 seconds the
   SEC box grew by twelve pixels, the three boxes came to more than the row
   and the last one dropped onto a second line. The row was doing what it
   was told; the fault is a box whose WIDTH depends on its VALUE.
 *
 * 28 IS 18 OF GLYPH AND 10 OF PADDING, measured at 15px/600 with tabular
 * figures - so "5" is centred in exactly the space "10" needs and nothing
 * moves when you press the arrow. The rate was already safe by accident
 * (12/24/25/30/48/60 are all two digits) and is now safe on purpose. */
.num-box b {
  /* 30, NOT 28: two tabular digits measure 18.6px rather than 18, so at
     ten seconds the cell came out a pixel wider than at five - and a pixel
     is all it takes, since the row wraps when the boxes exceed it. Give a
     reserved width the headroom its own rounding needs. */
  min-width: 30px; padding: 0 5px;
  font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums;
  color: var(--ink);
}

/* ONE SWATCH, 20px SQUARE, AT THE HEAD OF THE NUMBERS ROW - Babe,
   2026-10-21: "make a single black square color swatch smaller (just one of
   them) that you can click on to change the color." The six-across ramp
   went with the picture slot beside it, and what is left is a control that
   SHOWS its value and WALKS a list on a press, which is the frame rate
   stepper's own idiom one cell to the right.
 *
 * 20px RATHER THAN THE STEPPER'S 22, because a square reads heavier than a
 * row of glyphs at the same height - measured against the row it now sits
 * in rather than against the 32px cell it used to be. */
/* THE COLOUR IS A SEGMENT OF ITS BOX, not a square beside a word - Babe:
   "[color][black]". So it has no corners and no outline of its own: the box
   around it draws both, and what is left is the colour itself, which is the
   only thing this control has to say. */
/* THE SQUARE SITS INSIDE ITS CELL - Babe, 2026-10-21: "can you make the
   swatch square smaller inside of that cell." The cell is still the button,
   so the whole 34px is pressable; what shrank is the COLOUR, which is now
   an 18px square with the box's own ground around it.
 *
 * `background-clip: content-box` RATHER THAN AN INNER ELEMENT, so the
 * colour is still written on the one element everything else reads it off -
 * `data-bg` and its background are the same fact in the same place. The
 * price is that the colour must be set as `background-color` and never as
 * the `background` shorthand, which resets the clip.
 *
 * 34 WIDE AND 18 OF COLOUR: the three boxes came to 372 of a 371px row at
 * 1920, so the cell cannot grow - and 7px above and below is what centres
 * an 18px square in a 32px box. */
.bg-b {
  appearance: none; border: 0; cursor: pointer;
  width: 34px; align-self: stretch;
  padding: 7px 8px;
  background-clip: content-box;
}
.bg-b:hover { box-shadow: inset 0 0 0 1px rgba(237, 225, 211, 0.45); }

/* AND THE LAST GROUP TAKES WHAT IS LEFT - Babe, 2026-10-21: "theres a bit
   of a black gap at the bottom of the panel." Measured at 1920: thirteen
   pixels of the page's own black under the SETTINGS group, because the
   three groups come to less than the column once the row fits on one line.
 *
 * IT IS SCOPED TO THIS GROUP rather than to "the last one", because the
 * stack ends with HIDDEN groups - a task with no settings still builds
 * one - so `:last-of-type` is a group nobody can see. And it is the only
 * tile whose form is shorter than its panel; everywhere else the form
 * scrolls, where stretching anything would do nothing at all.
 *
 * `scrollHeight === clientHeight` CANNOT SEE THIS, which is why it went
 * unnoticed: a form whose content is shorter than its box reports the box.
 * The reading that finds it is the last visible group's bottom against the
 * form's.
 *
 * AND IT IS WRITTEN AS `.run-form > .grp-set` BECAUSE ORDER DECIDES AT
 * EQUAL SPECIFICITY: `.grp` carries `flex: 0 0 auto` fifteen hundred lines
 * BELOW this one, so a single class against a single class loses and the
 * group did not move a pixel. Two beats one wherever it is written. Third
 * time in this file - the desk's stream field and a `section > h2` were the
 * other two. */
.run-form > .grp-set { flex: 1 1 auto; }

/* THE PREVIEW'S SURROUND IS THE STAGE'S GROUND - see `setGround`. Only
   while this bench is open: on every other tile the pane holds a RESULT,
   where the surround is the page's own black and should stay it. The
   fallback is the colour the stage opens on, so a bench nobody has
   recoloured needs nothing written to it. */
.side.is-staging .run-out { background: var(--shot-ground, #07080b); }


/* ---- THE GROUND'S COLOUR PICKER ----
 *
 * FIXED, LIKE SELECT MODEL, and for the same reason written one surface
 * over: a panel inside a 246px scroller is clipped by the thing it is
 * trying to escape. It is positioned from the swatch at the moment it
 * opens, and it does NOT close on scroll - that was the date picker's first
 * bug, where merely bringing the pill into view shut the list a moment
 * after the press opened it.
 */
.sw-pop {
  position: fixed; z-index: 60;
  display: flex; flex-direction: column; gap: 6px;
  padding: 7px;
  background: #121212; border-radius: 4px;
  box-shadow: inset 0 0 0 1px rgba(237, 225, 211, 0.12), 0 10px 26px rgba(0, 0, 0, 0.6);
}
.sw-pop[hidden] { display: none !important; }
.sw-top { display: flex; gap: 6px; }
/* TWO GRADIENTS OVER A HUE, which is the whole picture: white to
   transparent across for saturation, transparent to black down for value.
   The hue underneath is set from the one number this control keeps. */
.sw-sv {
  position: relative; width: 132px; height: 96px;
  border-radius: 3px; cursor: crosshair;
  background:
    linear-gradient(to top, #000, rgba(0, 0, 0, 0)),
    linear-gradient(to right, #fff, rgba(255, 255, 255, 0)),
    var(--sw-hue, #f00);
  touch-action: none;
}
.sw-hue {
  position: relative; width: 14px; height: 96px;
  border-radius: 3px; cursor: ns-resize;
  background: linear-gradient(to bottom,
    #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%);
  touch-action: none;
}
/* THE TWO MARKS ARE RINGS, because whatever is under them is the colour
   they are pointing at - a filled dot would be a second colour sitting on
   the one you are choosing. */
.sw-dot {
  position: absolute; width: 10px; height: 10px; margin: -5px 0 0 -5px;
  border-radius: 50%; pointer-events: none;
  box-shadow: 0 0 0 1.5px #fff, 0 0 0 2.5px rgba(0, 0, 0, 0.45);
}
.sw-bar {
  position: absolute; left: -2px; right: -2px; height: 4px; margin-top: -2px;
  border-radius: 2px; pointer-events: none;
  box-shadow: 0 0 0 1.5px #fff, 0 0 0 2.5px rgba(0, 0, 0, 0.45);
}
.sw-hex {
  display: flex; align-items: center; gap: 4px;
  padding: 0 6px; height: 22px;
  border-radius: 3px; background: var(--raise);
  box-shadow: inset 0 0 0 1px rgba(237, 225, 211, 0.1);
}
.sw-hex span { color: var(--dim); font-size: 11px; }
/* SCOPED, BECAUSE EQUAL SPECIFICITY MEANS ORDER DECIDES - `input[type=text]`
   carries a `width: 100%` elsewhere in this file, which is the fault the
   desk's stream field paid for. */
.sw-hex input {
  flex: 1 1 auto; min-width: 0; width: auto;
  appearance: none; border: 0; background: none; padding: 0;
  color: var(--ink);
  font-family: Archivo, system-ui, sans-serif;
  font-size: 11px; letter-spacing: 0.08em; text-transform: lowercase;
  font-variant-numeric: tabular-nums;
}
.sw-hex input:focus { outline: none; }
.sw-hex:focus-within { box-shadow: inset 0 0 0 1px rgba(237, 225, 211, 0.3); }

/* THE BAR UNDER A RENDER, which is `.run-acts` with the page's own SAVE in
   it - the same row every other result gets. It ships with the pane and is
   hidden until there is something to name, because a bar whose button names
   nothing is worse than no bar. */
.shot-acts { flex: 0 0 auto; }
.shot-acts[hidden] { display: none !important; }
/* SAVE SAYS SAVED AND STAYS DOWN, because the thing it did cannot be undone
   by pressing it again - and a second press would file a second copy, which
   is the bug the desk's own SAVE paid for when two presses a second apart
   put two clips in one grid. */
.shot-keep:disabled { background: var(--raise); color: var(--dim); cursor: default; }
/* TALLER, 2026-10-21 - Babe: "would like to make the buttons / timeline bar
   a little bit taller (10-15px?)", 22 -> 30. Then **28**, to meet the
   buttons rather than to be met by them: at 30 the five squares could not
   fit the 140px the alignment leaves them, and two pixels off a strip
   nobody measures is cheaper than two pixels off every control beside it. */
.bo-track {
  grid-area: track; position: relative;
  height: 28px; cursor: pointer;
  background: #1b1b1b;
  touch-action: none;   /* or a scrub on a touchscreen scrolls the page */
}
.bo-ticks span {
  position: absolute; top: 0; bottom: 0; width: 1px;
  background: rgba(237, 225, 211, 0.12);
}
/* ---- the trimmed range ----
 *
 * THE BAND IS A GROUND AND THE HANDLES ARE GREY. The clip bench's rule is
 * that the ACCENT is the selection rather than progress - but on that track
 * there is no playhead competing for it, and here there is. Three cream
 * bars would read as three playheads, which is the mistake that bench
 * records from the other side ("the handles are light grey so two cream
 * bars do not read as three playheads"). So the playhead keeps the accent,
 * the selection is a lighter ground, and the handles are muted.
 */
.bo-band {
  position: absolute; top: 0; bottom: 0;
  background: rgba(237, 225, 211, 0.08);
  pointer-events: none;
}
/* THE TRACK'S OWN HEIGHT, AND A PIXEL WIDER - Babe, 2026-10-21: "lets make
   the trim handles like 1px wider and playhead... the height of these
   should match the height of the timeline bar." They overhung it by two
   pixels top and bottom, which read as three different heights on one strip
   once the buttons and the track agreed on one. */
.bo-h {
  position: absolute; top: 0; bottom: 0; width: 3px;
  transform: translateX(-50%);
  background: var(--muted);
  cursor: ew-resize;
  z-index: 2;
}
/* A 2px BAR IS NOT A 2px TARGET. The hit area is widened with a pseudo
   rather than with width, so the mark stays thin while the grab stays
   findable - the same trade the clip bench's handles make. */
.bo-h::before { content: ""; position: absolute; inset: -4px -5px; }
.bo-h:hover { background: var(--ink); }
.bo-track.is-trimmed .bo-h { background: var(--ink); }

/* THE PLAYHEAD IS THE ACCENT and sits above everything, because it is the
   one thing on this strip whose position IS the state. */
.bo-head {
  position: absolute; top: 0; bottom: 0; width: 3px;
  margin-left: -1.5px;
  background: var(--accent-text);
  pointer-events: none;
}
/* A KEY IS A DIAMOND, which is a square turned 45 degrees - the node's own
   mark, and the one shape on a timeline that is not a tick or a playhead. */
/* AND THE TWO TRACKS STACK - Babe, 2026-10-20: "the keyframes stack." One
   row meant a camera key and an object key at the same time drew on top of
   each other, and that pair is the commonest there is: you key the camera
   and the thing it is pointing at together. Two rows in the 22px track, the
   camera above and the object below. The numbers are measured rather than
   guessed - a 7px square turned 45 degrees is 9.9px corner to corner, so
   centres at 6px and 16px give 0.6-10.4 and 10.6-20.4 and the two rows
   clear each other by a fifth of a pixel inside a 22px bar. */
/* THE TWO ROWS ARE RE-SPACED FOR THE TALLER TRACK, by the same arithmetic
   that placed them in the 22px one: a 7px square turned 45 degrees is 9.9px
   corner to corner, so centres at 9px and 21px give 4.05-13.95 and
   16.05-25.95 - the rows clear each other by two pixels inside a 30px bar
   where they used to clear by a fifth of one.

   AND THE CURSOR SAYS IT SLIDES. A diamond is a thing you drag along the
   track now, so it wears the trim handles' own `ew-resize` rather than
   `context-menu` - the menu is still on the right button, and a cursor can
   only advertise one of the two. */
/* A DIAMOND IS A MARK INSIDE A TARGET, which is the shape that lets a key
   and a trim handle share one frame.
 *
 * It used to be one rotated element doing both jobs, with a `::before`
 * widening the grab - and a 17px pseudo inside a 45-degree rotation is 24px
 * corner to corner, so each row's hit area covered the WHOLE 30px track.
 * That is why putting keys above the handles made the handles unreachable
 * at their own line: there was no height left for them anywhere. Measured,
 * not reasoned about - `blockout-check` failed five assertions on it.
 *
 * So the box is unrotated and only as tall as its row, and the mark is a
 * rotated child inside it. The two boxes are 2-14 and 16-28 of a 30px
 * track, which leaves the handle the two outer strips, the two pixels
 * between the rows and its own six pixels of overhang either side - and the
 * middle of the track, which is where a hand grabs a 2px line. */
.bo-k {
  /* 1 AND 15 IN A 28px TRACK: two 12px rows with a pixel above, two
     between and one below - the same arithmetic as the 30, one notch in. */
  position: absolute; top: 1px; height: 12px;
  width: 14px; margin-left: -7px;
  display: grid; place-items: center;
  cursor: ew-resize;
}
.bo-k i {
  display: block; width: 7px; height: 7px;
  background: var(--ink);
  transform: rotate(45deg);
}
.bo-k.is-obj { top: 15px; }
/* A 7px DIAMOND IS NOT A 7px TARGET, and a key at either END of the shot
   is half outside the track - so the hit area is widened with a pseudo, the
   same trade the trim handles make. Without it the first and last keys, the
   two you set most, are the two hardest to grab. */
/* AND A DIAMOND BEATS A TRIM HANDLE WHERE THE TWO SIT ON ONE FRAME, which
   is the rule the track's own pointerdown already claims to follow - "a
   press on a diamond is a press on the diamond" - and did not, because
   `.bo-h` is z-index 2 and a key had no z-index at all. The in-point starts
   at 0s and so does the first key anybody sets, so the commonest key on the
   track was the one you could not pick up: the press moved the RECORDING
   RANGE instead, silently and in the other direction. Found by measuring a
   drag that did nothing and reading the range afterwards.
   THE HANDLE STAYS GRABBABLE because its own hit area reaches six pixels
   above and below the track, where no diamond does - the two key rows sit
   at 4-14 and 16-26 of a 30px bar. */
.bo-k { z-index: 3; }
/* THE COLOUR IS WRITTEN ON THE KEY NOW, because it is the object's own -
   Babe: "so maybe the object color is the keyframe color." The blocks
   already carry named colours so the sentence under the viewport can name
   them, and reusing them here means three keyed objects read apart at a
   glance where one shared accent said only "not the camera". This rule
   stays as the fallback for an import with no material colour to find. */
.bo-k.is-obj i { background: var(--accent-text); }
/* THE ONE YOU ARE HOLDING, which Del will take. A ring rather than a colour
   change, because the colour is already saying WHOSE key it is - the
   camera's or the selected object's - and one mark cannot carry two facts.
   Colour and shape only: this machine asks for reduced motion. */
.bo-k.is-sel i {
  box-shadow: 0 0 0 2px #050505, 0 0 0 3px var(--ink);
}
.bo-k.is-sel { z-index: 4; }
/* `through` IS A POSITIONAL WAYPOINT RATHER THAN A TIMING ANCHOR, so it is
   drawn hollow - the node's own convention, and the only way to see at a
   glance which keys the sampler is actually reading. */
/* `through` IS DRAWN HOLLOW IN WHATEVER COLOUR THE KEY IS, and both of
   those now come in on the element - these two stay as the fallback. */
.bo-k.is-through i { background: transparent; box-shadow: inset 0 0 0 1px var(--ink); }
.bo-k.is-obj.is-through i { box-shadow: inset 0 0 0 1px var(--accent-text); }

/* THE EASE MENU IS THE NODE'S, down to the heading: the ease belongs to the
   segment LANDING on a key, so "incoming" is load-bearing rather than
   decorative. */
.bo-ease {
  position: absolute; z-index: 6;
  min-width: 140px; padding: 4px 0;
  background: var(--raise-2);
  border: 1px solid rgba(237, 225, 211, 0.1);
  font-family: Oswald, system-ui, sans-serif;
}
.bo-ease-h {
  margin: 0; padding: 5px 10px 6px;
  color: var(--dim);
  font-size: 9px; letter-spacing: .1em; text-transform: uppercase;
}
.bo-ease button {
  appearance: none; border: 0; cursor: pointer;
  display: block; width: 100%; text-align: left;
  padding: 5px 10px 5px 22px;
  background: none; color: var(--ink);
  font: inherit; font-size: 11px; letter-spacing: .04em;
}
.bo-ease button:hover { background: rgba(237, 225, 211, 0.08); }
.bo-ease button.is-on { color: var(--accent-text); }
.bo-ease button.is-on::before {
  content: "\2713"; position: absolute; margin-left: -13px;
}
.bo-ease-x {
  margin-top: 4px; border-top: 1px solid rgba(237, 225, 211, 0.1) !important;
  color: var(--muted) !important;
}

.bo-dead {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  margin: 0; color: var(--dim);
  font-family: Archivo, system-ui, sans-serif; font-size: 12px;
}

/* A BOX IS THE ACCENT, because the accent marks the operation - the same
   rule the tile pictures follow. 2px, because 1px on a dark photograph
   disappears and 3px starts covering what you are pointing at. */
.boxer-b {
  position: absolute;
  border: 2px solid var(--accent);
  /* deliberately no fill - see boxDraw. A transparent box still takes
     pointer events over its whole area, so moving and picking are
     unaffected by there being nothing painted. */
  background: transparent;
  /* THE RECTANGLE IS A TARGET NOW, because it can be moved. It used to be
     inert so a press on it started the next box; a box you have drawn is
     repositioned far more often than it is drawn over, so the press moves
     it and a new box starts on bare picture instead. */
  pointer-events: auto;
  cursor: move;
}
/* THE ONE BEING WRITTEN FOR. A cream halo rather than a thicker border,
   because the border is the box's colour and its weight is what makes a
   rectangle readable on a photograph - changing it would also move every
   edge by a pixel, and the boxes are measured against the picture. */
/* HALF STRENGTH. At 0.85 the halo competed with the box's own border for
   the eye, which is the wrong way round - the border says which box this
   is, the halo only says it is the one being written for, and the prompt
   outline already says that in colour. Babe: "make the select outline
   more subtle, drop the opacity or something by half." */
.boxer-b.is-picked { outline: 1px solid rgba(237, 225, 211, 0.42); outline-offset: 2px; }

/* ---- A TAP, WHICH IS ONE PIXEL AND HAS TO BE FINDABLE ON A PHOTOGRAPH ----
   The house red says the same thing here it says on a tile picture: this
   is the operation. The cream ring is not decoration - a red dot on a red
   lantern is nothing at all, so the mark carries its own contrast with it
   and reads on any frame.

   `pointer-events: none` IS LOAD-BEARING. The dot is removed by tapping it
   again, hit-tested against the stored fraction rather than against this
   element, so a mark sitting on the part of the picture you most want to
   correct must not be able to swallow the press that corrects it.

   NO TRANSITION, because this machine asks for reduced motion and the dot
   has to be where it was put the frame it is put there. */
.boxer-p {
  position: absolute;
  width: 12px; height: 12px;
  margin: -6px 0 0 -6px;          /* centred ON the point, not beside it */
  border-radius: 50%;
  background: rgba(238, 35, 36, 0.92);
  box-shadow: 0 0 0 2px #EDE1D3, 0 0 0 3px rgba(5, 5, 5, 0.55);
  pointer-events: none;
}
/* THE CROP BENCH TAKES THE RECTANGLE AND NONE OF THE CHROME. Babe: "lets
   just remove the box label and x. we dont need it for this tool... or the
   lighter outline." The name, the cross and the picked outline all exist
   because Bounding Box has SEVERAL boxes and the prompt refers to them by
   name - a crop has one rectangle, nothing to call it, and nothing to tell
   it apart from. The bar also costs the bottom 16px of the thing you are
   pointing at, which is a bad trade when the thing you are pointing at is
   the crop itself. */
.side.is-cropping .boxer-bar { display: none; }
.side.is-cropping .boxer-b.is-picked { outline: none; }
/* THE BAR ALONG THE BOX'S FLOOR, holding the name and the cross. It was a
   chip on the line ABOVE the box, the way BFL's examples draw it, and that
   is fine until somebody boxes something at the top of the frame - then
   the name is off the picture and the box is anonymous. Inside, it cannot
   clip: the rectangle is already clamped to the picture, so anything
   within the rectangle is too.

   The bar owns the colour and the two controls inherit it, so a hue is
   set in one place per box rather than three. */
.boxer-bar {
  position: absolute; left: 0; right: 0; bottom: 0;
  height: 16px;
  display: flex; align-items: stretch;
  pointer-events: auto;
}
.boxer-l {
  flex: 1 1 auto; min-width: 0;
  margin: 0; padding: 0 4px;
  border: 0; border-radius: 0;
  background: transparent; color: inherit;
  /* CAPS, BUT IN THE PLAIN GROTESQUE RATHER THAN THE CONDENSED ONE. This
     label took three passes: Oswald caps read as a system token, Archivo
     lowercase read as the one piece of body copy in a surface made of
     caps, and Oswald caps again was too narrow sitting over a
     photograph. Babe, 2026-10-18: "can you use a more normal font for
     this... like the prompt header style or maybe even the commentary
     font. very basic sans serif." Archivo is what the commentary is set
     in - the app's reading face - and the caps are what make it a label.
     The id still TRAVELS lowercase, which is what BFL ask for; only the
     drawing of it has ever changed. */
  font-family: Archivo, system-ui, sans-serif; font-size: 10px;
  font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
}
/* FULL STRENGTH, BECAUSE THIS PLACEHOLDER IS NOT A HINT. A greyed
   placeholder is right when it describes what you COULD type; here it is
   the box's actual id - what the prompt points at and what the model is
   sent - and an artist who never renames a box still has `box_1` as its
   real name. Dimming it said "nothing here yet" about the one thing that
   was. The id is the same strength whether it was typed or defaulted,
   which is also the truth of it. */
.boxer-l::placeholder { color: currentColor; opacity: 1; }
.boxer-l:focus { outline: 1px solid currentColor; outline-offset: -2px; }

/* THE CROSS THAT TAKES A BOX AWAY. Top-right, outside the rectangle the
   way the chip sits outside it top-left, so neither covers the thing
   being pointed at. Its colours are set per box in the script, from the
   same hue the border and the chip take. */
/* the cross rides at the right-hand end of that bar, so the two controls
   are one object with the name rather than two things near a corner */
.boxer-x {
  flex: 0 0 auto;
  margin: 0; padding: 0 2px;
  /* 16px was the glyph's own width and nothing else - a press target the
     size of the letter in it. Babe: "the x is kind of narrow." */
  width: 24px;
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: 0;
  background: transparent; color: inherit;
  cursor: pointer;
  /* the cross rides in the same bar, so it takes the same face */
  font-family: Archivo, system-ui, sans-serif; font-size: 10px; line-height: 1;
  font-weight: 600; text-transform: uppercase;
  opacity: 0.75;
}
/* no block behind it - the bar is already a solid colour, and a second
   darker rectangle inside it read as a button sitting on a button */
.boxer-x:hover { opacity: 1; }


.chat {
  position: absolute; inset: 0; z-index: 3;
  /* The same black a queued SELECT square paints: --raise-2 at 0.4 over the
     pipe's own ground composites to rgb(7,7,7), measured off the pixel rather
     than guessed from the rule. The assistant covers the window, so it should
     read as the same empty dark the panel below it already has. */
  background: #070707;
  display: none;
  flex-direction: column;
}
.side.is-asking .chat { display: flex; }
/* A PICTURE DRAGGED ONTO THE ASSISTANT RIDES WITH THE NEXT QUESTION, so the
   whole of it answers the drag - the same outline a square and the grid
   already use, because it is the same gesture arriving somewhere else. */
.chat.is-over {
  outline: 1px solid var(--accent);
  outline-offset: -1px;
  background: var(--accent-sunk);
}

/* ---- the clip player, in the pane ----
   THE FIRST TRY PUT THIS OVER THE WHOLE WINDOW, borrowing the assistant's
   geometry, and Babe named what was wrong with it: "the issue is CUT is
   basically just a button at that point to reveal this mode". A tile that
   only reveals a mode is a door, not a tile. The player stands in the
   black pane instead - where the marks and the result stand - so the
   panel on the left can carry the options the way every workflow does. */
/* ---- THE CROP TOOL ----
   The same shape as the player: the picture takes the pane, a one-line bar
   under it says what you have. Babe, 2026-10-19: "i think the sheet or any
   image is brought to a CROP tool" - so it is a tool that takes any
   picture, and pulling one tile out of a contact sheet is simply a crop at
   a known cell rather than a feature of its own. */
.cropper { display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 0; }
.crop-pic {
  position: relative;
  flex: 1 1 auto; min-height: 0;
  display: grid; place-items: center;
  overflow: hidden;
  /* the drag is the control, so the I-beam would be a lie */
  cursor: crosshair;
  touch-action: none;
}
/* WIDTH AND HEIGHT WITH CONTAIN, never a percentage max - the same rule
   the player follows, and for the same reason: a sheet in here is 16:9 in
   a pane about 200px wide, and a percentage fit only bites when the
   parent height is definite. */
.crop-img { width: 100%; height: 100%; object-fit: contain; display: none; }
.crop-img.is-on { display: block; }
.crop-none {
  margin: 0; padding: 0 12px; text-align: center; color: var(--muted);
  font-size: 10px; letter-spacing: 0.05em; text-transform: uppercase;
}
/* ON THE PICTURE, NOT UNDER IT, so the crop takes the whole pane. Babe:
   "i think the crop preview can take up that full space too prob" - and
   the player's own numbers already work this way, for the same reason: a
   row of its own costs 20px of a pane that is mostly letterbox already. */
.crop-bar {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; align-items: center; gap: 6px;
  padding: 5px 8px;
  background: linear-gradient(to top, rgba(0,0,0,0.78), rgba(0,0,0,0));
  opacity: 0; transition: opacity 140ms var(--ease);
  pointer-events: none;
}
/* shown while you are looking at it, and whenever there is a crop to
   measure - the number is the answer to the gesture you just made */
.crop-pic:hover .crop-bar,
.crop-pic:has(.crop-img.is-on) .crop-bar { opacity: 1; }
.crop-size {
  flex: 1 1 auto; min-width: 0;
  font-size: 10px; letter-spacing: 0.04em; color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* what it would become, in the ink the rest of the panel uses for a value */
.crop-size b { color: var(--ink); font-weight: 600; }

/* ---- THE TRIM, in the pane where a result stands ----
   The crop bench's preview, one medium over: the picture takes the whole
   pane and what it measures sits on it in the corner rather than in a row
   of its own, because a row costs 20px of a pane that is mostly letterbox.
   It plays its own loop between the two points, which is the only way a
   clip can say what it is - a still frame of a trim is just a frame. */
.trim { display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 0; }
.trim-pic {
  position: relative;
  flex: 1 1 auto; min-height: 0;
  display: grid; place-items: center;
  overflow: hidden;
}
/* width and height with contain, never a percentage max - the rule the
   player and the crop preview both follow */
.trim-video {
  width: 100%; height: 100%; object-fit: contain; display: none;
  transform-origin: center center;
}
.trim-video.is-on { display: block; }
.trim-none {
  margin: 0; padding: 0 12px; text-align: center; color: var(--muted);
  font-size: 10px; letter-spacing: 0.05em; text-transform: uppercase;
}
.trim-bar {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; align-items: center; gap: 6px;
  padding: 5px 8px;
  background: linear-gradient(to top, rgba(0,0,0,0.78), rgba(0,0,0,0));
  opacity: 0; transition: opacity 140ms var(--ease);
  pointer-events: none;
}
/* ONLY WHEN THERE IS SOMETHING TO SIZE. The hover clause could only ever
   fire on an EMPTY pane - with a clip in it the rule below already keeps
   the bar up - so all it did was darken the bottom of an empty box and
   put a dash in it. Babe, 2026-10-19: "when nothing is there we can prob
   remove the hover gradient with the dash." A readout of nothing is worse
   than no readout, which is the rule the result bar under this pane
   already follows. */
.trim-pic:has(.trim-video.is-on) .trim-bar { opacity: 1; }
.trim-size {
  flex: 1 1 auto; min-width: 0;
  font-size: 10px; letter-spacing: 0.04em; color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* the frames, in the ink the rest of the panel uses for a value */
.trim-size b { color: var(--ink); font-weight: 600; }

.stage { display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 0; }
.stage-pic {
  position: relative;
  flex: 1 1 auto; min-height: 0;
  display: grid; place-items: center;
  overflow: hidden;
}
/* WIDTH AND HEIGHT WITH CONTAIN, never a percentage max: a percentage fit
   only bites when the parent height is definite, and a clip in here can be
   any shape in a pane about 200px wide. */
/* ---- THE FRAME THE CLIP SITS IN ----
   Sized in JS to the clip's own contained rect, so at rest it is exactly
   where the picture already was and nothing looks added. It clips, which
   is the whole mechanism: the clip is scaled and moved INSIDE it, and
   what the frame cuts off is what the crop cuts off.

   INSET IS THE FALLBACK, and it is load-bearing: videoWidth is 0 until
   the clip decodes, so there is a moment with no measurement to lay this
   out from - and an absolutely positioned box with no size at all is
   0x0, which would blank the picture between the src landing and the
   metadata arriving. */
.stage-frame { position: absolute; inset: 0; overflow: hidden; }
.stage-video { width: 100%; height: 100%; object-fit: contain; display: none; }
.stage-video.is-on { display: block; }
/* the clip is dragged, so the cursor says so - but only once there is
   room to drag it, which is the moment it is zoomed in. MULTIDIRECTIONAL
   rather than a hand, Babe 2026-10-19: the pan is clamped on both axes
   at once and the hand said "pick this up" where what you do is slide it
   under a fixed frame. Pressed keeps the same arrows - swapping to a
   closed fist mid-drag would say the gesture had changed. */
.stage.is-framed .stage-pic,
.stage.is-panning .stage-pic { cursor: move; }

/* ---- THE ZOOM, IN THE OTHER CORNER FROM THE NUMBERS ----
   The wheel and the drag are invisible, so the readout is not - and it
   is where FIT lives, which is what you want after losing yourself at
   400%. Top, because the clock and the frame count already have the
   floor. It stays up while the clip is framed, because then it is
   reporting something rather than offering something. */
.stage-zoom {
  position: absolute; left: 0; right: 0; top: 0;
  display: flex; align-items: center; gap: 8px;
  padding: 5px 8px;
  background: linear-gradient(to bottom, rgba(0,0,0,0.78), rgba(0,0,0,0));
  opacity: 0; transition: opacity 140ms var(--ease);
}
/* AND IT GOES WITH THE POINTER, like the clock and the frame count on
   the other corner. It used to stay up for as long as the clip was
   framed, on the argument that it was then REPORTING rather than
   offering - but a readout nailed to the picture is a readout you stop
   seeing, and the zoom is legible from the picture itself. Babe,
   2026-10-19: "can you make the fit and percentage zoom values
   disappear when you hover off like the timecode". Panning keeps it,
   because then the number is moving. */
.stage-pic:hover .stage-zoom,
.stage.is-panning .stage-zoom { opacity: 1; }
.zoom-b {
  background: none; border: 0; padding: 0 2px; cursor: pointer;
  color: var(--ink); flex: 0 0 auto;
  font: inherit; font-size: 10px; letter-spacing: 0.08em;
  line-height: 1; text-transform: uppercase;
}
.zoom-b:hover { color: var(--accent-text); }
.zoom-val {
  flex: 1 1 auto; text-align: right;
  font-size: 10px; letter-spacing: 0.04em; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
/* ---- THE CLIPS, ABOVE THE TRACK ----
   A row of what you have loaded, drawn only at two or more: with one clip
   the bench IS the thumbnail, so a strip of one would report what already
   fills the window. Babe, 2026-10-19: "small clip/rectangle above the
   scrubber in a row only if you add multiple - you click on each one to
   trim", and "to show its settings", which comes free because the spec
   panel reads the bench and the bench is what a press changes.

   SMALL, because this is a shelf rather than a viewer - the clip you are
   working on is the whole window above it. 44px holds a readable frame of
   a 16:9 clip and costs the picture less than the track does. */
.stage-strip {
  flex: 0 0 auto; display: flex; gap: 3px; align-items: center;
  padding: 4px 0 0;
}
.stage-strip[hidden] { display: none !important; }
.clip {
  position: relative; flex: 0 0 auto; width: 78px; height: 44px;
  border: 0; padding: 0; cursor: pointer; overflow: hidden;
  background: var(--raise); opacity: 0.55;
  transition: opacity 150ms var(--ease);
}
.clip video { width: 100%; height: 100%; object-fit: cover; display: block; }
.clip:hover { opacity: 0.8; }
/* THE WAY IN, at the end of the clips it adds to. It was drawn as a HOLE -
   darker than everything, on the argument that it is the one cell with
   nothing in it - and that made the one cell you have to FIND the quietest
   thing in the row. Babe, 2026-10-19: "can you make the upload slot square
   a little brighter". A door is a control, so it sits above the strip
   rather than below it, and the arrow comes up with it. */
.clip-add {
  display: flex; align-items: center; justify-content: center;
  background: var(--raise-2); opacity: 1; color: var(--muted);
  box-shadow: inset 0 0 0 1px var(--line);
}
.clip-add:hover { color: var(--accent-text); }
.clip-add svg { width: 16px; height: 16px; }
/* THE LIT ONE IS THE ONE ON THE BENCH, and it says so the way every other
   chosen thing here does: full strength against dimmed neighbours, with
   the accent as a rule under it. Colour only - this machine asks for
   reduced motion. */
.clip.is-on { opacity: 1; box-shadow: inset 0 -2px 0 var(--accent); }
.clip-n {
  position: absolute; left: 0; top: 0; padding: 1px 4px;
  background: rgba(0,0,0,0.65); color: var(--ink);
  font-size: 9px; font-weight: 600; letter-spacing: 0.06em;
  line-height: 1.4;
}
/* the way back out, or a wrong drop is permanent */
.clip-x {
  position: absolute; right: 0; top: 0; width: 15px; height: 15px;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0,0,0,0.65); color: var(--muted);
  font-size: 11px; line-height: 1; opacity: 0;
  transition: opacity 140ms var(--ease);
}
.clip:hover .clip-x { opacity: 1; }
.clip-x:hover { color: var(--accent-text); }

.stage-none {
  margin: 0; padding: 0 12px; text-align: center; color: var(--muted);
  font-size: 10px; letter-spacing: 0.05em; text-transform: uppercase;
}
/* PLAY SITS BESIDE THE TRACK, not under it. Babe: "i think we should add a
   play button with arrow in it on the left side of the timeline bar, which
   would give us more video space, trying to remove the black bars" - the
   second row was 20px off a picture that is mostly letterbox already. */
/* ON THE BENCH, NOT IN THE PANE. The bar is the width of the window now,
   so the track below it measures a frame in pixels rather than in
   fractions of one - at 960 the window is 454px against the 225px the
   pane gave it, and at 1920 it is more than twice that. */
.stage-bar {
  display: flex; align-items: center; gap: 8px;
  /* 5px, not 7: four pixels off the bar is four more of the picture, and
     the picture is the thing you are choosing frames out of. */
  padding: 5px 9px; background: var(--raise);
}
/* ---- the track ----
   TALLER, AND ON A ROW OF ITS OWN. Play and the clock beside it left about
   115px of a 225px pane for the scrubber, which is no width for two handles
   and a playhead. Babe: "i would rather the scrub be a little taller and
   have trim handles with a crop button". */
.stage-scrub {
  position: relative; height: 20px; width: 100%;
  background: #202020; cursor: pointer;
  touch-action: none;
}
/* WHAT WOULD BE KEPT IS THE COLOURED PART. The accent used to mark how far
   you had watched, stopping at the playhead - a progress bar in a track
   whose job is to say what survives the crop. The selection carries it
   now, and nothing else in here is accent-coloured. */
/* THE BRAND ACCENT, not the text one. This was the only FILL in the
   stylesheet reaching for --accent-text (#ff5248), which is the lightened
   red that exists so 10px labels stay legible on near-black - as a 20px bar
   it simply read as a different orange from every other accent surface in
   the app. The lit cell, Run's square, the send arrow, submit and the orbit
   chip all fill with --accent, and so does the selection now. */
.stage-sel { position: absolute; top: 0; bottom: 0; background: var(--accent); }
/* THE PLAYHEAD OUTRANKS THE HANDLES. Dragging a handle SEEKS to it, so the
   moment you set an in or out point the playhead is sitting exactly under
   that handle - and the grips come after it in the markup, so it painted
   behind one and looked lost. It is also outlined, because a cream line on
   an orange selection beside a grey handle is three near-tones in 3px. */
.stage-head {
  position: absolute; top: -5px; width: 2px; height: 30px;
  margin-left: -1px; z-index: 4;
  background: var(--ink);
  box-shadow: 0 0 0 1px rgba(0,0,0,0.6);
  /* A READOUT MUST NOT TAKE A PRESS, and this one was taking the most
     important press on the track. The playhead is a <span> nothing listens
     to - the SEEK lives on `.stage-scrub` underneath it - and at the start
     of a clip it parks at 0%, which is exactly where the IN handle lives
     and where every trim begins. At z-index 4 over the handle's z-index 3
     it covered the 3px bar you aim at, so pressing the handle you can SEE
     seeked instead of trimming, and the clip jumped.
     Measured, not guessed: `elementFromPoint` at the in-handle's centre
     answered `stage-head`, where the out-handle's answered `stage-grip`.
     Same family as the keyframe diamond on this bench's own trim handle -
     when two controls want one point, fix the SHAPE, not the order - and
     the shape here is that one of them is not a control at all. */
  pointer-events: none;
}
.stage-grip { z-index: 3; }
/* A HANDLE IS A PRESS TARGET FIRST. The bar it draws is 3px of cream, but
   the button is 13px wide and the full height of the track, so a pointer
   does not have to find a hairline. */
.stage-grip {
  position: absolute; top: 0; bottom: 0; width: 13px; margin-left: -6px;
  padding: 0; border: 0; background: none; cursor: ew-resize;
}
/* LIGHT GREY, NOT CREAM. Babe: "maybe the trim handles are a light grey so
   its not to be confused with the playhead" - two cream bars on one track
   read as three playheads, and only one of them is where the picture is. */
.stage-grip::before {
  content: ''; position: absolute; top: -2px; bottom: -2px; left: 5px;
  width: 3px; background: #b4b0ab;
}
.stage-grip:hover::before { background: #e4e0db; }
/* ---- the numbers, on the picture ----
   A DARK ROW THAT IS NOT THERE UNTIL YOU WANT IT. It fades in on hover and
   stays up while a handle is being dragged, because that is the moment the
   frame count matters and the pointer is on the TRACK rather than the
   picture. Colour and opacity only - no movement - so it reads the same
   under reduced motion, which this machine asks for. */
.stage-hud {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; padding: 5px 8px;
  background: linear-gradient(to top, rgba(0,0,0,0.78), rgba(0,0,0,0));
  opacity: 0; transition: opacity 140ms var(--ease);
  pointer-events: none;
}
.stage-pic:hover .stage-hud,
.stage.is-scrubbing .stage-hud { opacity: 1; }
.stage-play {
  background: none; border: 0; padding: 0; cursor: pointer;
  display: flex; color: var(--ink); flex: 0 0 auto;
}
.stage-play svg { width: 13px; height: 13px; fill: currentColor; stroke: none; }
.stage-time {
  flex: 1 1 auto; min-width: 0;
  font-size: 10px; letter-spacing: 0.04em; color: var(--muted);
  font-variant-numeric: tabular-nums;
}
/* the selection, in the vocabulary the node speaks */
.stage-mark {
  flex: 0 0 auto;
  font-size: 10px; letter-spacing: 0.04em; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
/* THE ACTIONS STAY IN SIGHT. The run form is a scroller and at 960 it is
   151px tall against 190px of content, so Crop and Save sat 30px below the
   fold of the panel they belong to - present, and findable only by
   scrolling a column whose other two controls are the clip and its specs.
   Pinned to the bottom, the inputs scroll under them instead. */
.grp-acts {
  position: sticky; bottom: 0; z-index: 2;
  background: var(--sunk);
}
/* ONE ROW AGAIN, AND SMALLER TO EARN IT. Babe, 2026-10-19: "make the
   buttons smaller so we can fit them all on one line." Dividing the row
   had bought the type back up to 10px; five cells take it down to 9 - the
   trade in the other direction, and this time asked for.

   MEASURED AT FOUR WIDTHS rather than at one, because the last three
   numbers this row was given were each right at exactly one window. Five
   cells leave 33-36px of usable width between 960 and 1920, REVERSE is
   the longest word at 37px at 10px and 33 at 9, and EXTRACT 36 and 32.
   So 9px fits everywhere and needs no breakpoint - which is the point:
   a layout that changes with the window is the bug this row keeps
   finding. JOIN is usually absent, and the other four simply share the
   room it is not taking. */
/* 3px, not 2. The row went to 2 to buy width for a fifth cell and the
   cells needed none of it - the longest word is 33px of a 49px cell at
   every width - while a lighter ground made five buttons 2px apart read
   as one block. Measured before and after: nothing clips either way. */
.stage-row { display: flex; gap: 3px; }
.stage-row + .stage-row { margin-top: 3px; }
/* The actions live in the form now, where every other control is a full
   width row of the 246px column. Disabled is the state they ship in: a
   tool with no clip has nothing to take a frame from. */
/* DISABLED BEATS THE HANDOFF'S OWN COLOUR. `.is-go` is muted and `:disabled`
   is dim, and both are two selectors - so PREP sat there looking brighter
   than the three live buttons beside it while refusing a press. */
.stage-act:disabled,
.stage-act.is-go:disabled { color: var(--dim); cursor: default; }

/* A TOGGLE HAS TO SAY SO, IN COLOUR. REVERSE is the one press in this row
   that stays down - the other edits keep a gesture already made on the
   clip, this one is the edit - so the word takes the accent while it is
   on. Colour and nothing else, because this machine asks for reduced
   motion, and it beats :hover by sitting after it. */
.stage-act.is-on { color: var(--accent-text); background: var(--accent-sunk); }

/* EQUAL CELLS. The row was divided in the proportion the words needed,
   which left four buttons of three different widths - Babe: "buttons are
   all equal sizes". The words have to fit the cell now rather than the
   cell the words. */
/* THE WAY OUT IS NOT ONE OF THE THREE. It sits on its own row, quieter than
   them, because pressing it leaves this tool rather than doing something
   here - the same distinction the run panel draws between USE and RUN. */
.stage-act.is-go { color: var(--muted); }
.stage-act.is-go:not(:disabled):hover { color: var(--ink); }
.stage-act {
  /* min-width: 0, or these are not equal. A flex item's default minimum is
     its CONTENT, so the EXTRACT cell quietly took the width of its own word
     and the other three shared what was left - which is exactly the
     unevenness this was meant to fix, invisible because every measurement of
     that cell came back "the word fits". */
  /* LIGHTER THAN THE PAIR UNDER THE PANE. Babe, 2026-10-19: "can we make
     the action buttons a lighter grey... so they look different from the
     use and save". They were a step DARKER than USE and SAVE, which read
     as the same control twice at a glance - and the two rows are not the
     same kind of thing: ACTIONS are what you do to the clip on the
     bench, USE and SAVE are what you do with a result. A step apart in
     the other direction says so with no word added. */
  flex: 1 1 0; min-width: 0; border: 0; cursor: pointer;
  background: #2e2e2e; color: var(--ink);
  /* SHORTER, AND NARROWER. Four cells in one 181px row leave 45px each,
     so the side padding comes down to 6 - and the height with it, because
     every pixel this row gives up is one the input square above it takes.
     Babe: "we can make the buttons shorter so we can fit four". */
  /* FOUR LABELS IN 181px, MEASURED: the words need 138 of it, the padding
     32 and the gaps 9. There is no slack for the 0.08em tracking the rest
     of this panel uses - a tighter row is the trade for a fourth cell, and
     the alternative was cutting a word down to an initial. */
  padding: 6px 2px;
  /* AND THE 9px WENT BACK UP WHEN THE ROW WAS DIVIDED. A quarter of 181px
     is 45 and EXTRACT needs 48, which is what had cost this row the only
     9px in the panel; a THIRD is 58 and a half is 89, so three verbs and
     two verbs both fit at the size everything else here is set in.
     Dividing the row paid for the type, which is the house rule arriving
     at its own answer: give the new thing a cell rather than cramming the
     row it joins. */
  font: inherit; font-size: 9px; letter-spacing: 0.015em; text-transform: uppercase;
  transition: background 150ms var(--ease);
}
/* THE WORD HIGHLIGHTS, NOT THE CELL. Filling a whole cell in a row of four
   says "this one is selected" rather than "your pointer is here" - Babe:
   "i think they do but its hard to tell with the entire cell highlight". So
   the ground stays put and the word takes the accent, which is what every
   other pressable word in this panel does. */
.stage-act:not(:disabled):hover { color: var(--accent-text); }

/* ---- something landed in the grid ----
   Extract puts a picture in your assets while you are looking at a clip, so
   the door to the grid says so. COLOUR, NOT MOVEMENT: this machine asks for
   reduced motion, and a flash that moved would say nothing on it. */
/* AND A SQUARE THAT REFUSED SOMETHING SAYS SO THE SAME WAY, for the same
   reason: the only signal this machine can be trusted to show is colour.
   A drop that does nothing at all reads as a broken square. */
.slot.is-no { outline: 1px solid var(--accent); outline-offset: -1px;
              background: var(--accent-sunk); }

.pipe-btn.is-new { color: var(--accent-text); background: var(--accent-sunk); }
.pipe-btn.is-new svg { stroke: var(--accent-text); }


.chat-log {
  flex: 1 1 auto; min-height: 0; overflow-y: auto;
  padding: 18px 20px; display: flex; flex-direction: column; gap: 14px;
}

/* Neither of these needs a track drawn down it: the log keeps itself at the
   bottom, and the composer is two rows deep. They both still scroll. */
.chat-log,
.chat-ask textarea { scrollbar-width: none; }
.chat-log::-webkit-scrollbar,
.chat-ask textarea::-webkit-scrollbar { width: 0; height: 0; display: none; }
.chat-line { max-width: 72ch; font-size: 14px; line-height: 1.55; }
/* BOTH SIDES DOWN ONE EDGE. Babe: "do you think its weird that you is on
   the right and the agent is on the left?" - yes, and the WIDTH is why.
   Right-aligning your own lines is the phone convention, which survives
   there because the column is narrow enough to hold both sides in one
   glance; this column is ~950px at 1920, so a two-word instruction sat 900px
   from the answer it produced and the two halves stopped reading as one
   conversation. The speaker labels were already doing the job the alignment
   was doing a second time. */
.chat-line.is-artist { align-self: flex-start; text-align: left; color: var(--ink); }
.chat-line.is-gemma  { align-self: flex-start; color: var(--muted); }
.chat-line b {
  display: block; font-size: 9px; letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--dim); font-weight: 600; margin-bottom: 4px;
}
/* DOWN THE SAME EDGE AS EVERYTHING ELSE. Babe: "when you drag and asset
   into the agent. lets keep it aligned left". This sat on the right
   because the artist's own lines did, and it was missed when they
   straightened - a held attachment belongs to the question you are
   about to ask, so it reads under the thread rather than away from it. */
.chat-shot {
  align-self: flex-start; display: flex; align-items: center; gap: 8px;
  background: var(--raise); padding: 6px; max-width: 72ch;
}
/* BIGGER, AND THE SAME SIZE AS THE ONE ON A SENT QUESTION. Babe: "we
   dont need the text really, it can be a bigger thumbnail" - 46px was
   sized to sit beside a line of type, and with the type gone there is
   nothing for it to be beside. 110x62 is what `.chat-said-shot` draws,
   so the picture you are about to send and the picture you just sent
   are one object at one size rather than two that merely rhyme. */
/* A SQUARE, AT THE ASSET GRID'S OWN SIZE. Babe: "make the inline chat
   asset a square like the asset size". A tile there is `aspect-ratio: 1/1`
   in a `minmax(120px, 1fr)` grid, so this is the same object at the same
   size rather than a thumbnail that merely rhymes with one. */
.chat-shot img { width: 120px; height: 120px; object-fit: cover; }
/* AND THE PICTURE ON A SENT QUESTION, WHICH WAS THE SAME LEFTOVER.
   `margin-left: auto` is what pushed it right, and it is the kind that
   hides: nothing names a side, so the rule reads as spacing. */
.chat-said-shot {
  display: block; margin: 0 auto 6px 0;
  width: 110px; height: 62px; object-fit: cover;
}
.chat-line.is-gemma.is-waiting { color: var(--dim); font-style: italic; }

/* Under what the agent said, not beside it: the answer is the thing being
   read, and the ways to take it sit quietly underneath. */
/* Two buttons of the same height, in their own cells: take it, or run it. */
.line-acts { display: flex; align-items: center; gap: 4px; margin-top: 8px; }
/* BIGGER AND WITH MORE GROUND UNDER THEM. Babe: "use and run and copy could
   be a little bigger... to stand out more". Measured first, because the
   wording pointed the other way: the chat sits on rgb(7,7,7) and these
   chips on rgb(35,35,35), so they are already the LIGHTER thing and a
   darker grey would sink them into the panel. The contrast goes up instead,
   and the word takes #a8a199 - which this file already describes as "a step
   brighter than --muted: these are live controls, not labels". */
.line-copy {
  border: 0; background: #2b2b2b; padding: 0; cursor: pointer;
  width: 24px; height: 24px;
  display: inline-grid; place-items: center;
  color: #a8a199;
  transition: background 150ms var(--ease), color 150ms var(--ease);
}
.line-copy svg { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 1.6; }
.line-copy:hover { background: #2e2e2e; color: var(--ink); }
/* Copied: the square itself lights. No word beside it - the press was the
   question, the colour is the answer, and nothing moves to say so. It is a
   colour change, not an animation, so a machine asking for less motion still
   sees it. */
.line-copy.is-done,
.line-copy.is-done:hover { background: var(--accent); color: #0a0a0a; }

/* One press from an answer to a run. It sits with the copy icon, quiet until
   wanted - the orange belongs to Run up in the panel. */
/* AND ONLY WHILE THERE IS A WORKFLOW TO PUT IT IN. Babe: "use only really
   makes sense if you are currently in a workflow", then "run as well" - and
   both HAVE been gated since they were built, by
   `.side:not(:has(.run-prompt))`. SOLO BROKE THAT REACH: the chat is moved
   out of `.side` into the empty seat, so a selector anchored on the side
   stopped matching and the buttons came back. The rule was right; its
   anchor was the thing that moved.

   SO THE FACT IS WRITTEN ON THE CHAT, by live.js, which knows where the
   chat is standing and already asks this question for the context line.
   One rule, in one place, instead of two that can disagree. */
.chat:not(.in-wf) .line-run,
.chat:not(.in-wf) .line-use { display: none; }

/* USE is the quieter half of the pair: it hands the prompt back for editing,
   where RUN spends a move. Same size and ground, and only RUN takes the accent
   on hover, because only one of them is irreversible. */
.line-use:hover { background: #2e2e2e; color: var(--ink); }

.line-use,
.line-run {
  background: #2b2b2b; color: #a8a199; border: 0; cursor: pointer;
  font: inherit; font-size: 10.5px; font-weight: 600;
  letter-spacing: 0.07em; text-transform: uppercase;
  height: 24px; padding: 0 11px;
  transition: background 150ms var(--ease), color 150ms var(--ease);
}
.line-run:hover { background: var(--accent); color: #0a0a0a; }
.line-run[disabled] { opacity: 0.35; cursor: default; background: #2b2b2b; color: #a8a199; }

/* The composer reads as its own row: a band across the bottom with a rule
   above it, not a field floating in the dark. Sending is a quiet grey icon -
   the orange belongs to Ask up in the nav, which is the thing you look for. */
/* ---- what the agent has already been told ----
   SMALL, QUIET AND ABOVE THE COMPOSER, where it is read in the moment
   before you ask rather than while you are reading an answer. It is a
   LABEL and its value, the same two-part shape the rail cells use. */
.chat-ctx {
  flex: 0 0 auto;
  display: flex; align-items: baseline; gap: 8px;
  /* THE LOG'S OWN INSET, measured rather than guessed: `.chat-log` is
     `padding: 18px 20px` and the composer sits on the same 20, so every
     line in this column starts at one edge. At 2px this one hung off the
     side of its own panel. */
  padding: 0 20px 8px;
  min-width: 0;
  font: 600 9px/1.4 Oswald, sans-serif;
  letter-spacing: 0.09em; text-transform: uppercase;
}
.chat-ctx[hidden] { display: none !important; }
.chat-ctx-k { flex: 0 0 auto; color: var(--dim); }
/* THE VALUE CAN BE LONG - a tile, a model and a filename - so it gives
   rather than pushing the composer around. */
.chat-ctx-v {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--muted);
}

.chat-ask {
  flex: 0 0 auto;
  display: flex;
  align-items: stretch;
  /* The same 3px the header row leaves between ASSETS and RUN, showing the
     same dark behind it - the box and the arrow are two cells of a row, not
     a field with a button stuck to its end. */
  gap: 3px;
  /* No rule above it either: the row is 46px of cells on the dark, exactly
     the header below it, and the dark between them IS the seam. */
  background: var(--sunk);
}
.chat-ask textarea {
  /* the same 46px the pipe header is, so the two rows read as one grid */
  flex: 1 1 auto; resize: none; height: 46px;
  background: var(--raise); border: 0; color: var(--ink);
  font: 400 13px/1.5 Archivo, sans-serif;
  /* 20px, NOT 16: the log is `padding: 18px 20px` and the context line
     sits on the same 20, so at 16 the placeholder started 4px left of
     everything above it. Babe: "lets have the context text line up with
     the other text above / below" - measured, the context and the chat
     lines were already identical at 674 and THIS was the odd one at 670,
     so the composer moved rather than the line. One edge down the
     column. */
  padding: 13px 20px;
}
.chat-ask textarea:focus { outline: 0; background: #121212; }
.chat-ask textarea::placeholder { color: var(--dim); }
.chat-send {
  /* the same width as RUN, which sits directly under it */
  flex: 0 0 auto; width: var(--run-w); border: 0; cursor: pointer;
  /* the arrow sits bright at rest - it is the one thing in the bar you press */
  background: var(--raise); color: var(--ink);
  display: grid; place-items: center;
  transition: background 150ms var(--ease), color 150ms var(--ease);
}
.chat-send svg { width: 19px; height: 19px; fill: currentColor; }
.chat-send:hover:not(:disabled) { background: #262626; color: var(--ink); }
.chat-send:disabled { color: #3f3b36; cursor: default; }

/* ---- what the commentator says, along the bottom ---- */

/* One line, always. However long the commentator runs on, the rail is one row
   high and the layout does not move: a line too long for its cell slides left
   far enough to be read, then back. */
.called {
  flex: 1 1 auto; min-width: 0; overflow: hidden;
  white-space: nowrap;
  color: var(--muted);
  /* The rail is set in Oswald, which is right for the labels either side of
     this - GAME MASTER, WATCHING - because they are two short words in caps.
     It is wrong for a whole sentence: condensed, tight-tracked and 11px, a
     line of commentary turns into a wall to be decoded rather than read at a
     glance, which is the one thing a ticker cannot afford. So the line takes
     the body face at its own size and normal tracking, and only the line. */
  font: 400 12.5px/1.4 Archivo, system-ui, sans-serif;
  letter-spacing: 0;
}
.called .said { display: inline-block; }
.called.is-live { color: var(--ink); }

/* while it is sliding, both ends fade - otherwise a word is chopped in half
   against the edge of the cell and reads as a rendering fault */
.called.is-sliding {
  mask-image: linear-gradient(90deg, transparent 0, #000 14px, #000 calc(100% - 20px), transparent 100%);
}

@keyframes rail-slide {
  0%, 14%   { transform: translateX(0); }
  86%, 100% { transform: translateX(var(--shift, 0)); }
}
.called .said.is-run {
  animation: rail-slide var(--dur, 12s) linear infinite alternate;
}
/* Nobody who asked for less motion gets a sliding ticker, so for them the
   line is cut with an ellipsis instead - which is what the inline-block has
   to be told to do itself. */
@media (prefers-reduced-motion: reduce) {
  html:not(.wants-motion) .called .said.is-run {
    animation: none;
    max-width: 100%; overflow: hidden; text-overflow: ellipsis;
  }
}

/* ---- workflow list ---- */

.wf {
  position: relative;
  display: flex; flex-direction: column; gap: 3px;
  background: transparent;
  padding: 0; min-width: 0; min-height: 0;
  border: 0; cursor: pointer; text-align: left; color: var(--ink);
}
/* The picture takes whatever the two lines of text leave, so a taller row is
   a bigger thumbnail rather than a bigger gap. */
.wf img {
  width: 100%; height: auto; flex: 1 1 0; min-height: 0;
  object-fit: cover; background: #000;
  border-radius: 5px;
  outline: 2px solid transparent; outline-offset: -2px;
  transition: outline-color 150ms var(--ease);
}
.wf:hover img { outline-color: var(--accent); }
.wf-empty { cursor: default; }
/* in the catalogue but with no graph behind it */
.wf-off { opacity: 0.38; cursor: not-allowed; }
/* AND A MODEL ROW WITH NO GRAPH READS THE SAME WAY, because one look
   should mean one thing wherever it appears: this is not pickable. */
.model-opt.opt-off { opacity: 0.38; cursor: not-allowed; }
.wf-off:hover img, .wf-off:hover .wf-thumb { outline-color: transparent; }
.wf-empty .wf-thumb { opacity: 0.45; }
.wf-thumb {
  display: block;
  width: 100%; height: auto; flex: 1 1 0; min-height: 0;
  background: var(--raise-2);
  border-radius: 5px;
  outline: 2px solid transparent;
  outline-offset: -2px;
  transition: outline-color 150ms var(--ease), background 150ms var(--ease);
}
.wf:hover .wf-thumb { outline-color: var(--accent); background: #232323; }
.wf-dot { display: none; }

/* A TILE HOLDING A PHOTOGRAPH PUTS ITS WORDS UNDER IT.
   The caption sits in the box's bottom-left corner everywhere else, which
   works because the box is flat grey and the shadow on the type is doing
   nothing. Over a face it is the opposite problem: a light picture eats
   white type whatever the shadow does, and the one thing a thumbnail is
   for is being LOOKED at. So a tile with a picture stacks - picture,
   then the two lines - which is also how Babe drew it. The other tiles in
   the row keep their corner, and the baselines land close enough that the
   row still reads as a row. */
.wf-pic {
  display: block; width: 100%; height: auto; flex: 1 1 0; min-height: 0;
  object-fit: cover; background: #000;
  border-radius: 5px;
  outline: 2px solid transparent; outline-offset: -2px;
  transition: outline-color 150ms var(--ease);
}
.wf:hover .wf-pic { outline-color: var(--accent); }
.wf-off:hover .wf-pic { outline-color: transparent; }
/* THE PICTURE SITS INSIDE THE CARD, not instead of it. Without this the
   tile is a photograph with two lines under it on the panel floor, and the
   two tiles beside it are grey cards - three boxes, two shapes. The card
   takes the grey `.wf-thumb` would have been, the picture is inset by the
   same 10px the words are, and what the eye lines up on is one edge rather
   than two. */
.wf:has(.wf-pic) {
  background: var(--raise-2);
  border-radius: 5px;
  padding: 10px;
  gap: 9px;
  outline: 2px solid transparent; outline-offset: -2px;
  transition: outline-color 150ms var(--ease), background 150ms var(--ease);
}
.wf:has(.wf-pic):hover { background: #232323; outline-color: var(--accent); }
/* the lift belongs to the CARD now, so the picture inside it keeps still */
.wf:hover .wf-pic { outline-color: transparent; }
.wf:has(.wf-pic) .wf-cap {
  position: static; left: auto; right: auto; bottom: auto;
  padding: 0;
  text-shadow: none;
}

/* ---- what a tile says, in its own bottom-left corner ----
   The card at the top of the screen in miniature: the name in the lockup's
   face, the line under it in the one LIVE is set in - same size, same
   tracking, without the red, which up there means something else. The
   text-shadow is for the day these boxes hold a picture instead of a
   flat grey; on the grey it reads as nothing. */
.wf-cap {
  position: absolute; z-index: 2; left: 12px; right: 12px; bottom: 10px;
  display: flex; flex-direction: column; gap: 2px;
  min-width: 0; pointer-events: none;
  text-shadow: 0 1px 10px rgba(0,0,0,0.75);
}
.wf-name {
  font-family: Oswald, Archivo, sans-serif; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.03em;
  font-size: 15px; line-height: 1.1; color: var(--ink);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* ONE line. Two were allowed and six of the twelve took them, which reads as
   a paragraph sitting on a thumbnail rather than a caption under one. A line
   that will not fit is a line to rewrite, so this ellipsises instead of
   wrapping: the fault then shows up in the words, where it can be fixed. */
/* TRACKING IS WIDTH. At 10px uppercase, 0.1em is a pixel per character -
   22px of a 172px line spent on air, in a box that is 129px wide. Half
   that reads the same at this size and buys two words. */
.wf-t {
  font-size: 10px; letter-spacing: 0.05em; text-transform: uppercase;
  line-height: 1.3; color: var(--muted);
  white-space: normal; overflow: hidden;
  display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical;
}

/* ---- the mini UI ---- */


.slot img, .slot video { width: 100%; height: 100%; object-fit: cover; }

/* ---- THE PROMPT IS TWO LAYERS NOW ----
   A textarea paints its value as one run of text, so no word inside it can
   take a colour of its own. The styled copy is therefore drawn BEHIND it
   and the textarea is left transparent with a visible caret: you type into
   the real control, and what you read is the backdrop.

   EVERY METRIC HAS TO MATCH. Font, size, padding, line height and the
   wrapping rules are shared between the two, because a single pixel of
   disagreement slides the text off its own highlight further with every
   line. They are written once here, on both selectors, rather than twice
   in two places that can drift. */
.prompt-wrap {
  position: relative;
  width: 100%; min-height: 46px;
  display: flex;
  background: #232323;
  transition: background 150ms var(--ease);
}
.prompt-wrap:hover { background: #2a2a2a; }
.prompt-hi,
.run-prompt {
  font: inherit; font-size: 12px; line-height: 1.45;
  padding: 8px 9px;
  white-space: pre-wrap; overflow-wrap: break-word;
  border: 0;
}
/* THE BACKDROP PAINTS BLOCKS, NOT WORDS - and that distinction is the
   whole safety of this thing. It drew the text at first, with the
   textarea left transparent over it, and that made EVERY programmatic
   write to .run-prompt a trap: anything that set the value without
   repainting left a prompt that looked completely empty while holding a
   full sentence. Babe hit it within minutes - "i hit change and dont see
   a temp prompt" - on a mark that had written its words correctly.

   So the textarea draws its own text again, the way it always did, and
   this layer is behind it with its ink turned off: all it contributes is
   the coloured rectangle under a reference. A missed repaint now costs
   the COLOUR and nothing else, which is a cosmetic lag instead of a
   prompt that appears to have vanished. */
.prompt-hi {
  position: absolute; inset: 0;
  margin: 0;
  overflow: hidden;
  pointer-events: none;
  color: transparent;
}
.run-prompt {
  /* The group behind it is --raise-2. A box the same colour as the thing it
     sits in is not a box, so the WRAP is lifted a step - the same step a
     slot takes - and it is the lift that says "type here". The control
     itself is now glass: transparent ground so the backdrop shows, and
     transparent ink so the glyphs are not drawn twice, half a pixel
     apart. The caret is put back explicitly or there is nothing to aim. */
  position: relative; z-index: 1;
  flex: 1 1 auto; width: 100%;
  background: transparent; color: var(--ink);
  resize: none;
}
/* ONE RING, NOT TWO. The app draws a focus ring on every control, and the
   picked box draws its colour on the WRAP - so typing into a prompt that
   belonged to a box gave you both, a red ring inside the box's own ring.
   Babe, 2026-10-18: "i dont think we need the double outline."

   The wrap keeps the job in both states. With a box picked its colour is
   set inline, and an inline style outranks the rule below, so the box
   wins and focus changes nothing; with none picked there is no inline
   style and this is the ring you get. The textarea itself never draws
   one, which is what removes the second. */
.run-prompt:focus, .run-prompt:focus-visible { outline: none; }
.prompt-wrap:focus-within { outline: 2px solid var(--accent); outline-offset: -2px; }

/* AND THE ID INSIDE THE SENTENCE TAKES ITS BOX'S COLOUR. Babe, 2026-10-18:
   "so when prompting to remove a chair, you would say remove the chair in
   [BOX_1] and [BOX_1] appears in the same color block as the bounding box."
   The padding is horizontal only - a vertical one would change the line
   box and push the backdrop out of step with the textarea. */
/* the block only - its own text stays transparent, because the textarea
   above is already drawing those same characters in the ordinary ink */
.prompt-tok {
  color: transparent;
  border-radius: 2px;
}
.run-prompt::placeholder { color: var(--muted); }
.run-opts { display: flex; gap: 3px; flex-wrap: wrap; }
.opt {
  background: var(--raise-2); color: var(--muted);
  padding: 6px 9px; font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase;
  border: 0; cursor: pointer;
}
.opt:hover { color: var(--ink); }
.opt b { color: var(--ink); font-weight: 500; }

.pipe[data-pick="1"] .asset-grid img,
.pipe[data-pick="1"] .asset-slot { cursor: pointer; outline: 2px solid transparent; }
.pipe[data-pick="1"] .asset-grid img:hover,
.pipe[data-pick="1"] .asset-grid video:hover { outline-color: var(--ink); }


/* ============================ SCROLLBARS ============================ */

* { scrollbar-width: thin; scrollbar-color: #2e2e2e transparent; }

::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
  background: #2e2e2e;
  border-radius: 4px;
  border: 2px solid transparent;
  background-clip: content-box;
}
::-webkit-scrollbar-thumb:hover { background: var(--accent); background-clip: content-box; }
::-webkit-scrollbar-corner { background: transparent; }

/* Inside the panel there is no scrollbar at all. Two reasons: current
   Chrome honours `scrollbar-width: thin` and then IGNORES the
   ::-webkit-scrollbar rules above, so what actually appeared was the
   operating system's own bar, arrows and all, in a 246px column; and that
   bar takes real width out of a form that is already tight, which pulls
   every row in the stack off the panel's right edge. The chat log and the
   filter strip were already handled this way. The content still scrolls;
   a row cut in half at the bottom is the sign that it does.

   .chain JOINED THEM when the strip became a reel. It never scrolled before
   - three squares with no floor under them simply got narrower - so it was
   never in this list, and the first thing a fourth generation did was put an
   8px bar across the bottom of the row and take that height out of every
   square in it. The tile cut off at the right edge is the sign that there is
   more, the same bet the filter strip already makes. */
.chain,
.view-list,
.asset-grid,
.model-opts {
  scrollbar-width: none;
  -ms-overflow-style: none;
}
.chain::-webkit-scrollbar,
.view-list::-webkit-scrollbar,
.asset-grid::-webkit-scrollbar,
.model-opts::-webkit-scrollbar { width: 0; height: 0; display: none; }

/* THE PARAMETER PANEL KEEPS ITS BAR, because it is the one scroller whose
   hidden half is WORK rather than more of the same. Measured at 960x870
   with four input squares: the form shows 151px of 327px, so the PROMPT
   and the SETTINGS are both under the fold - and nothing on screen said
   so. Babe, 2026-10-18: "we may still need a scroll bar, mostly because
   people wont find the settings... you can currently scroll with
   scrollwheel no matter what but seeing a scrollbar is a nice way to know
   there is more below."

   The other four stay hidden on purpose: a reel, a tile list and an asset
   grid all cut a tile in half at the edge, which says "there is more" by
   itself. A stack of full-width rows does not - a row ending flush at the
   bottom looks like the end of the form.

   AND IT HAS TO OPT OUT OF 'scrollbar-width' TO BE STYLED AT ALL. Current
   Chrome honours the standard property and then IGNORES every
   ::-webkit-scrollbar rule, which is what put the operating system's own
   bar in a 246px column the last time this was tried - so both standard
   properties go back to 'auto' here and the pseudo-elements do the work.
   6px rather than the global 8, because this one lives inside the panel. */
.run-form {
  scrollbar-width: auto;
  scrollbar-color: auto;
  -ms-overflow-style: auto;
}
.run-form::-webkit-scrollbar { width: 6px; height: 6px; }
/* AND THE TRACK IS DRAWN, not left open - Babe, 2026-10-21: "can you make
   the scrollbar background a little brighter... the dark gap is a little
   weird." Transparent meant the 6px the bar reserves showed the PAGE
   through it, so between the panel's groups and the pane there was a strip
   of near-black with a pale thumb floating in it, reading as a gap in the
   layout rather than as a channel the thumb runs in. `--raise` is the tone
   the panel's own surfaces sit on, so the bar now reads as part of the
   panel and the thumb as something moving along it. */
.run-form::-webkit-scrollbar-track { background: var(--raise); }
.run-form::-webkit-scrollbar-thumb {
  /* --dim, which is the colour every other quiet marker in the panel uses.
     It has to be SEEN - the whole job of this bar is telling you there is
     more below - so it sits a step up from the global #2e2e2e thumb. */
  background: var(--dim);
  border-radius: 3px;
}
.run-form::-webkit-scrollbar-thumb:hover { background: var(--muted); }


/* ============================ THE RUN VIEW ============================ */

/* What you are asking for, and what came back. They are two different things
   and the panel says so with a seam on the same 3px grid as everything else,
   rather than letting the form run straight into the picture. */
.run {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  /* The row has to be capped, not auto. An auto row grows to whatever the
     picture wants, so a tall result pushed its own bottom out of the panel and
     got clipped - `max-height: 100%` further down was measuring against a row
     that had already given up. */
  grid-template-rows: minmax(0, 1fr);
  /* THE SEAM IS THE SAME CUT AS THE ONES BETWEEN THE GROUPS - Babe,
     2026-10-21: "i think we still want that vertical gap tho. it just
     wasn't there where the blockview bar was. so it can be the same size
     as the gaps that separate each panel on the left."

     It went to ZERO for an hour on the reading that it had become a
     gutter - and the gutter was never the problem. What looked wrong was
     the PICTURE being inset by its letterbox while the bar under it ran
     the full width, so one cut appeared to have two widths. With the
     letterbox gone the seam reads as a seam again, and it is 3px because
     that is what separates the groups on the other side of it: one value
     for every cut in this panel rather than one for each. */
  gap: 3px;
  background: var(--bg);
  height: 100%;
  min-height: 0;
}

.run-form {
  display: flex; flex-direction: column; gap: 3px;
  min-height: 0; height: 100%; overflow-y: auto;
  align-content: start;
}

/* ---- the model row ----
   Not a group: there is nothing to open, it is one line that is the whole
   control. It takes a group's lift and a summary's type so it sits in the
   stack as one of them, and the chevron points down because that is where
   its list comes from. */
.run-model { position: relative; }
.model-btn {
  width: 100%;
  display: flex; align-items: center; gap: 7px;
  padding: 8px 9px;
  background: none; border: 0; cursor: pointer;
  font-size: 10px; font-weight: 600; letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--muted); text-align: left;
  transition: color 150ms var(--ease);
}
.model-btn:hover { color: var(--ink); }
/* the picked name is the thing you read on this row, so it goes to full ink
   while the placeholder stays the colour of every other label */
[data-picked] .model-val { color: var(--ink); }
.model-val { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The same triangle the groups carry, in the same place and turning the
   same way: this row opens something too. */
.model-btn::before {
  content: "";
  flex: 0 0 auto;
  width: 0; height: 0;
  margin: 0 4px;
  border-left: 4px solid currentColor;
  border-top: 3.5px solid transparent;
  border-bottom: 3.5px solid transparent;
  transition: transform 160ms var(--ease);
}
/* UP, BECAUSE THE LIST IS UP. A group's triangle turns down because its
   body drops below the summary; this one opened the same way and pointed at
   the floor while the menu sat over the button. Babe: "wondering if we
   should spin the arrow icon the other way so it faces up toward the menu
   since its above". It is safe to state it flatly rather than compute it -
   `place()` pins the list to `bottom` and never to `top`, on the rule that a
   menu which moves is a menu you have to look for. */
.model-btn[aria-expanded="true"]::before { transform: rotate(-90deg); }

.model-list {
  position: fixed; z-index: 60;
  display: flex; flex-direction: column; gap: 1px;
  background: var(--sunk);
  padding: 1px;
  box-shadow: 0 10px 30px rgba(0,0,0,0.6);
}
.model-list[hidden] { display: none; }
/* the rows scroll; the search row above them does not, or you lose it the
   moment you look past the first few */
.model-opts { display: flex; flex-direction: column; gap: 1px; min-height: 0; overflow-y: auto; }

/* ---- finding one in a long list ----
   Partner endpoints and open-weight checkpoints both live here, and the two
   tags are the difference: they filter, so the list can be held to what runs
   on Comfy Cloud when that is what a round needs. */
.model-find {
  flex: 0 0 auto;
  display: flex; align-items: center; gap: 3px;
  background: var(--raise-2);
  padding: 5px;
}
.model-q {
  flex: 1 1 auto; min-width: 0;
  background: var(--sunk); border: 0; color: var(--ink);
  padding: 5px 7px;
  font: inherit; font-size: 10px; letter-spacing: 0.06em;
}
.model-q::placeholder { color: var(--dim); text-transform: uppercase; letter-spacing: 0.09em; }
.model-q:focus { outline: 1px solid var(--raise); outline-offset: 0; }
.model-tags { flex: 0 0 auto; display: flex; gap: 3px; }
.model-tag {
  background: var(--sunk); border: 0; cursor: pointer;
  padding: 5px 7px;
  font: inherit; font-size: 9px; font-weight: 600;
  letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--dim);
  transition: background 150ms var(--ease), color 150ms var(--ease);
}
.model-tag:hover { color: var(--ink); }
/* ON is the lift and full ink the rest of the panel uses for a live
   control, not a coloured slab in a list of names */
.model-tag.is-on { background: var(--raise); color: var(--ink); }
.model-none {
  padding: 10px 9px; background: var(--raise-2);
  font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase; color: var(--dim);
}
.model-none[hidden] { display: none; }
.model-opt[hidden] { display: none; }
.model-opt {
  display: flex; align-items: center; gap: 9px;
  padding: 8px 9px;
  background: var(--raise-2); border: 0; cursor: pointer;
  font-size: 10px; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--muted); text-align: left;
  transition: background 150ms var(--ease), color 150ms var(--ease);
}
.model-opt:hover { background: #262626; color: var(--ink); }
.model-opt.is-on { color: var(--ink); }
.model-opt-name { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* the clock is half the choice in a timed round, so it is never clipped */
.model-opt-t { flex: 0 0 auto; color: var(--dim); font-variant-numeric: tabular-nums; }
/* WHOSE MODEL IT IS, now that the chips sort by what a row READS instead.
   It rides beside the NAME rather than in the right-hand column, because
   that column is already the seconds - and it is dim and small, because it
   is a fact to read once rather than a thing to compare rows on. */
.model-opt-v { margin-left: 6px; color: var(--dim); font-size: 10px; letter-spacing: .04em; text-transform: uppercase; }
.model-opt:hover .model-opt-v, .model-opt.is-on .model-opt-v { color: var(--muted); }
.model-opt:hover .model-opt-t, .model-opt.is-on .model-opt-t { color: var(--muted); }

/* ---- a collapsible group ---- */

.grp { background: var(--raise-2); flex: 0 0 auto; transition: background 150ms var(--ease); }
/* The cell is the hover target, not its first row: reaching for the prompt
   box or a slot lifts the whole group a step, and its label comes up to
   full ink wherever in the cell you are. */
.grp:hover { background: #212121; }
.grp:hover > summary,
.grp:hover > .model-btn { color: var(--ink); }

/* One group may be told to take whatever height the others leave. A camera is
   a view, not a row of fields, so it gets the rest of the panel - and because
   only this one flexes, opening it never closes anything else. */
/* A floor under the prompt: with Inputs and Settings both open there is not
   246px to go round, and a prompt squeezed to one line is worse than a form
   you scroll. It goes on the GROUP, not on the box inside it - a floor under
   the box makes the box outgrow the group and hang out of the bottom of it,
   which is exactly what it did (24px at 1500, 84px at 1200). */
.grp-tall[open] {
  flex: 1 1 auto; min-height: 105px;
  display: flex; flex-direction: column;
}

/* ---- NO DEAD SPACE UNDER THE LAST GROUP ----
   Babe, 2026-10-21, looking at the Music panel: "i would make sure that in
   the panels, theres no gaps below the bottom panel like they always fit
   that space."

   IT WAS NEVER AN AUDIO FAULT. The only group that has ever taken the spare
   is the one with the SQUARES in it, so every tile that reads no media -
   text to image, text to music, text to sound - left the ground showing
   under its last group. Measured at 1920x1080: 58px under Subject on
   Z-Image, 58px under Music, 89px under Sound. At 960x870 there is none,
   because the form OVERFLOWS there and has no spare to leave - which is why
   this had never been seen at Babe's own window.

   THE LAST OPEN GROUP MOPS UP, AND ONLY WHAT NOBODY CLAIMED. A group that
   asked for the height still gets it - 1000 against 1, so opening the
   squares hands them the room rather than splitting it - and the mop-up
   only ever absorbs what is genuinely left over.

   `:has(~ ...)` is what "last" means here, and the two `[hidden]` tests are
   load-bearing: a hidden group is `display: none` and still matches
   `[open]`, so without them a panel whose Settings group is hidden would
   hand the space to nobody. */
.grp-tall[open], .run-form > .grp-set { flex-grow: 1000; }
.run-form > .grp[open]:not([hidden]):not(:has(~ .grp[open]:not([hidden]))) { flex-grow: 1; }
/* and the box inside it grows with it, or the gap simply moves indoors -
   a group stretched round a 51px prompt is the same hole one step in. */
.run-form > .grp[open]:not([hidden]):not(:has(~ .grp[open]:not([hidden]))) > .grp-body > .prompt-wrap,
.run-form > .grp[open]:not([hidden]):not(:has(~ .grp[open]:not([hidden]))) > .grp-body > .run-second {
  flex: 1 1 auto; height: auto;
}
/* ---- TWO BOXES, ONE SPARE, SPLIT BETWEEN THEM ----
   Babe, 2026-10-21, on the Music panel: "maybe not as tall. maybe lyrics is
   open with it."

   The mop-up hands the slack to the LAST open group, which on a song row
   meant one text box at 150px with a 109px field in it and the other at 51
   - and which of the two got it depended only on which came last. They are
   the same KIND of thing: two halves of what the artist is writing. So when
   both are open they share, and the boxes inside them grow together.

   `1 0 auto`, AND BOTH HALVES OF THAT ARE PAID FOR. At basis `0` the prompt
   has no size of its own to defend, so opening SETTINGS crushed it to a
   38px box beside a 51px lyrics - two boxes for the same kind of words, one
   unusable. And with shrink at `1` the pair crushed TOGETHER at 960x870,
   down to a 38px box and a 16px one, which is worse than the overflow it
   was avoiding: this form has a visible scrollbar precisely so it can be
   longer than the panel. So they grow into spare and never give up their
   own height.

   Measured at 1920: prompt 150 / lyrics 29 before, 91 / 87 after, both with
   a 50px box. At 960x870 nothing changes, because there is no spare there
   to share - which is why opening SETTINGS had to be fixed separately. */
.run-form > .grp-prompt[open]:not([hidden]):has(~ .grp-second[open]:not([hidden])),
.run-form > .grp-second[open]:not([hidden]):not(:has(~ .grp[open]:not([hidden]))) {
  flex: 1 0 auto;
}
.run-form > .grp-prompt[open]:not([hidden]):has(~ .grp-second[open]:not([hidden])) > .grp-body > .prompt-wrap,
.run-form > .grp-second[open]:not([hidden]) > .grp-body > .run-second {
  flex: 1 1 auto; height: auto; min-height: 0;
}

/* `hidden` is only a UA `display: none`, and the rule above is a real one, so
   a tall open group ignores it - the same way .pipe-btn's inline-flex once
   kept a Run button on the Rounds view. The prompt group hides for a model
   that reads no prompt, and it is the tall one. */
.grp[hidden] { display: none !important; }
.grp-tall[open] > .grp-body { flex: 1 1 auto; min-height: 0; display: flex; }
/* the one child of a tall body fills it - a camera, or a prompt box. The
   prompt is a WRAP now (backdrop + textarea), so it is the wrap that
   stretches and the textarea fills it from inside. */
.grp-tall[open] > .grp-body > .run-prompt,
.grp-tall[open] > .grp-body > .prompt-wrap { flex: 1 1 auto; height: auto; min-height: 0; }

/* THE PROMPT BOX IS TWO LINES, AND IT STOPPED TAKING THE SPARE HEIGHT.
   Babe, 2026-10-18: "maybe we just make the prompt box 2 lines instead of
   four or whatever... i guess its 3." It measured 64px against a 17.4px
   line and 16px of padding - 2.76 lines, which is the three they counted -
   and that height came from the GROUP's 105px floor rather than from
   anything on the box.

   SO THE FLOOR WAS THE WHOLE MECHANISM, and it only existed because the
   panel could not scroll: "a prompt squeezed to one line is worse than a
   form you scroll" was written when the form had no scrollbar, so the
   prompt had to be the one group that flexed or an opened Inputs drawer
   would have crushed it with nothing on screen to say so. The form has a
   bar now, so the box can be the size it should be and the form can be
   longer than the panel - which is the trade that note was refusing.

   SPECIFICITY, NOT ORDER, both times: `.grp-tall[open]` and
   `.grp-tall[open] > .grp-body > .prompt-wrap` are what these have to
   beat, which is why each repeats the class it is narrowing. */
.grp-prompt.grp-tall[open] { flex: 0 0 auto; min-height: 0; }
.grp-prompt.grp-tall[open] > .grp-body > .prompt-wrap {
  /* two lines of 12px at 1.45, then the box's own 8px top and bottom */
  flex: 0 0 auto;
  height: calc(2 * 1.45 * 12px + 16px);
}

/* ---- THE SECOND BOX ----
   Same ground, same type, same padding as the prompt - it is the same kind
   of control and reading as a different one would say it was. What it does
   NOT have is the backdrop: the coloured [BOX_1] blocks belong to the
   sentence that addresses a rectangle, and nothing addresses a rectangle
   from in here, so this is a plain textarea and one rule covers it.

   FOUR LINES RATHER THAN TWO, because a verse is not a sentence. The form
   scrolls, which is what pays for the extra two - the same trade that let
   the prompt come DOWN to two. */
.run-second {
  width: 100%;
  font: inherit; font-size: 12px; line-height: 1.45;
  padding: 8px 9px;
  border: 0; resize: none;
  background: #232323; color: var(--ink);
  transition: background 150ms var(--ease);
}
.run-second:hover { background: #2a2a2a; }
.run-second::placeholder { color: var(--muted); }
.grp-second.grp-tall[open] { flex: 0 0 auto; min-height: 0; }
.grp-second.grp-tall[open] > .grp-body > .run-second {
  flex: 0 0 auto;
  height: calc(4 * 1.45 * 12px + 16px);
}
/* ---- and the one child of a TOOL's tall body is a square ----
   The wide slot is sized in px off --slot-h because a percentage height
   resolves against an indefinite area by falling back to the picture's own
   ratio. Inside this chain the height IS definite - the form is 100%, the
   group flexes, the body flexes - so the row can take all of it, and the
   grid row becomes a 1fr that cannot exceed its track. */
.grp-inputs.grp-tall[open] > .grp-body { align-items: stretch; }
.grp-inputs.grp-tall[open] .slots { flex: 1 1 auto; min-height: 0; width: 100%; }
.grp-inputs.grp-tall[open] .slot-row { height: 100%; }
.grp-inputs.grp-tall[open] .slot-wide {
  height: 100%; min-height: var(--slot-h);
  grid-template-rows: minmax(0, 1fr);
}
/* THE WORD IS A CELL, SO IT IS THE CELL'S HEIGHT. Both halves are sized
   off --slot-h, which is the fixed px the row uses when it is NOT
   growing; once it grows the picture follows the track and the word was
   left at 48. Babe: "just make the VIDEO cell the same height as the
   image". Stretch is what the grid already offers - it only needed the
   px height taken off both. */
/* ---- the input drawer, in two halves ----
   The square takes one and what you loaded takes the other, filling it:
   Babe, on the word cell that sat there at 48px beside a growing square,
   "its just expanding this to fit". The specs read like the reel card does
   - the kind in cream, the numbers in grey under it - because that is the
   line an artist reads back off a source. */
/* SPECIFICITY, NOT ORDER. `.grp-tall[open] > .grp-body` is three selectors
   and sets `display: flex`; `.body-split` is one, so it lost wherever it
   sat in the file - and a flex row sizes the spec panel to its own text,
   which is why "NO CLIP" kept wrapping in a column meant to be half the
   drawer. Matched here rather than fought with !important. */
.grp-tall[open] > .grp-body.body-split,
.body-split {
  /* minmax(0, 1fr), NOT 1fr. A plain 1fr is minmax(AUTO, 1fr), so the half
     holding the square took whatever the square's content asked for and the
     specs got what was left - "NO CLIP" wrapping into two lines in a column
     that was meant to be half the drawer. Babe: "its just making it all
     even, so input box and info box are the same widths". */
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 3px;
  align-items: stretch;
}
.body-split .slots { display: block; }
.body-split .slot-wrap, .body-split .slot { width: 100%; height: 100%; }
.slot-spec {
  display: flex; flex-direction: column; justify-content: flex-start; gap: 2px;
  overflow: hidden;
  /* 8px, not 9. With the ratio on the duration line the longest spec is
     "10.3s · 248f · 1.74:1", which overflowed by exactly 2px at 1280 -
     and only at 1280, where this drawer is tightest. The ellipsis ate
     the fact that had just been added to the line, which is the worst
     possible two pixels to be short of. */
  min-width: 0; padding: 8px 8px;
  /* a step above the square beside it, so the half that is TEXT reads as a
     panel rather than a hole with words in it */
  background: #2a2a2a;
}
/* 12px, where the rest of this panel is 10. This is the one place in it
   that is READ rather than scanned - a filename, a duration, a frame count
   you are about to act on - so it is not the size of the labels that only
   name things. At 960 the drawer cannot hold five lines at 12px, so the
   narrow rule below steps it back to 10. */
.spec-kind {
  font-family: Oswald, sans-serif; font-weight: 600;
  font-size: 12px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--ink);
}
/* a filename is the one line here that is not ours to shorten, so it gets
   the ellipsis rather than the panel getting wider */
.spec-name {
  font-size: 12px; color: #a39c92;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.spec-line {
  font-size: 10px; color: var(--muted); font-variant-numeric: tabular-nums;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.spec-line:empty, .spec-name:empty { display: none; }
/* A FLEX CHILD SHRINKS BY DEFAULT, WHICH IS HOW THE WORDS GOT CRUSHED. This
   panel was built to pack from the top and clip whatever does not fit - but
   `flex-shrink: 1` means nothing ever reaches the clip: the browser
   squeezes every line box instead, so a 15px line came out 6px and the
   glyphs were sliced through the middle on EVERY line at once. Babe,
   2026-10-19: "the input text gets all small or something. like the height
   changes?" - the type was never restyled, the boxes holding it were. A
   line is its own height or it is not drawn. */
.slot-spec > * { flex: 0 0 auto; }
/* AT 960 THE DRAWER IS 68px AND FIVE LINES AT 11px NEED 89, so the panel
   showed its first line and clipped the rest - which is a spec panel that
   says "VIDEO" and nothing else. One step down and tighter padding fit four
   of the five; the fifth is the resolution, which is last for that reason.
   After the rules it overrides, not in the narrow block at the top of this
   file, which loses on source order at equal specificity. */
/* AND AT 960 THE DRAWER STOPS GROWING. The form is 151px there and the
   panel wants a model row, a picture, its specs and four buttons - 199px of
   content. A growing drawer in a scroller that short is a drawer the browser
   COMPRESSES, so the square came out a 25px sliver with one line of specs
   beside it. Fixed height instead: the form scrolls, the actions stay
   pinned, and the square is the size it says it is. The tall version is a
   wide-window luxury. */
/* AND WHAT SQUEEZED IT WAS THE SECOND TALL DRAWER. Inputs and Prompt are
   both `grp-tall`, so they SHARE the form's spare height - and a tool
   opens with no prompt at all, so PICKING A MODEL takes 42px out of a
   drawer that was already exactly full: measured at 1286, the drawer goes
   147px -> 105px and the spec panel 109 -> 68, which is five lines in the
   room for three. The floor is what those five lines need, so the drawer
   keeps them and the FORM gives way instead - which is the trade the
   scrollbar was added to pay for. Wide windows only: under 1180 the drawer
   is pinned small and the type is already a step down, where all five fit
   in 87px. */
@media (min-width: 1181px) {
  .grp-inputs.grp-tall[open] { min-height: 141px; }
}
@media (max-width: 1180px) {
  .grp-inputs.grp-tall[open] { flex: 0 0 auto; min-height: 0; }
  .grp-inputs.grp-tall[open] .slot-row,
  .grp-inputs.grp-tall[open] .slot-wide,
  .grp-inputs.grp-tall[open] .slot-name { height: var(--slot-h, 56px); }
  .slot-spec { padding: 5px 7px; gap: 1px; }
  .spec-kind, .spec-name, .spec-line { font-size: 10px; }
}

.grp-inputs.grp-tall[open] .slot-name,
.grp-inputs.grp-tall[open] .slot-wide img,
.grp-inputs.grp-tall[open] .slot-wide video { height: 100%; }
/* A <details> does not hand its own height to its content: the part after the
   summary sits in an anonymous wrapper, which is where the spare height was
   getting stranded. Chrome exposes that wrapper, so flex it too. Where the
   pseudo is not understood the camera simply keeps its min-height. */
.grp-tall[open]::details-content {
  display: flex; flex-direction: column;
  flex: 1 1 auto; min-height: 0; block-size: auto;
}
.grp summary {
  list-style: none; cursor: pointer;
  padding: 7px 9px;
  display: flex; align-items: center; gap: 7px;
  font-size: 10px; font-weight: 600; letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--muted);
}
.grp summary::-webkit-details-marker { display: none; }
/* THE PROMPT'S HEADER IS A SENTENCE, NOT A LABEL. Every other drawer says
   one upright word, so the form uppercases them all; this one says
   "ADD : in box_1", where the id is a name the artist typed and has to
   read back exactly as they typed it. The mode is written in capitals in
   the string itself, so the two halves still differ in weight. */
.grp-prompt > summary { text-transform: none; }
/* The marker sits in a 12px berth - the width of the icons on the other
   rows - so every label in this form starts at the same 28px whether the
   row opens, drops or chooses. */
.grp summary::before {
  content: "";
  width: 0; height: 0;
  margin: 0 4px;
  border-left: 4px solid currentColor;
  border-top: 3.5px solid transparent;
  border-bottom: 3.5px solid transparent;
  transition: transform 160ms var(--ease);
  flex: 0 0 auto;
}
.grp[open] summary::before { transform: rotate(90deg); }
/* ---- a drawer that is the whole panel ----
   KILL THE CHEVRON WHERE THERE IS NOTHING TO COLLAPSE INTO. Babe: "for
   some of these workflows we may just want to kill the collapse icon. or
   disable it" - a tool panel is two drawers and a hole, and folding
   either one away leaves the hole. The summary keeps its label and its
   lift, because the row is still what says what is below it; what goes
   is the triangle and the press. */
.grp-fixed > summary { cursor: default; pointer-events: none; }
.grp-fixed > summary::before { content: none; }
.grp-fixed > summary { padding-left: 11px; }
.grp summary:hover { color: var(--ink); }
/* The sparkle that hands this workflow to the agent, in the place the word
   "empty" used to sit - which told you nothing the empty box had not. */
.grp-ask {
  margin-left: auto;
  background: none; border: 0; padding: 0; cursor: pointer;
  width: 18px; height: 18px; color: var(--muted);
  display: grid; place-items: center;
  transition: color 150ms var(--ease);
}
.grp-ask svg {
  width: 14px; height: 14px;
  fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
}
.grp-ask:hover { color: var(--accent-text); }
.grp-ask.is-on { color: var(--accent-text); }

/* Sheet or single: one icon or the other, never both, and a touch smaller
   than the eye beside it - four panes at that size read heavier than they
   should next to a line drawing. */
.orbit-grid svg { width: 12px; height: 12px; }
.orbit-grid .icon-one { display: none; }
.orbit-grid:not(.is-on) .icon-grid { display: none; }
.orbit-grid:not(.is-on) .icon-one { display: block; }
/* the chips already took the gap to the right, so this one sits next to them */
.orbit-view { margin-left: 0; }

.grp-sum {
  margin-left: auto;
  text-transform: none; letter-spacing: 0;
  font-size: 10px; color: var(--dim);
}
.grp-body { padding: 0 9px 9px; }

/* ---- a setting you press ----
   A row per decision: what it is on the left, the choices on the right as
   cells of the same 3px grid the rest of the panel is laid out on. */
.pick { display: flex; align-items: center; gap: 9px; padding: 3px 0; min-width: 0; }
.pick-k {
  flex: 0 0 auto; width: 46px;
  font-size: 9px; font-weight: 600; letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--dim);
}
.pick-v { flex: 1 1 auto; display: flex; gap: 3px; flex-wrap: wrap; min-width: 0; }
.pick-o {
  background: #232323; border: 0; cursor: pointer;
  padding: 5px 8px;
  font: inherit; font-size: 10px; letter-spacing: 0.06em;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  transition: background 150ms var(--ease), color 150ms var(--ease);
}
.pick-o:hover { background: #2c2c2c; color: var(--ink); }
.pick-o.is-on { background: var(--raise); color: var(--ink); }

/* ---- THE VOICE PANEL ----
   A settings pill that outgrew its row. Babe, 2026-10-21, looking at ROGER
   (MALE, AMERICAN) filling the width with STABILITY still beside it: "maybe
   we do a custom panel for it tho", then "and also prob keep it open".

   IT SHIPS OPEN, which is where it parts from the camera dial it otherwise
   copies: AUTO is a real answer on a dial and there is no such answer here,
   so the choice IS the panel rather than an option inside it.

   AND IT IS A DROPDOWN, WHICH IT ALWAYS WAS. The first build drew the whole
   library as chips and the dialogue row killed that on sight - thirteen
   names, then the same thirteen again underneath. Babe: "i might just want
   dropdown menus for these and in the same panel so it's just VOICES and
   the in the panel are two dropdown menus to select one for each."

   So the thing that was wrong was never the control, it was WHERE it was:
   a pill in SETTINGS sharing a row with STABILITY, under a closed group
   below the fold. The same pill in its own open panel is one press from the
   top of the form, and the panel costs 2 rows rather than 10.

   THE GROUP IS SIZED BY ITS ROWS, so it takes no part in the form's spare
   height - which is what keeps it from being the panel that makes every
   other panel scroll. Measured: it pushed the form over by 58px at 1920 and
   271px on the dialogue row while the list was chips. */
.grp-voice > .grp-body { display: flex; flex-direction: column; gap: 3px; }
.voice-row { display: flex; align-items: center; gap: 6px; min-width: 0; }
.voice-row[hidden] { display: none; }
/* The label only appears when there are two of them - with one dropdown the
   group heading already says Voice. Fixed width so the two pills line up,
   which is the one thing a pair of rows has to do that a single one does
   not. */
.voice-k {
  flex: 0 0 auto; width: 46px;
  font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--dim);
}
.voice-k[hidden] { display: none; }
/* A NAME, NOT A SETTING. Every other pill face is a value - 16:9, 1K, ON -
   and wears the tracked-out upper case that says so; a voice is a name
   somebody chose, so it keeps the case it was written in. */
.voice-row .pick-voice { flex: 1 1 auto; min-width: 0; }
.voice-row .pick-voice .pick-now {
  width: 100%; justify-content: space-between;
  text-transform: none; letter-spacing: 0.02em; font-size: 11px; font-weight: 500;
}
.voice-row .pick-voice.is-open .pick-v .pick-o,
.voice-row .pick-voice .pick-v .pick-o {
  text-transform: none; letter-spacing: 0.02em; font-size: 11px;
}

/* ---- RECORD IS THE OTHER HALF OF THE INPUT ROW ----
   It shipped as a small IMPORT | RECORD chip pair above the square and Babe
   cut it on sight, 2026-10-21: "remove the small import and record buttons.
   add this style... but make the second cell just a big record button (not
   with the import icon)", pointing at the [square | VIDEO] pairs the video
   tiles draw.

   SO THE TWO WAYS TO FILL THE SQUARE ARE THE TWO HALVES OF THE ROW.
   `.slots-wide` is already a two-column grid and an audio input is a single
   square, so the second column was simply empty - the other way of filling
   it now sits exactly where the eye already expects a second cell. Nothing
   new above the square to read before you reach it.

   IT IS THE WORD CELL'S TYPE, EXACTLY: Oswald 500 at 12px on 0.04em, which
   is what VIDEO and IMAGE are set in. Babe asked for that in the same
   breath - "make sure the font is the same size" - and it is the whole
   reason this reads as the row's other half rather than as a button that
   landed in it.

   AND IT CARRIES THE BRAND. Babe: "lets make it the brand orange." It is
   the one cell in this row that STARTS something rather than receiving it,
   which is the same test the accent is applied by everywhere else here. */
.slot-rec {
  border: 0; cursor: pointer; padding: 0 3px; min-width: 0;
  width: 100%; height: 48px;
  display: grid; place-items: center; text-align: center;
  background: var(--accent); color: #0a0a0a;
  font-family: Oswald, sans-serif; font-weight: 500;
  font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase;
  line-height: 1.2;
  transition: background 150ms var(--ease), color 150ms var(--ease);
}
.slot-rec:hover { background: #ff3d3e; }
/* WHILE IT IS LISTENING the cell is the thing to press to stop, so it goes
   quiet and lets the spectrum carry the colour - two full-accent surfaces
   saying different things is how an accent stops meaning anything. */
.grp-inputs.is-recording .slot-rec { background: var(--accent-sunk); color: var(--accent-text); }

/* ---- THE SPECTRUM IN THE PANE ----
   Babe, 2026-10-21: "using the preview box (after selecting a preset like
   clone) ... it shows an audio spectrum that basically captures your audio,
   with a record button and save button after you are done to save to
   assets. we use the similar patterns from other workflows, when showing a
   generated image or video there."

   THE PANE IS THE MICROPHONE - the same move SCENE EDIT made when the
   result pane became the shot camera. The right half of this view is where
   you LOOK at what you are making, so a thing being made live belongs
   there rather than crushed into a 56px square.

   THE BAR IS THE BENCH'S TRANSPORT, deliberately: buttons touching as a
   segmented strip, the elapsed time on the left where the bench puts its
   clock, and the row the full width of the pane. A second vocabulary for
   "press this to start and that to keep it" is a second thing to learn. */
.rec-pane {
  width: 100%; height: 100%; min-height: 0;
  display: flex; flex-direction: column; gap: 3px;
}
.rec-spec {
  flex: 1 1 auto; min-height: 0; width: 100%;
  /* THE PANE'S OWN GROUND, SHOWING THROUGH. Babe, 2026-10-21: "can we use
     this dark color for the audio spectrum background", pointing at the
     empty result pane - which is `.run-out` at #080808. So the canvas is
     left TRANSPARENT rather than painted that colour: a second copy of the
     value is a second thing to change, and the spectrum stands exactly
     where a generated frame stands, on exactly the ground it stands on. */
  background: transparent; display: block;
}
/* The bars are drawn in the canvas rather than in CSS, so the two colours
   they use are named here for the one reason a reader would come looking:
   idle is the brand at a third, live is the brand. Babe: "lets make it the
   brand orange." */
/* ---- THE BAR IS THE STAGING BENCH'S BAR ----
   Babe, 2026-10-21: "you know how we did the blockout preview... bottom
   left is like a header bar. so the buttons can be smaller to the right.
   bar says RECORD AUDIO 00:00 or something."

   That row was built for a reason this pane has too: a lone press stretched
   across the width reads as a BANNER rather than a button, and the pane
   above it was the one surface here with no name on it while INPUTS, SCRIPT
   and SETTINGS all say what they are. The label takes the slack; each press
   keeps its own width. Set like a group summary rather than like a button,
   so it never reads as a third thing to press. */
.rec-bar { flex: 0 0 auto; display: flex; gap: 3px; height: 28px; }
.rec-what {
  flex: 1 1 auto; min-width: 0;
  display: inline-flex; align-items: center; gap: 10px;
  padding: 0 11px;
  background: var(--raise); color: var(--muted);
  font-size: 10px; font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* THE CLOCK IS PART OF WHAT THE BAR SAYS, not a third control - it is the
   only thing on screen that says the microphone was ever granted, so it
   belongs in the sentence that names the microphone. Brighter than the
   label, because it is the half that changes. */
.rec-time { color: var(--ink); font-variant-numeric: tabular-nums; }
.rec-act {
  flex: 0 0 auto; border: 0; cursor: pointer;
  padding: 0 14px;
  background: var(--raise-2); color: var(--ink);
  font: inherit; font-size: 10px; font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase;
  transition: background 150ms var(--ease), color 150ms var(--ease);
}
.rec-act:hover:not(:disabled) { background: #262626; }
/* RECORD IS RED. Babe, 2026-10-21: "lets make the record button red" - it
   is the press that starts the live thing, which is the test the accent is
   applied by everywhere else here, and the one button on this bar you
   reach for first. It stays red once it reads STOP, because it is the same
   control and the recording state is red throughout: the bars, the bar's
   own button, one colour saying one thing. */
.rec-act[data-rec="go"] { background: var(--accent); color: #0a0a0a; }
.rec-act[data-rec="go"]:hover:not(:disabled) { background: #ff3d3e; }
/* SAVE IS THE ONE THAT FILES SOMETHING, so it is the one that takes the
   accent - the rule the rest of the app follows, where the accent marks
   the press that cannot be taken back rather than every press on the row. */
.rec-act.rec-save:hover:not(:disabled) { background: var(--accent); color: #0a0a0a; }
/* AND A DISABLED SAVE SAYS SO BY GOING DIM, not by vanishing: the button
   has to be there before there is a take, or the row changes shape under
   the hand at the moment you stop recording. The same treatment the bench's
   own SAVE gets once it has fired. */
.rec-act:disabled { background: var(--raise); color: var(--dim); cursor: default; }

/* A number has no options to choose between, so it steps. These are NOT
   .pick-o: the chip handler would treat them as two more choices and light
   one of them, and "+" is not a value the setting can hold. */
.pick-s {
  background: #232323; border: 0; cursor: pointer;
  padding: 5px 7px; line-height: 1;
  font: inherit; font-size: 11px;
  color: var(--muted);
  transition: background 150ms var(--ease), color 150ms var(--ease);
}
.pick-s:hover { background: #2c2c2c; color: var(--ink); }
/* the readout between them is the value, not a button: it shows what it is
   and takes no press */
.pick-n { cursor: default; min-width: 34px; text-align: center; }

/* ---- media slots ---- */

.slots { display: flex; gap: 4px; flex-wrap: wrap; align-items: start; }
/* The square and the word that says what goes in it. */
.slot-wrap { display: flex; flex-direction: column; gap: 3px; width: 56px;
  position: relative; }
/* A SQUARE UNDER A DRAG says so where the picture will land, not around
   the whole button - on a wide pair the right half is a word, and lighting
   that up would read as dropping onto the name. */
.slot.is-over { outline: 1px solid var(--red); outline-offset: -1px; }
.slot-wide.is-over { outline: none; }
.slot-wide.is-over::before { background: #3f3f3f; box-shadow: inset 0 0 0 1px var(--red); }
/* a picture you can pick up should say so under the pointer */
.run-out img, .run-out video,
.asset img, .asset video,
.slot img, .slot video { cursor: grab; }
.run-out img:active, .run-out video:active,
.asset img:active, .asset video:active,
.slot img:active, .slot video:active { cursor: grabbing; }

/* THE WAY OUT. A filled slot could only be swapped for another picture
   before this, never emptied - so a reference dropped on the wrong square
   stayed there. The cross is a button of its own, sitting over the top
   corner of the picture, and it exists only while there IS a picture:
   :has means nothing has to remember to show or hide it. */
/* HARD INTO THE CORNER, and small. It sat 2px in from each edge at 16px
   square, which reads as a control placed ON the picture rather than one
   tucked out of its way - and the picture is the thing being looked at.
   Babe, 2026-10-18: "can we make this x square smaller on uploaded assets
   and put it all the way in the corner, no padding." */
.slot-clear {
  position: absolute; top: 0; right: 0; z-index: 2;
  width: 13px; height: 13px; padding: 0; border: 0;
  display: none; place-items: center; cursor: pointer;
  background: rgba(5, 5, 5, 0.66); color: var(--ink);
  transition: background 150ms var(--ease), color 150ms var(--ease);
}
.slot-wrap:has(img, video) .slot-clear { display: grid; }
/* AND THE HIGHLIGHT WAS DEAD. This read `var(--red)`, which this stylesheet
   has never defined - the token is `--accent` - so an invalid background
   simply dropped and hovering did nothing at all. Third time a borrowed
   name has been wrong here (--line was 19px, not a colour), and the same
   lesson: grep a variable before using it.

   `--accent-text` is the brand's lighter tint, the one every live state in
   this app already uses - the camera on air, a running cell, a playing
   tile - so a destructive control lights with the same family rather than
   a colour of its own. Near-black on it, because cream does not read. */
.slot-clear:hover { background: var(--accent-text); color: #121212; }
.slot-clear svg {
  width: 9px; height: 9px;
  fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round;
}
/* On a wide pair the picture is the LEFT half, so the cross belongs on that
   half's corner rather than the pair's - over the far edge of the word it
   would read as removing the name. */
.slot-row .slot-clear { right: auto; left: calc(50% - 13px); }
.slot-cap {
  font-family: Archivo, sans-serif; font-weight: 600;
  font-size: 8px; letter-spacing: 0.07em; text-transform: uppercase;
  line-height: 1.25; color: var(--muted); text-align: center;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
.slot-wrap:hover .slot-cap { color: var(--ink); }
/* The squares you have not needed yet: there to say the model will read
   more than one, quiet enough not to look like work waiting to be done.
   They come up to full strength on hover, because a dim thing you can
   press should say so when you reach for it. */
.slot-more { opacity: 0.34; }
.slot-more:hover { opacity: 0.75; }
/* AN EMPTY SQUARE HAS TO LOOK LIKE A TARGET. It sat at #232323 on a group
   of #1c1c1c - seven levels of separation, which is not a box so much as a
   slightly different patch of the same dark. Babe, 2026-10-18: "can you
   make the upload slot this color.. it kind of gets lost in that panel
   color", pointing at the tooltip's own grey, "then highlight from there."
   So the RESTING state is lifted to where the eye finds it and the hover
   climbs from the new floor rather than from the old one. A filled slot
   covers this entirely, so it only ever shows where something is wanted. */
.slot {
  width: 56px; height: 56px;
  background: #3a3a3a;
  display: grid; place-items: center;
  border: 0; padding: 0; cursor: pointer;
  color: var(--muted);
  overflow: hidden;
  transition: background 150ms var(--ease);
}
.slot:hover { background: #474747; }
.slot img, .slot video { width: 100%; height: 100%; object-fit: cover; }
/* An empty slot says "bring something in" with the icon; WHICH input it is
   is written under the square - see .slot-cap. */
.slot-in {
  width: 19px; height: 19px;
  fill: none; stroke: currentColor; stroke-width: 1.6;
  stroke-linecap: round; stroke-linejoin: round;
}
.slot:hover .slot-in { color: var(--ink); }

/* ---- the wide slot ----
   RESTYLE is the one tile where telling two inputs apart IS the task: one
   thing is being changed and the other says what into. Two 56px squares with
   a caption under each are the same grey box at a glance, so this tile draws
   a full-width row per input - the name set in the type the panel already
   uses for a group, the picture filling the right half of the row.

   The name sits INSIDE the button, so the whole pair is the drop target: the
   word is as pressable as the picture, which is the point of making it this
   big. */
/* FOUR CELLS ACROSS: image, source, image, style - two cells per input,
   all of them separated on the same 4px grid the rest of the floor is on.
   Two rows down the panel read as a list of steps; one row of pairs reads as
   this-into-that, which is what a restyle is.

   The pair DIVIDES IN HALF: the picture and the word take the same width, so
   neither reads as the label of the other - they are two cells of equal
   weight that happen to belong together. Both are the same size whatever
   shape the asset is, so a clip and a frame can be compared at a glance;
   object-fit does the cropping. The word sits in a cell of its own rather
   than as a caption on the picture, which is what "separated" means here -
   and it is why the button itself carries no ground.

   Inside a pair there is NO gap at all and the two cells are different
   greys: the join is what makes them one thing, and the 12px between the
   pairs is what makes them two. A gap in both places left four evenly
   spaced cells, which is four things, and the eye had to be told which
   two go together. */
.slots-wide { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; width: 100%; }
.slots-wide .slot-row { width: auto; min-width: 0; }
.slot-wide {
  /* ONE number for the row, because the picture is sized in px off it rather
     than in percent: a percentage height resolves against the grid area only
     while that area is definite, and when it is not the image falls back to
     its own aspect ratio, grows past the row and drags the word off centre
     beside it. px cannot fail that way. */
  --slot-h: 48px;
  width: 100%; height: var(--slot-h); overflow: hidden;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  grid-template-rows: var(--slot-h);
  /* STRETCH, not the base .slot's `place-items: center`. Overriding only
     align-items left justify-items: center in force, which shrank both
     cells to their contents - the square's ground measured 0px wide and
     simply did not paint, and the word's box ended 8px short of its
     column. The screenshot looked like a missing background; the
     computed width said it was a missing override. */
  place-items: stretch; gap: 0;
  padding: 0; background: none; text-align: left;
}
/* The picture cell's ground, behind both the arrow and the picture - and a
   step brighter than the word beside it. With no gap between them the
   change in grey IS the division, and the brighter half is the half you
   are meant to drop something on. */
.slot-wide::before {
  content: ""; grid-column: 1; grid-row: 1;
  background: #2e2e2e;
  transition: background 150ms var(--ease);
}
.slot-wide:hover::before { background: #3a3a3a; }
.slot-wide:hover .slot-name { background: #2a2a2a; }
.slot-wide .slot-in { grid-column: 1; grid-row: 1; place-self: center; }
.slot-wide img, .slot-wide video {
  grid-column: 1; grid-row: 1; align-self: center;
  width: 100%; height: var(--slot-h); object-fit: cover;
}
/* The word keeps its own cell, so a filled square never pushes it out of the
   button - which is what made a filled slot anonymous the first time. */
.slot-name {
  grid-column: 2; grid-row: 1; align-self: stretch; height: var(--slot-h);
  display: grid; place-items: center; text-align: center;
  background: #232323; padding: 0 3px; min-width: 0;
  transition: background 150ms var(--ease), color 150ms var(--ease);
  font-family: Oswald, sans-serif; font-weight: 500;
  font-size: 12px; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--muted); line-height: 1.2;
}
/* two lines, because "Second reference" is a label this shape may be handed
   and one clipped line would say less than nothing */
.slot-word {
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; max-width: 100%;
}
/* A word with no space in it cannot wrap, so a long one steps down instead:
   the pair's word cell is 42px of usable width and "BLOCKOUT" is 56 at
   twelve point. */
.slot-word.is-long { font-size: 9px; letter-spacing: 0.01em; }
.slot-wide:hover .slot-name { color: var(--ink); }

.run-prompt { width: 100%; min-height: 38px; }
.prompt-wrap { min-height: 38px; }

/* ---- the camera, lifted out of the Comfy node ----
   The scene sizes itself from the container, and its canvas is positioned
   absolute, so the stage needs both a height and a corner to sit in. A chip is
   one of the sheet's four panels, and reads like the round pips above it. */

.orbit { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; width: 100%; }
.orbit-chips { display: flex; gap: 4px; margin-bottom: 4px; flex: 0 0 auto; }
/* in the group's header row the chips take no row of their own */
.grp summary .orbit-chips { margin-left: auto; margin-bottom: 0; }
.orbit-chip {
  width: 22px; height: 22px;
  background: #232323; border: 0; padding: 0; cursor: pointer;
  color: var(--muted); font: inherit; font-size: 10px;
  font-variant-numeric: tabular-nums;
  transition: background 150ms var(--ease), color 150ms var(--ease);
}
.orbit-chip:hover { background: #2e2e2e; color: var(--ink); }
.orbit-chip.is-on { background: var(--accent); color: #fff; }

.orbit-stage {
  position: relative;
  width: 100%;
  flex: 1 1 auto;      /* the group hands it the rest of the panel */
  min-height: 90px;    /* ...but never less than a camera you can aim */
  background: #0a0a0f;   /* the node's own scene colour; it paints this itself */
  overflow: hidden;
  touch-action: none;    /* the handles are dragged, so the page must not scroll */
}
.orbit-stage canvas { display: block; cursor: grab; }

/* The node's value strip, as a sub-panel. Shut it is one 20px row; open it is
   three dropdowns and a reset, which is why it is shut in the narrow column. */
.orbit-props { flex: 0 0 auto; margin-top: 4px; }
.orbit-props summary {
  list-style: none; cursor: pointer;
  display: flex; align-items: center; gap: 6px;
  font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--dim); padding: 2px 0;
}
.orbit-props summary::-webkit-details-marker { display: none; }
.orbit-props summary:hover { color: var(--muted); }
.orbit-nums {
  margin-left: auto; letter-spacing: 0;
  color: var(--muted); font-variant-numeric: tabular-nums;
}
.orbit-fields { display: flex; gap: 4px; align-items: center; padding: 4px 0 2px; }

/* The page's own menu, in the same cells as everything else here. A native
   select would open the operating system's list, which is the one surface on
   this screen the stylesheet cannot reach. */
.orbit-menu { position: relative; min-width: 0; flex: 1 1 0; }
.orbit-menu-btn {
  width: 100%;
  display: flex; align-items: center; gap: 4px;
  background: #232323; color: var(--ink); border: 0;
  font: inherit; font-size: 10px; letter-spacing: 0.02em;
  padding: 4px 5px; cursor: pointer;
  transition: background 150ms var(--ease);
}
.orbit-menu-btn:hover,
.orbit-menu.is-open .orbit-menu-btn { background: #2e2e2e; }
.orbit-menu-val { flex: 1 1 auto; min-width: 0; text-align: left;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.orbit-caret { width: 8px; height: 5px; flex: 0 0 auto;
  fill: none; stroke: var(--muted); stroke-width: 1.4;
  stroke-linecap: round; stroke-linejoin: round; }

.orbit-menu-list {
  position: fixed; z-index: 60;
  background: var(--raise-2);
  border: 1px solid #2e2e2e;
  padding: 2px;
  display: flex; flex-direction: column;
  max-height: 46vh; overflow-y: auto;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
}
.orbit-menu-list[hidden] { display: none; }
.orbit-opt {
  background: none; border: 0; cursor: pointer;
  color: var(--muted); font: inherit; font-size: 10px;
  text-align: left; padding: 5px 8px; white-space: nowrap;
  transition: background 120ms var(--ease), color 120ms var(--ease);
}
.orbit-opt:hover { background: #2e2e2e; color: var(--ink); }
.orbit-opt.is-on { color: var(--accent-text); }
.orbit-reset {
  margin-left: auto;
  background: #232323; color: var(--muted); border: 0; cursor: pointer;
  font: inherit; font-size: 9px; letter-spacing: 0.06em; text-transform: uppercase;
  padding: 4px 7px;
}
.orbit-reset:hover { background: #2e2e2e; color: var(--ink); }

.orbit-say {
  margin-top: 4px; flex: 0 0 auto;
  font-size: 9px; letter-spacing: 0.04em;
  line-height: 1.3; color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* ---- run ---- */


/* ---- the result side ---- */

/* LIFTED OFF PURE BLACK, BUT ONLY JUST. It was #000 - Babe, 2026-10-19:
   "is this black? can we use that same dark background from the output
   thumbnails" - and measured, it was: rgb(0,0,0) against the reel card's
   rgb(28,28,28). An empty pane this size reads as a hole in the screen at
   pure black, where every other dark surface here sits above it.

   THE CARD'S OWN GREY WAS TOO FAR, which is the half that took a second
   look: "the preview background doesn't look dark enough tho.. should be
   darker". The reel is a CARD with a black thumb inside it and this is
   the thumb's job, not the card's - what goes here is a picture to be
   judged, and a light ground is the thing a letterbox should not be made
   of.

   AND THE LAST FOUR LEVELS WERE SETTLED BY READING THE PIXELS. Babe:
   "what color is behind that text... compared to the NOT RUN in output".
   Painted rather than declared: the pane was rgb(12,12,12) against the
   NOT RUN square's rgb(7,7,7) - and the gap is an ARTEFACT, because
   .link.is-queued is opacity 0.4, so an empty tile is #000 knocked back
   over the column rather than a ground anything sits on. A tile holding
   a real result paints pure #000. So the honest range was 0 to 12 and
   this is the middle of it: darker than the placeholder reads, still a
   step off the black that made this pane look like a hole. A number
   rather than a token, because no other surface here wants it. */
.run-out {
  position: relative;
  background: #080808;
  display: flex; flex-direction: column;
  height: 100%; min-height: 0; overflow: hidden;
}

/* The result pane is the picture and a row under it. Nothing covers the thing
   you are judging. */
.run-view {
  flex: 1 1 auto; min-height: 0; position: relative;
  display: grid; place-items: center; overflow: hidden;
}
/* WHAT JUST HAPPENED, OVER THE THING IT HAPPENED TO. Centred on the
   result rather than written into a button, so no row changes width when
   it appears. pointer-events off - the picture underneath is still a
   drag source and still opens full size. */
.run-flash {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  z-index: 2; pointer-events: none;
  padding: 6px 12px; border-radius: 2px;
  background: var(--accent); color: var(--ink);
  font-family: Archivo, system-ui, sans-serif;
  font-size: 10px; font-weight: 600;
  letter-spacing: 0.08em; text-transform: uppercase;
  white-space: nowrap;
}

/* Fill the pane and letterbox inside it, rather than asking the picture to be
   no taller than 100% of something. A percentage max-height only bites when
   the parent's height is definite, and here it simply never did - which went
   unnoticed for as long as the result happened to be smaller than the pane.
   A near-square sheet in a wide pane is where it shows. */
.run-view img, .run-view video {
  width: 100%; height: 100%;
  min-width: 0; min-height: 0;
  object-fit: contain;
}
/* THE CAMERA'S EYE, FITTED THE SAME WAY A RESULT IS - it is the same pane
   holding the same shape, and a preview that letterboxed differently from
   the render replacing it would make the swap look like a reframe. The
   canvas carries the shot's ratio in its own width and height, so
   `contain` is what crops the pane rather than the picture. */
.run-view canvas.shot-live {
  width: 100%; height: 100%;
  min-width: 0; min-height: 0;
  object-fit: contain;
  /* AND NO GROUND OF ITS OWN, which is what makes the CROP visible. Painted
     black the canvas filled the pane edge to edge, and since a blockout is
     mostly black too the letterbox vanished into it - a 16:9 shot in a
     squarish pane read as a square shot. Left transparent, the bars are the
     pane's own grey and the dark rectangle inside them is the frame, which
     is the one thing this preview exists to show. It is also what a result
     does in here, and the two have to agree or the swap reads as a
     reframe. */
}
/* ---- modes ----
   Which kind of this thing you are making, in the pane that is empty until
   a run finishes. NOT cards: three marks floating on the same dark grey a
   round sits on before it has run, because a card would be a third kind of
   square on a screen that already has tiles and slots. The ground is grey
   rather than black so the pane reads as waiting rather than off - and a
   result, which is drawn into this same pane, still lands on black. */
.modes {
  /* the pane is a grid that CENTRES what it holds, so without this the row
     sat at its own smallest width - three 54px squares in a 392px pane */
  width: 100%; justify-self: stretch; align-self: stretch;
  height: 100%; min-height: 0;
  /* As many equal columns as there are marks, not three. Restyle offers
     TWO, and a hardcoded three left them on a third of the pane each with
     an empty column beside them - which clipped both lines to "ONE FR..."
     and "THE WH...". Auto-flow gives three columns to three children and
     two to two, so the Subject marks are unchanged. */
  display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr);
  align-content: center;
  gap: 3px; padding: 12px;
  /* The colour a queued round's square actually PAINTS. Measured off the
     pixel rather than read off the rule: the square is black, but the whole
     round sits at opacity 0.4 over the column, so what reaches the screen
     is rgb(7,7,7) - a hair off black, and plainly not the same as the
     pane's pure #000 next to it. The grey on a round is only its frame. */
  background: #070707;
}
.mode {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 9px;
  min-width: 0;
  padding: 14px 6px;
  background: none; border: 0; cursor: pointer;
  /* All three sit at full strength: they are three equal offers, and
     dimming two of them to light one would say the dim ones are spent. */
  color: var(--ink);
  transition: color 150ms var(--ease);
}
.mode svg {
  width: 34px; height: 34px; flex: 0 0 auto;
  fill: none; stroke: currentColor; stroke-width: 1.4;
  stroke-linecap: round; stroke-linejoin: round;
}
/* the parts of an icon that are a solid shape rather than a drawn line */
.mode svg .ic-fill { fill: currentColor; stroke: none; }
/* Nothing to brighten on hover, so the ICON takes the accent - the smallest
   possible thing to change, and the same red that marks what is live
   everywhere else. The one you picked keeps it. */
.mode:hover svg,
.mode.is-on svg { color: var(--accent); }
.mode-cap {
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  width: 100%; min-width: 0; text-align: center;
}
/* The workflow tile's caption, at the size these squares can carry: the
   lockup face for the name, the LIVE setting for the line under it. A mode
   and a tile are the same KIND of thing - a thing you pick - so they are
   named the same way. */
.mode-name {
  font-family: Oswald, Archivo, sans-serif; font-weight: 600;
  text-transform: uppercase; letter-spacing: 0.03em;
  font-size: 14px; line-height: 1.1; color: currentColor;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
}
/* A NAME TOO LONG FOR ITS COLUMN wraps and steps down a size rather than
   ending in an ellipsis. Three marks share about 65px each at Babe's
   window, which is eight characters of the full size - "SINGLE REFERENCE"
   is sixteen. Measured in the markup, because the stylesheet cannot count
   letters. */
/* WRAPPING KEEPS THE SIZE. A two-line name beside a one-line name reads as
   one family; a name two sizes down beside its neighbour reads as a mistake,
   which is the thing that gets noticed. */
.mode-name.is-wrap { white-space: normal; text-overflow: clip; }

.mode-name.is-long {
  font-size: 11px; letter-spacing: 0.01em; line-height: 1.15;
  white-space: normal; text-overflow: clip;
}
/* ONE line. The name and a line under it is a caption; the name and two
   lines under it is a paragraph sitting on a square. */
.mode-t {
  font-size: 9px; letter-spacing: 0.1em; text-transform: uppercase;
  line-height: 1.35; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%;
}
/* and the line under it takes a second line rather than losing its end -
   two lines of nine point is still a caption, "FROM A ..." is not */
.mode-t.is-long {
  font-size: 8px; letter-spacing: 0.05em; line-height: 1.25;
  white-space: normal; text-overflow: clip;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
}
/* the line under the name does not move with the hover - the icon going
   red is the whole of the signal */

/* Two groups, not five buttons spread across a row: what you can do to the
   result sits on the left, what you can do with it sits on the right. The
   3px between them is the gap the cells beside them already use. */
/* One row, always. The buttons sit at their own width, 3px apart, in the
   order you would use them - nothing is stretched to fill the pane and there
   is no hole in the middle. Squeezed, the words give way before the row does:
   they shrink and clip rather than wrapping to a second line. */
.run-acts {
  flex: 0 0 auto;
  display: flex; gap: 3px; justify-content: flex-start;
  flex-wrap: nowrap;
  background: #060606;
  padding-top: 3px;
}
/* Both groups grow, so the row is filled rather than two clusters with dead
   black between them. The icons keep their square; the words take the slack. */
/* The groups grow too, or there is nothing for the buttons inside them to
   grow into - the slack just sits outside the group as a gap at the end. */
.run-left, .run-right { display: flex; gap: 3px; min-width: 0; flex: 1 1 auto; }
.run-acts button {
  border: 0; cursor: pointer;
  height: 28px; padding: 0 11px;
  display: inline-flex; align-items: center; justify-content: center;
  /* 10px/600 like every other button here, but tracked a shade tighter: at
     0.09em the five of them need 397px of a 390px pane and the row breaks in
     two. The words have to fit on one line for the row to read as one. */
  font-size: 10px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
  white-space: nowrap;
  transition: background 150ms var(--ease), color 150ms var(--ease);
}
/* The three with words in them take whatever the row has left over, so the
   row ends where the picture ends. Growing cannot break the line - flex-grow
   only ever shares out space that is already there - and the icons keep
   their square. Squeezed the other way, the words shrink and clip. */
/* THE CUT'S OWN ROW. Same buttons, same rules, a pane that is a fraction
   of the width - so it keeps the result row's look and drops its left
   half, which on a cut would be three controls with nothing to read. */
.trim-acts { flex: 0 0 auto; }
.trim-acts[hidden] { display: none !important; }
/* AND `hidden` IS A UA `display: none`, so any real display rule beats
   it - .run-use and .run-keep are `display: block` two rules down, which
   would have drawn both of them over a preview. */
.trim-acts button[hidden] { display: none !important; }
/* SAVE MOVED HERE FROM THE ACTIONS ROW, and it is the same button: the
   class and the data-stage are untouched, so the handler, arm() and the
   harness all still find it. What changes is the clothes - `.run-acts
   button` already gives it the height, padding and type, so this is the
   ground and the hover, which .stage-act sets for a cell in a row. */
.trim-acts .stage-act { background: var(--raise-2); font-size: 10px; }
/* the pane plays on a press, so it says so - the picture is the button */
.trim-video.is-on { cursor: pointer; }
.trim-acts .stage-act:not(:disabled):hover { background: #262626; color: var(--ink); }

.run-ask, .run-use, .run-keep {
  flex: 1 1 auto; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; display: block; line-height: 28px;
  text-align: center;
}
.run-ask { background: var(--raise); color: var(--muted); }
.run-ask:hover { background: #262626; color: var(--ink); }
/* Use and Save are the two things you do WITH a result; Submit is the one
   that commits the round, so it keeps the accent to itself. */
.run-use { background: var(--raise-2); color: var(--ink); }
.run-use:hover { background: #262626; }
/* Scoped past `.run-acts button`, which is a class AND an element and so
   outranks a bare class: left unscoped, an icon button inherits the text
   buttons' 14px side padding, which on a 28px button is no room at all -
   that is what flattened the `i` to a sliver. */
/* WHAT THE PANE IS, said in the row under it - Babe, 2026-10-21: "maybe keep
   the row open tho so its not a long button, maybe we just add text / title
   what the panel is for", then "like BLOCKOUT VIEW". The pane was the one
   surface on this tile with no name on it while OBJECTS, ASPECT RATIO and
   SETTINGS all say what they are, and a lone button stretched across the row
   reads as a banner rather than a press. The label takes the slack; the
   button keeps its own width. Set like the group summaries, not like a
   button, so it never reads as a second press. */
.shot-acts .shot-what {
  flex: 1 1 auto; min-width: 0;
  display: inline-flex; align-items: center;
  padding: 0 11px;
  background: var(--raise);
  color: var(--muted);
  font-size: 10px; font-weight: 600;
  letter-spacing: 0.06em; text-transform: uppercase;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.shot-acts .shot-keep { flex: 0 0 auto; }

.run-acts .run-icon {
  width: 28px; padding: 0; flex: 0 0 auto;
  background: var(--raise); color: var(--muted);
  font-size: 11px;
}
.run-icon svg { width: 13px; height: 13px; fill: currentColor; }
/* The i is drawn, not set. A letter centred in a ring never looks centred -
   the glyph carries its own side bearings and its dot pushes the mass off
   the middle - so the stem, the dot and the ring are one drawing, on the
   same stroke weight as the icon beside it. */
.run-info svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.5; }
.run-info svg .dot { fill: currentColor; stroke: none; }
.run-icon:hover { background: #262626; color: var(--ink); }
.run-icon.is-on { background: var(--raise-2); color: var(--ink); }
.run-keep { background: var(--raise-2); color: var(--ink); }
.run-keep:hover:not(:disabled) { background: #262626; }
.run-keep:disabled { background: var(--raise); color: var(--dim); cursor: default; }
/* SUBMIT took the row's only accent with it, which is the right outcome
   rather than a gap to fill: nothing under a result is irreversible any
   more, so nothing under it should be shouting. */

/* ---- what the result is made of ---- */

.run-meta {
  position: absolute; inset: 0; z-index: 2;
  background: #0b0b0b;
  padding: 16px 18px; overflow-y: auto;
  text-align: left;
}
.run-meta[hidden] { display: none; }
.run-out.is-meta .run-view img,
.run-out.is-meta .run-view video { visibility: hidden; }
/* What the workflow is, over what this run of it did. It sits above the
   prompt because it is the slower-changing fact of the two. */
.meta-blurb {
  margin: 0 0 12px; padding-bottom: 11px;
  font-size: 12px; color: var(--ink);
  border-bottom: 1px solid rgba(255,255,255,0.07);
}
.meta-prompt { position: relative; margin-bottom: 14px; padding-right: 54px; }
.meta-prompt b {
  display: block; font-weight: 600; font-size: 9px; line-height: 1;
  letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--dim); margin-bottom: 5px;
}
.meta-prompt p {
  margin: 0; font-size: 13px; line-height: 1.5; color: var(--ink);
  user-select: text; cursor: text;
}
.meta-copy {
  position: absolute; right: 0; top: 0;
  border: 0; cursor: pointer; padding: 5px 8px;
  background: var(--raise-2); color: var(--muted);
  font-size: 9px; font-weight: 600; letter-spacing: 0.09em; text-transform: uppercase;
}
.meta-copy:hover { background: #262626; color: var(--ink); }
.meta-rows { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: 4px 14px; }
.meta-rows dt {
  font-weight: 600; font-size: 9px; line-height: 1.6; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--dim);
}
.meta-rows dd {
  margin: 0; font-size: 11.5px; color: var(--muted);
  user-select: text; overflow-wrap: anywhere; min-width: 0;
}

/* ---- a result at full size ---- */

/* THE ONE MODAL THAT DIMS, and it is the exception that proves the rule
   above. The others announce something that landed on both columns at the
   same second, so the room behind them has to stay readable. This one is
   not an announcement - you opened it to look at ONE picture - and with a
   transparent backdrop the asset grid went on showing through beside it,
   including a thumbnail of the very frame being enlarged. Babe: "we prob
   should dim / blur the background a little bit". */
#big {
  background: rgba(5, 5, 5, 0.74);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
}

/* THE PANEL IS THE PICTURE'S SIZE, not the window's. It was `width:
   min(92vw, 1600px)` - a fixed box whatever was inside it - with the shadow
   on the PANEL, so a portrait frame got a shadow stretching most of the
   screen to either side of it and nothing to cast it. Babe: "there is a
   shadow off to the right, like its not conforming to the assets width and
   height". `.modal` is a centring grid, so dropping the width lets this
   shrink to its content and the shadow hugs the frame. The CAP moves onto
   the media, where it was doing the real work anyway - `max-width: 100%`
   of a shrink-to-fit parent is circular and sized nothing. */
.big-panel {
  display: grid; place-items: center;
  box-shadow: 0 40px 120px -20px rgba(0, 0, 0, 0.95);
}
.big-panel img, .big-panel video {
  max-width: min(92vw, 1600px); max-height: 88vh;
  width: auto; height: auto;
  /* a picture of the wrong shape is the one that finds this: it must be
     held by BOTH caps at once, never cropped to fill */
  object-fit: contain;
  display: block;
}
.run-out img, .run-out video {
  width: 100%; height: 100%;
  min-width: 0; min-height: 0;
  object-fit: contain;
}
.run-out-hint {
  font-size: 9.5px; letter-spacing: 0.08em; text-transform: uppercase;
  color: #3f3b36;
}
/* An empty pane waiting for a result can afford to be nearly invisible. One
   with a job running in it is the thing a spectator is watching, and it holds
   that state for half a minute - so it gets a size you can read from the back
   of the room. */
.run-out-hint.is-running {
  font-family: Oswald, Archivo, sans-serif;
  font-weight: 600; font-size: 34px; letter-spacing: 0.02em;
  /* big enough to read from the back of the room, quiet enough that it is the
     result you look at and not the wait */
  color: #423d38;
}


/* ---- working wide: the panel takes the column while you build ---- */

.side { transition: none; }
.side.is-wide { grid-template-rows: 116px minmax(0, 1fr); }
.side.is-wide .tools { height: auto; min-height: 0; }
/* (the wide view used to bias a cropped frame upward; contain centres it, so
   there is nothing left to bias) */

.pipe-run[hidden] { display: none !important; }
/* RUN FLOATS TOO, and the red moves off the button and onto the square under
   the triangle. A red slab in a row of three bare words was the row's whole
   weight; the same red in a 17px box still says ACTION and stops shouting.
   The width stays fixed, because Run holding its place is what keeps Agent
   and Assets from sliding every time you look at your grid. */
.pipe-run {
  background: none; color: var(--ink);
  width: var(--run-w); padding: 0; justify-content: center;
}
.pipe-run:hover { background: none; color: var(--ink); }
/* INK, STROKE AND ALL. The triangle is filled #0a0a0a but .pipe-btn svg
   leaves `stroke: currentColor` on it, so at 9px a cream outline was most of
   what you saw and the arrow read light-on-orange instead of dark. */
.pipe-run svg { background: var(--accent); fill: #0a0a0a; stroke: #0a0a0a; }
.pipe-run:hover svg { background: #ff3b30; }

/* A panel whose model has not been chosen has nothing to run: the tile is
   the intent and the model is the graph, so Run waits for one. */
.pipe[data-needs-model] .pipe-run { background: none; color: var(--dim); cursor: default; }
.pipe[data-needs-model] .pipe-run:hover { background: none; color: var(--dim); }
.pipe[data-needs-model] .pipe-run svg,
.pipe[data-needs-model] .pipe-run:hover svg { background: var(--raise-2); fill: var(--dim); }

/* And with no workflow open at all there is nothing to run either - but the
   button still holds its place, or Agent and Assets slide along the row the
   moment you go to look at your grid. Dim is the honest state; gone is a
   layout change standing in for one. */
.pipe-run[data-idle] { background: none; color: var(--dim); cursor: default; }
.pipe-run[data-idle]:hover { background: none; color: var(--dim); }
.pipe-run[data-idle] svg,
.pipe-run[data-idle]:hover svg { background: var(--raise-2); fill: var(--dim); }

/* Before a round starts nothing has happened yet, so nothing is lit. The red
   is reserved for what is live - spend it here and it stops meaning that. */
[data-state="fresh"] .pipe-run { background: none; color: var(--muted); }
[data-state="fresh"] .pipe-run:hover { background: none; color: var(--ink); }
[data-state="fresh"] .pipe-run svg { background: var(--raise-2); fill: var(--muted); }
[data-state="fresh"] .pipe-run:hover svg { background: #2e2e2e; fill: var(--ink); }
/* square: exactly as tall as the buttons beside it, and just as wide */
/* A caret is a text glyph: thin, and it sits high in its own line box. A
   filled triangle centres properly and turns over when the panel is open. */
.pipe-wide { padding: 0; width: var(--btn); flex: 0 0 auto; }
.pipe-wide svg {
  display: block; width: 9px; height: 9px;
  fill: currentColor;
  transition: transform 180ms var(--ease);
}
.side.is-wide .pipe-wide svg { transform: rotate(180deg); }


/* ============================ ASSET TYPES + FILTERS ============================ */

.filters { display: flex; gap: 2px; margin-left: 12px; }
.filters[hidden] { display: none; }
.chip {
  border: 0; background: transparent; color: var(--dim);
  padding: 5px 9px;
  font-size: 10px; letter-spacing: 0.08em; text-transform: uppercase;
  cursor: pointer;
  transition: color 150ms var(--ease), background 150ms var(--ease);
}
.chip:hover { color: var(--ink); }
.chip.is-on { background: var(--raise-2); color: var(--ink); }

.asset {
  position: relative;
  aspect-ratio: 1 / 1;
  background: #000;
  overflow: hidden;
  cursor: pointer;
  display: grid; place-items: center;
}
.asset[hidden] { display: none; }
/* the shape of the space, before anything is in it */
.asset-pad { aspect-ratio: 1 / 1; background: #1a1a1a; }
.asset img, .asset video { width: 100%; height: 100%; object-fit: cover; }
/* hover is the sand the ASSETS title is set in; red is reserved for the one
   you have actually chosen, so the two never read as the same state */
.asset:hover { outline: 2px solid var(--ink); outline-offset: -2px; }

/* Removing a frame is quiet until you go for it, like throwing a round away:
   the cross only arrives under the pointer. It carries the same scrim the play
   badge does, because a tile is a photograph and a bare stroke over a bright
   one is invisible. */
.asset-x {
  position: absolute; z-index: 2; top: 0; right: 0;
  width: 18px; height: 18px;
  background: rgba(0, 0, 0, 0.65); border: 0; padding: 0;
  display: grid; place-items: center;
  cursor: pointer; color: #fff; opacity: 0;
  transition: opacity 150ms var(--ease), color 150ms var(--ease);
}
.asset-x svg {
  width: 11px; height: 11px;
  fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round;
}
.asset:hover .asset-x { opacity: 1; }
.asset-x:hover { color: var(--accent); }
.asset-x:focus-visible { opacity: 1; }

.asset-badge {
  position: absolute; left: 5px; bottom: 4px;
  font-size: 9px; line-height: 1;
  color: #fff;
  background: rgba(0, 0, 0, 0.65);
  padding: 3px 5px;
}
.asset[data-kind="audio"] { background: var(--raise-2); gap: 5px; }
.asset[data-kind="audio"] svg {
  width: 26px; height: 26px;
  stroke: var(--muted); fill: none; stroke-width: 1.6; stroke-linecap: round;
}
.asset-cap {
  font-size: 8.5px; letter-spacing: 0.05em; text-transform: uppercase;
  color: var(--dim); text-align: center; padding: 0 4px; line-height: 1.2;
}


/* ---- submitting a result to the round ---- */

.pipe-submit { background: var(--accent); color: #0a0a0a; }
.pipe-submit[hidden] { display: none !important; }

/* The expand arrow is out of the header for now. `hidden` is a UA
   display:none and .pipe-btn sets display:inline-flex, which beats it, so
   this needs !important like every other button here that hides itself.
   The wide view still exists - nothing was deleted, it just has no door. */
.pipe-wide { display: none !important; }

/* ---- BARE ----
   `?look=1` sets data-bare while the new version is being designed. It
   strips nothing at the moment: the cameras came back first - two live
   faces are what make the columns read as two rooms rather than two
   panels - and the rail came back after them, because the game master
   lives down there and a screen with no game master is not this game.
   The hook stays on <body> because the next thing to try may want it. */
/* `hidden` is a UA display:none and .pipe-btn sets display:inline-flex, which
   beats it - so every button that hides itself needs its own !important rule */
.pipe-ask[hidden] { display: none !important; }
.pipe-submit:disabled { background: var(--raise-2); color: var(--dim); cursor: default; }
.pipe-submit:not(:disabled):hover { background: #ff3b30; }

/* and a tile you have chosen stays red while the cursor is still on it */
.asset.is-picked,
.asset.is-picked:hover { outline: 2px solid var(--accent); outline-offset: -2px; }
.asset.is-picked::after {
  content: "";
  position: absolute; inset: 0;
  background: rgba(238, 35, 36, 0.18);
}

/* No label over the picture. What was submitted, and when, is written under
   its round below - saying it twice only covered up the frame. */

.link.is-failed { background: #1a0f0f; }
.link.is-failed .link-t { color: var(--accent-text); }

/* TWO QUESTIONS IN ONE ROW. Whose library you are looking at and which kind
   you want are different axes, so a gap says where the pair ends and the kind
   chips begin - cheaper than a rule, and it survives the row being squeezed. */
.chip-gap { flex: 0 0 auto; width: 10px; }
/* THE NAME IS PINNED, because the chips row SCROLLS. Below about 1400px the
   kind chips do not all fit beside the three controls, so they scroll inside
   their own cell - and a chip sitting last in that scroller is a chip nobody
   can see at Babe's window. Sticky to the right edge keeps the borrow chip on
   screen in the place it was asked for, with the row's own ground behind it
   so the kind chips pass underneath rather than through it. */
.chip-src {
  position: sticky; right: 0;
  background: var(--sunk);
  padding-left: 8px;
  box-shadow: -8px 0 8px -4px var(--sunk);
}
.chip-src.is-on { background: var(--raise-2); }


/* ---- the settings row ----
   One line of pills along the bottom of the form, the way a camera app puts
   its aspect, its size and its length together: what you read is the VALUE,
   and the label is a title rather than a word taking width. Babe pointed at
   Cinematic Studio's "16:9 · 720p · 5s · OFF" for the shape. */
.opt-row { display: flex; flex-wrap: wrap; gap: 4px; align-items: center; }
/* AND A PILL IS ITS OWN WIDTH OR IT WRAPS - never squeezed. A flex child
   shrinks by default, so a row one pixel over its width takes that pixel
   out of a WORD rather than taking a new line, which is how SECONDS came
   to be drawn as SECOND. Wrapping is what this row is for. */
.opt-row > * { flex: 0 0 auto; }
.pick.is-pill {
  display: inline-flex; align-items: center; gap: 0; padding: 0;
  background: var(--raise); min-width: 0;
}
.pick.is-pill .pick-k { display: none; }          /* the value speaks for itself */
.pick-now {
  display: inline-flex; align-items: center; gap: 4px;
  background: none; border: 0; color: var(--muted); cursor: pointer;
  font: inherit; font-size: 10px; font-weight: 600; letter-spacing: 0.06em;
  text-transform: uppercase; white-space: nowrap;
  height: 22px; padding: 0 8px;
}
.pick-now:hover { color: var(--ink); }
.pick-caret { width: 10px; height: 10px; fill: none; stroke: currentColor; stroke-width: 2; opacity: 0.55; }
.pick.is-open .pick-now { color: var(--ink); background: var(--raise-2); }

/* FIXED, like SELECT MODEL's list and for the same reason: the run form is a
   scroller, and a list inside it is clipped by the panel it is escaping.

   AND `nowrap`, WHICH IS THE WHOLE OF THE "BREAKS AT EIGHT" BUG. `.pick-v`
   is a wrapping row by default - it has to be, that is the chip row - and
   this rule turned the direction to `column` and left the WRAP alone. A
   column that wraps inside a 180px box with 22px rows breaks every 8, so
   thirteen camera presets drew as 8 and 5 with a hole beside them (Babe,
   2026-10-19: "can we make this more even? we can add more if needed") and
   twenty-one voices drew as 8 / 8 / 5. The camera was moved to chips and
   the menu's own fault was never fixed - it just stopped being looked at.

   One word, and the list is what it always read as: a single column that
   scrolls. Every pill in the app gets it, because every pill had it. */
.pick.is-pill .pick-v { display: none; }
.pick.is-pill.is-open .pick-v {
  display: flex; flex-direction: column; flex-wrap: nowrap; gap: 0;
  position: fixed; z-index: 40; background: #141414;
  box-shadow: 0 10px 24px rgba(0,0,0,0.6);
  max-height: 180px; overflow-y: auto; scrollbar-width: none;
}
.pick.is-pill.is-open .pick-v::-webkit-scrollbar { display: none; }
.pick.is-pill .pick-v .pick-o { text-align: left; white-space: nowrap; height: 22px; }

/* A NUMBER AND A BOOLEAN KEEP THEIR CONTROLS, because a stepper and a
   two-state switch are already as small as a pill and pressing them once is
   the whole interaction - putting either behind a menu would add a press. */
.pick.is-num .pick-v { display: inline-flex; gap: 0; }
.pick.is-num .pick-s { height: 22px; }
.pick.is-pill[data-kind="bool"] .pick-v { display: inline-flex; gap: 0; }
/* AND THE WORD DECIDES THE WIDTH, NOT A COLUMN. .pick-k is 46px wide
   because the ROW layout wants its keys to line up - but a pill is not in
   a column with anything, and 46px less 14 of padding is 32 for a word
   that needs 41: SECONDS was drawn as SECOND with the S sliced off. Babe,
   2026-10-19: "seeconds is cut off". The row already wraps, so a pill
   that takes the width its own label needs costs a wrap at worst. */
.pick.is-pill[data-kind="bool"] .pick-k,
.pick.is-num .pick-k { display: inline-flex; align-items: center; height: 22px; padding: 0 7px; width: auto; }
