/* 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; }
.card-bout .vs { color: var(--accent); }

/* 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: flex-start; justify-content: flex-end; gap: clamp(14px, 2vw, 28px); }
/* 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%;
}
.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. */
  flex: 0 0 auto; height: 34px;
  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); }
.cell svg { width: 15px; height: 15px; 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; }

.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; }
  .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;
  color: var(--muted); font-size: 22px; 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); }

/* ---- 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; }

/* ---- 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; }
.cell-vote[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;
}

/* ---- PREVIEW: the other column's buttons are gone ----
   Babe: "it might be less confusing while working... can you just hide the
   others buttons for now so i can preview what it looks like." One block,
   deliberately one block, so it comes out in one deletion.

   The argument for it: none of the three reports anything you can act on.
   AGENT and ASSETS answer nothing and say nothing; RUN says whether they
   have armed a model, which is the only crumb in the row. Against it: the
   branch's own rule is that chrome follows the mirror, and this is the
   second time that rule has been bent - the camera switch and the borrow
   chip went first, for the better reason that neither can tell the truth
   about a column that is not yours.

   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 { display: none; }

/* 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; }

/* ---- 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; }

/* 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; }
/* 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; }

/* ---- 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);
}
.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; }
.chat-line.is-artist { align-self: flex-end; text-align: right; 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;
}
.chat-shot {
  align-self: flex-end; display: flex; align-items: center; gap: 8px;
  background: var(--raise); padding: 6px; max-width: 72ch;
}
.chat-shot img { width: 46px; height: 46px; object-fit: cover; }
.chat-shot span {
  font-weight: 600; font-size: 9px; line-height: 1.4; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--dim); padding-right: 6px;
}
.chat-said-shot {
  display: block; margin: 0 0 6px auto;
  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: 3px; margin-top: 6px; }
.line-copy {
  border: 0; background: #232323; padding: 0; cursor: pointer;
  width: 20px; height: 20px;
  display: inline-grid; place-items: center;
  color: var(--muted);
  transition: background 150ms var(--ease), color 150ms var(--ease);
}
.line-copy svg { width: 12px; height: 12px; 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 run it in. The panel keeps its form
   once a workflow has been opened - the assistant covers it rather than
   replacing it - so the prompt box being in the page is the test, and it is
   live: open a workflow after reading the answer and the button arrives. */
.side:not(:has(.run-prompt)) .line-run,
.side:not(:has(.run-prompt)) .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: #232323; color: var(--muted); border: 0; cursor: pointer;
  font: inherit; font-size: 9px; letter-spacing: 0.07em; text-transform: uppercase;
  height: 20px; padding: 0 9px;
  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: #232323; color: var(--muted); }

/* 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. */
.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; padding: 13px 16px;
}
.chat-ask textarea:focus { outline: 0; background: #1c1c1c; }
.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; }
.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; }
.run-form::-webkit-scrollbar-track { background: transparent; }
.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 has to be darker than both sides or it is not a seam: the form
     is #1c1c1c and the result is a picture, so the cut between them is the
     page's own black showing through. */
  gap: 4px;
  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;
}
/* `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);
}
/* ---- 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); }

/* 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;
}
/* ---- 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. */
.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. */
.pick.is-pill .pick-v { display: none; }
.pick.is-pill.is-open .pick-v {
  display: flex; flex-direction: column; 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; }
