/* Arenos chat — layout on top of the Arenos design system.
   Every colour, space and radius comes from ds/styles.css tokens, so a theme
   can repaint the app by overriding them and nothing here needs to change.

   Two literals remain, both video-ground exceptions: white on the thumbnail
   clock and true black on the letterbox matte. The thumbnail is dark under
   every theme, so its foreground does not follow the page the way --color-bg
   would. The matte is the same argument at full strength: it is video, not a
   UI surface. Everywhere text sits on the accent fill instead now goes through
   --color-on-accent rather than a literal white — Arenos amber fails contrast
   outright under white (about 1.9:1) and needs ink, which a hardcoded #fff
   could not express. */

/* Structure, alongside the design system's colour and space tokens.

   Rule weight is a theme's business as much as its palette is: a system built
   on hairlines and small radii is a different object from one built on heavy
   rules and square corners, even reusing the same component classes. --rule,
   --rule-hair and --color-rule live here because ds/styles.css names none of
   them — it has no concept of a border at all, only of the colours one might
   be drawn in. */
:root {
  /* Arenos specifies one hairline weight everywhere — no separate heavier
     structural rule. Both tokens carry the same value so nothing downstream
     has to change to ask for it, and a future theme that does want two weights
     only has to override one of them.

     0.1px does not mean a tenth of a pixel, and it does not mean a faded line.
     Measured in Chrome: any non-zero width below one device pixel snaps up to
     exactly one device pixel, painted at full colour — 0.5px, 0.1px and 0.01px
     are indistinguishable, and none of them disappears. So this reads as "the
     thinnest line the display can draw".

     What that means in practice depends on the display, which is the part
     worth knowing before changing it back. At 1x a 1px rule is already one
     device pixel, so 0.1px paints identically and nothing looks different. At
     2x a 1px rule is two device pixels and 0.1px is one, so the rule is
     genuinely halved. The change is a retina change. */
  --rule: 0.1px;
  --rule-hair: 0.1px;

  /* The colour a rule is drawn in, which is not the colour of text.
     It used to be --color-text, and that read as deliberate while the two
     wanted the same value — near-black on light, near-white on dark. They do
     not: a border wants to sit under the reading, and text wants to carry it.
     Sharing one token meant softening the borders could only be done by
     softening the prose, which on the dark theme's near-black ground is the
     difference between a quiet line and body copy at 1.9:1.

     #3E4248 is a mid-slate that works against both grounds — a soft line on
     #F6F4EF and a visible one on #0F1113 — so it is defined once here rather
     than per theme. A theme that wants its own may override it like any other
     token. */
  --color-rule: #3E4248;
}

body {
  margin: 0;
  background: var(--color-bg);
  font-family: var(--font-body);
  color: var(--color-text);
}

a { color: var(--color-accent-700); text-decoration: none; }
a:hover { color: var(--color-accent); }
input, button, textarea { font-family: var(--font-body); }

.shell {
  height: 100vh;
  min-height: 620px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  background: var(--color-bg);
}

/* ── Brand ──────────────────────────────────────────────────────────── */

/* The brand sits in the session rail's header, not in a bar of its own. With
   Settings and Sign out moved into the account menu at the foot of that rail,
   a full-width header had nothing left in it. */
.brand {
  display: flex;
  align-items: center;
  min-width: 0;
}

/* The full horizontal lockup, tagline and all — artwork bounds 819.358 x 211,
   roughly 3.88:1 — sitting in a 232px rail rather than a full-width header.
   The rail leaves 172px once its 13px padding, the 8px gap and the 26px plus
   button are taken out; at 42px tall this wants about 163px, so it clears the
   control beside it. Taller than the untagged cut used to be at 35px because
   there is a second line of type in it now, and a tagline set too small is
   worse than no tagline. object-fit keeps the ratio; the cap is what stops a
   wider lockup from ever reaching the button. */
.brand-logo {
  height: 42px;
  width: auto;
  max-width: 172px;
  object-fit: contain;
  display: block;
}

/* The tagline cut (819.358 x 211, same ~3.87:1) at a legible sign-in size. */
.signin-logo { height: 34px; width: auto; margin-bottom: var(--space-4); }

.brand-name {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 15px;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  white-space: nowrap;
}

/* ── Transcript ─────────────────────────────────────────────────────── */

.scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 32px 0 24px; }

/* The full width of the content column, on the same 48px gutters the composer
   and the suggestion chips already use — so the card an answer arrives in and
   the box the next question is typed into line up on both edges. The transcript
   used to stop at 860px while the composer did not, which read as a card that
   had not finished loading. */
.transcript {
  padding: 0 48px;
  display: flex;
  flex-direction: column;
  gap: 24px;
}

/* A user turn is a line in the margin; an agent turn is a card. The two are
   different kinds of thing and the transcript reads better for saying so: one
   is what somebody typed, the other is the reply the desk works from — and
   usually the frame around a moments, games or stage-strip list as well. */
.msg-user { border-left: var(--rule) solid var(--color-neutral-400); padding-left: 16px; }

.card {
  background: var(--color-surface);
  border: var(--rule-hair) solid var(--color-divider);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
}

/* Replies land at the foot of a scrolled column, so a card that fades up says
   a new one has arrived without shifting anything already read.
   It is not a decoration that can be left in the markup: the transcript is
   rewritten wholesale on every render, and an agent reply re-renders on every
   streamed token, so the class has to be applied once and then dropped — see
   the WeakSet in render(). */
@keyframes card-fade-up {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}
.fade-in { animation: card-fade-up 0.32s cubic-bezier(0.2, 0.7, 0.3, 1) both; }

@media (prefers-reduced-motion: reduce) {
  .fade-in { animation: none; }
}

.msg-label {
  font-size: 9.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-neutral-600);
  margin-bottom: 6px;
}
.msg-agent .msg-label { color: var(--color-accent-700); }

/* The card is as wide as the column; the prose inside it is not. A line of
   text 1,000px long is measured in the eye having to find the start of the
   next one — 640px is about 80 characters at this size, which is where that
   stops being work. Lists and tiles have no such limit: a row of tiles is
   scanned, not read. */
.msg-text {
  font-size: 14px;
  line-height: 1.6;
  text-wrap: pretty;
  white-space: pre-wrap;
  max-width: 640px;
}

.thinking-text { font-size: 13px; color: var(--color-neutral-600); }

/* ── Inline cards ───────────────────────────────────────────────────── */

.panel { margin-top: 14px; border: var(--rule) solid var(--color-rule); }
.panel-light { margin-top: 14px; border: var(--rule-hair) solid var(--color-neutral-300); }

/* Inside a card the label's own spacing and the card's padding already do most
   of that work, so the panel's margin is trimmed to keep a card that answers
   with a list reading at the same rhythm as one that answers in a sentence. */
.card > .panel,
.card > .panel-light { margin-top: var(--space-3); }

.panel-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 10px 12px;
  border-bottom: var(--rule-hair) solid var(--color-neutral-300);
}

.panel-head-title {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.panel-head-meta {
  font-size: 11px;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  display: flex;
  /* Centred, not stretched: beside a boxed control like the sort, a stretched
     count sits at the top of the row and reads as misaligned with it. */
  align-items: center;
  gap: 10px;
}

/* The chosen sort. A pressed link-button reads as the current state rather
   than as somewhere to go, so it takes the text colour and loses the hand. */
/* The quietest control: a word that acts, with no box around it. Withdrawing
   the reel (#147) took this rule out with the styles that went with it, but
   28 buttons still carry the class — so every one of them fell back to the
   browser's own grey box, and the pressed one to light text on it. */
.link-btn {
  border: 0;
  background: transparent;
  color: var(--color-neutral-600);
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 700;
  padding: 4px 2px;
  cursor: pointer;
}
.link-btn:hover { color: var(--color-accent); }

.link-btn[aria-pressed="true"] {
  color: var(--color-text);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: default;
}

.row { border-bottom: var(--rule-hair) solid var(--color-neutral-300); }
.row:last-child { border-bottom: 0; }

/* ── List panels: a headed section, not a box ───────────────────────── */

/* The moments and games lists always render inside an agent turn, which is
   already a card, so these carry no frame of their own — a box inside a box
   is one border too many and the tiles are what wants the outline. */
.list { display: flex; flex-direction: column; }
.card > .list { margin-top: var(--space-3); }

/* Baselines line a title up with plain text beside it, but the meta now holds
   a bordered control whose own text sits inside padding — so the row centres
   instead, and the title, the count and the control share one middle. */
.list-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  margin-bottom: var(--space-3);
}

/* The count sits opposite the title in mono: it is a number the eye comes back
   to rather than something read once, and tabular figures stop it shifting as
   a page changes. */
.list-count {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 10.5px;
  color: var(--color-neutral-600);
  white-space: nowrap;
}

/* A grid that wraps, not a row that scrolls. Sideways scrolling hides how much
   is there behind an edge with nothing to say how far it runs, and it hid it
   from the pager too — the count underneath was describing a page most of
   which was off-screen. Wrapping puts the whole page in view and lets the
   pager mean what it says.

   auto-fill rather than a fixed column count: the same card is 190px of a
   narrow window and a quarter of a wide one, and the tile has a floor because
   below about 190px the summary stops fitting in three lines. */
.tile-row {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
  gap: var(--space-4);
  padding-bottom: var(--space-3);
}

/* Every moment is a beta result, and the tile says so in the one place a badge
   can sit without taking room from anything: across the corner. A ribbon
   rather than a pill because a pill would have to sit either over the picture,
   where it covers a play, or in the text, where it would push a line down on
   every tile of a page. Hidden from assistive tech on the tile — a screen
   reader reading "beta" before every one of two hundred moments is noise; the
   list's own head is where that belongs. */
/* A banner in the top-right corner of the frame, over the picture: level, so
   it reads as a statement about the moment rather than as a sticker across
   its corner, and out of the way of the clock at bottom-left and the play
   button in the middle. */
.tile-beta {
  position: absolute;
  top: 6px;
  right: 6px;
  max-width: calc(100% - 12px);
  overflow: hidden;
  pointer-events: none;
  z-index: 2;
  border-radius: var(--radius-sm);
}
.tile-beta > span {
  display: block;
  padding: 2px 7px;
  background: var(--color-accent);
  /* Ink on amber, never white: amber as a fill is about 1.9:1 against white. */
  color: var(--color-on-accent);
  font-size: 8px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  white-space: nowrap;
}

.tile {
  position: relative;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3);
  background: var(--color-neutral-200);
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-lg);
  text-align: left;
  font-family: var(--font-body);
  color: var(--color-text);
}

/* The open match, marked the way the session rail marks it. */
.tile[aria-current="true"] { border-color: var(--color-accent); }

button.tile { cursor: pointer; }
button.tile:hover { border-color: var(--color-accent); }

/* Taller than the row thumbnail was, because it is the width of the tile now
   rather than a 72px column beside the text. */
.tile .thumb { height: 56px; }

/* A game has no still to show, and never will: the record is written from the
   whole match rather than from a frame of it. So this is a plain quiet ground
   holding the design's proportions, not the striped texture a moment's
   thumbnail wears — those stripes mean "a picture is loading", and wearing
   them for a picture that is never coming is a promise the card cannot keep.
   Shorter than a moment's frame for the same reason: it is a slot, not an
   image, and it should not out-weigh the text it sits above. */
.tile-thumb {
  display: block;
  height: 44px;
  border-radius: var(--radius-md);
  background: color-mix(in srgb, var(--color-rule) 22%, transparent);
}

/* Which game a tile is from, when the grid spans the desk. Mono and small,
   in the accent-700 ramp — the same treatment as a search result's game line,
   because it is the same fact. */
.tile-game {
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-accent-700);
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tile-name {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  line-clamp: 3;
  overflow: hidden;
  font-size: 12px;
  font-weight: 700;
  line-height: 1.3;
}

/* Mono and sentence case. These are the model's own readings — a class from
   the taxonomy, a duration, a confidence — which is what the mono face is for,
   and uppercasing them turned "4s" into "4S", a duration that reads as a unit
   nobody uses. */
.tile-meta {
  font-family: var(--font-mono);
  font-size: 9.5px;
  font-variant-numeric: tabular-nums;
  color: var(--color-neutral-600);
  line-height: 1.45;
}

.tile-why {
  font-size: 10px;
  font-style: italic;
  line-height: 1.35;
  color: var(--color-neutral-700);
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
}

.tile .job-status { display: block; margin-top: auto; padding-top: var(--space-2); }

/* The two readings a moment is weighed by: what kind of moment the model
   called it, and how sure it was. Pushed to the foot so tiles of different
   text lengths line them up across the row, the way the buttons they replace
   used to. The type is the catalogue's own word, so it stays in the sans; the
   confidence is the model's reading, so it is in the mono, with a bar that
   says the same thing at a glance down a column of tiles. */
.tile-facts {
  margin: auto 0 0;
  padding-top: var(--space-3);
  border-top: var(--rule-hair) solid var(--color-neutral-300);
  display: grid;
  grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr);
  gap: var(--space-3);
}
.tile-fact { min-width: 0; }
.tile-fact dt {
  font-family: var(--font-mono);
  font-size: 9px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-neutral-600);
}
.tile-fact dd {
  margin: 3px 0 0;
  font-size: 11.5px;
  font-weight: 600;
  line-height: 1.3;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.tile-sure { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.sure-meter {
  display: block;
  margin-top: 4px;
  height: 3px;
  border-radius: 2px;
  background: var(--color-neutral-400);
  overflow: hidden;
}
.sure-meter span { display: block; height: 100%; background: var(--color-accent); }

/* Which match the moments below belong to. It is the line that ties the two
   lists together, so it sits under the games rather than above the moments. */
.focus-caption {
  font-size: 11px;
  color: var(--color-neutral-600);
  padding-top: var(--space-1);
}
.focus-caption b { color: var(--color-text); font-weight: 700; }

/* Moments */

.moment-row {
  display: grid;
  grid-template-columns: 72px 1fr auto;
  gap: 12px;
  align-items: center;
  padding: 10px 12px;
}

.thumb {
  position: relative;
  height: 40px;
  background: var(--color-neutral-900);
  overflow: hidden;
  cursor: pointer;
  border: 0;
  padding: 0;
  width: 100%;
}
.thumb:hover { outline: 2px solid var(--color-accent); outline-offset: -2px; }

.thumb-stripes {
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(115deg,
    color-mix(in srgb, var(--color-neutral-900) 80%, var(--color-neutral-700)) 0 7px,
    var(--color-neutral-900) 7px 15px);
}

.thumb img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }

/* The way into a moment: a play button on the frame, since the frame is what
   opens the player. It carries its own dark disc — like the clock and the
   star, it has to read against a bright arena floor as well as a dark crowd —
   and is sized off the frame's height, so the 56px strip of the flat lists
   and the 16:9 frame of the rides board each get one that fits. On hover it
   takes the accent, ink on amber, never white. */
.thumb-play {
  position: absolute;
  left: 50%;
  top: 50%;
  z-index: 1;
  height: min(44px, 62%);
  aspect-ratio: 1 / 1;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.58);
  border: 1.5px solid rgba(255, 255, 255, 0.85);
  transition: background 120ms ease, border-color 120ms ease;
}
.thumb-play::after {
  content: "";
  position: absolute;
  inset: 30% 26% 30% 38%;
  background: #fff;
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}
.thumb:hover .thumb-play,
.thumb:focus-visible .thumb-play {
  background: var(--color-accent);
  border-color: var(--color-accent);
}
.thumb:hover .thumb-play::after,
.thumb:focus-visible .thumb-play::after { background: var(--color-on-accent, #111); }
@media (prefers-reduced-motion: reduce) { .thumb-play { transition: none; } }

.thumb-clock {
  position: absolute;
  left: 4px;
  bottom: 3px;
  font-size: 8.5px;
  font-weight: 700;
  /* On the video ground, not on the page: the thumbnail is dark under every
     theme, so this is white rather than --color-bg, which follows the page. */
  color: #fff;
  z-index: 1;
  /* A timecode — the brand's own example of what the mono face exists for. */
  font-family: var(--font-mono);
}

.moment-label { font-size: 12.5px; font-weight: 700; line-height: 1.25; }

.moment-meta {
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-neutral-600);
  margin-top: 3px;
}

.rerank-why {
  font-size: 10.5px;
  color: var(--color-neutral-700);
  margin-top: 4px;
  font-style: italic;
  line-height: 1.4;
}

/* Buttons: one system, further down under "Buttons, on the brand's own
   terms". What is left here belongs to particular buttons, not to a family. */

/* A ride's way into its whole round: the pane's one filled button, because
   watching the ride is what the pane is for. The length is a timecode, so it
   is set in the mono. */
.play-ride-len {
  font-family: var(--font-mono);
  font-weight: 500;
  letter-spacing: 0;
  opacity: 0.8;
}

.chip {
  border: var(--rule-hair) solid var(--color-neutral-400);
  background: transparent;
  color: var(--color-neutral-800);
  padding: 6px 10px;
  font-size: 11px;
  cursor: pointer;
}
.chip:hover { border-color: var(--color-accent); color: var(--color-accent-700); }
.chip[aria-pressed="true"] {
  border-color: var(--color-accent);
  background: var(--color-accent-100);
  color: var(--color-accent-900);
}

.chip-row { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 12px; }

/* Inline player — opens in place, so a moment is reviewed without a timeline */

.inline-player {
  border-top: var(--rule-hair) solid var(--color-neutral-300);
  background: var(--color-neutral-900);
  position: relative;
}
/* Letterbox bars stay true black: this is video matte, not a UI surface, and
   a tinted ground would read as a rendering fault against the footage. */
.inline-player video { width: 100%; display: block; aspect-ratio: 16 / 9; background: #000; }

.player-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 7px 12px;
  background: var(--color-bg);
  border-top: var(--rule-hair) solid var(--color-neutral-300);
  font-size: 10.5px;
  font-variant-numeric: tabular-nums;
  color: var(--color-neutral-700);
}

/* The player's own controls. The frame carries the range it is on, in the
   mono — it is the model's reading of where the moment is — on a dark chip,
   because the picture behind it is not a UI surface (the same reasoning as
   the thumbnail clock). The bar and the clock under it are relative to the
   range: "0:04 / 5:06" into the ride, not two hours into the recording. */
.player-frame { position: relative; }
.player-tag {
  position: absolute;
  top: var(--space-3);
  left: var(--space-3);
  padding: 3px 7px;
  border-radius: var(--radius-sm);
  background: rgba(0, 0, 0, 0.66);
  color: #FFFFFF;
  font-family: var(--font-mono);
  font-size: 10.5px;
  pointer-events: none;
}

/* The bar is the whole ride; the moment is marked on it. Three layers, in
   painting order: what has been played (a fill), the moment (an amber band
   between an in and an out marker), and the playhead on top. With a band on
   the bar, the played fill steps back to grey so the only amber on it is the
   moment and the head; without one — a full ride, a moment with no ride — the
   fill is amber, as a plain bar's progress is. */
.scrub {
  padding: 12px 12px;
  background: var(--color-bg);
  cursor: pointer;
  touch-action: none;
}
.scrub-track {
  position: relative;
  height: 4px;
  border-radius: 2px;
  background: var(--color-neutral-400);
}
.scrub-fill {
  height: 100%;
  border-radius: 2px;
  background: var(--color-accent);
}
.scrub-track[data-banded="true"] .scrub-fill { background: var(--color-neutral-700); }

.scrub-range {
  position: absolute;
  top: 50%;
  height: 10px;
  transform: translateY(-50%);
  border-radius: 2px;
  /* Stronger than the accent's soft steps: it has to hold its own over the
     grey of what has been played as well as over the empty track. */
  background: color-mix(in srgb, var(--color-accent) 45%, transparent);
  pointer-events: none;
}
.scrub-mark {
  position: absolute;
  top: 50%;
  width: 2px;
  height: 18px;
  border-radius: 1px;
  background: var(--color-accent);
  transform: translate(-1px, -50%);
  /* Only the markers take the pointer, for their titles; the band passes
     clicks through to the bar so seeking works across it. */
  pointer-events: auto;
}
.scrub-mark-in { left: 0; }
.scrub-mark-out { left: 100%; }

.scrub-head {
  position: absolute;
  top: 50%;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--color-accent);
  box-shadow: 0 0 0 2px var(--color-bg);
  transform: translate(-50%, -50%);
  pointer-events: none;
}

.player-controls {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px;
  padding: 0 12px 10px;
  background: var(--color-bg);
}
.pc {
  border: var(--rule-hair) solid var(--color-neutral-400);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--color-text);
  padding: 7px 10px;
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.pc:hover { border-color: var(--color-accent); }
.pc[aria-pressed="true"] {
  border-color: var(--color-accent);
  background: var(--color-accent-100);
  color: var(--color-accent-900);
}
.player-time {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--color-text);
}

/* Beside the player: the ride it is part of, then the moment's own record. */
.detail-body { min-width: 0; }
.ride-panel { display: grid; gap: var(--space-4); padding: 14px 16px 16px; }
.ride-panel-rider { font-size: 16px; font-weight: 600; }
.ride-panel-sub {
  margin-top: 2px;
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--color-neutral-700);
}
.ride-panel-note { margin-top: 4px; font-size: 11px; color: var(--color-neutral-700); }

/* The ride's readings, one tile each. Separate boxes rather than one ruled
   block: a ride has anything from one reading to six, and a shared grid shows
   its empty cells as dead space when the last row is short. */
.score-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(92px, 1fr));
  gap: 6px;
}
.score-tile {
  min-width: 0;
  padding: 8px 10px;
  border: var(--rule-hair) solid var(--color-neutral-400);
  border-radius: var(--radius-sm);
  background: var(--color-neutral-100);
}
.score-key {
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-neutral-600);
}
.score-value {
  margin-top: 2px;
  font-family: var(--font-mono);
  font-size: 14px;
  font-variant-numeric: tabular-nums;
  overflow-wrap: anywhere;
}

.ride-callout {
  padding: 7px 10px;
  border-left: 2px solid var(--color-neutral-500);
  background: var(--color-neutral-200);
  font-size: 11.5px;
  line-height: 1.45;
}
.ride-callout[data-tone="failed"] { border-left-color: var(--color-accent-2); color: var(--color-accent-2); }
.ride-callout[data-tone="confirmed"] { border-left-color: var(--color-accent); }

.panel-section { display: grid; gap: 6px; justify-items: start; }
.panel-section > * { max-width: 100%; }
.panel-section > .source-ref, .panel-section > .range-select { justify-self: stretch; }
.panel-label {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-neutral-700);
}
.panel-label-record {
  padding: 12px 16px 6px;
  border-top: var(--rule-hair) solid var(--color-neutral-300);
}
.panel-line { font-size: 12px; line-height: 1.45; color: var(--color-neutral-800); }
.mono-soft { font-family: var(--font-mono); font-size: 10.5px; color: var(--color-neutral-700); }

/* What is playing, as one control. Full width of its section: the labels are a
   movement and a timecode, and a select sized to its longest option jumps
   width as the ride changes. */
.range-select { width: 100%; font-family: var(--font-body); }

.source-ref {
  margin: 0;
  padding: 8px 10px;
  border: var(--rule-hair) solid var(--color-neutral-300);
  border-radius: var(--radius-sm);
  background: var(--color-neutral-200);
  color: var(--color-neutral-800);
  font-family: var(--font-mono);
  font-size: 10.5px;
  line-height: 1.5;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

/* Ingest */

.dropzone {
  border: var(--rule) dashed var(--color-neutral-500);
  background: var(--color-neutral-100);
  padding: 18px;
  display: flex;
  align-items: center;
  gap: 16px;
}

.dropzone.is-over { border-color: var(--color-accent); background: var(--color-accent-100); }

.dz-thumb {
  width: 74px;
  height: 42px;
  background: var(--color-neutral-900);
  position: relative;
  overflow: hidden;
  flex: none;
}

.dz-name { font-size: 12.5px; font-weight: 700; overflow-wrap: anywhere; }
.dz-meta { font-size: 11px; color: var(--color-neutral-700); margin-top: 3px; }

.file-label {
  border: var(--rule-hair) solid var(--color-rule);
  background: transparent;
  padding: 7px 11px;
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  cursor: pointer;
  white-space: nowrap;
}
.file-label:hover { background: var(--color-accent-100); }

.field-label {
  font-size: 9.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-neutral-600);
}

/* Jobs

   The runs on the desk, as a table. These rules were deleted wholesale by the
   edit that withdrew clip generation — the Jobs block sat next to the
   reel's and went with it — so the card has been rendering as unstyled divs
   since. They are restored here in the shape the table needs.

   A table is the one place in this app that may scroll sideways: five columns
   of a run do not fold into 400px, and stacking them would put us back where
   we started. The wrapper scrolls, the page never does. */

/* Still the row shape of the activity feed and of every card's empty state,
   which are lists of one thing rather than tables. */
.job { padding: 11px 12px; border-bottom: var(--rule-hair) solid var(--color-neutral-300); }
.job:last-child { border-bottom: 0; }
.job-top { display: flex; justify-content: space-between; gap: 12px; align-items: baseline; }

.job-table-wrap { overflow-x: auto; }

/* Each job is its own panel. The rows stay one table, so the columns still
   line up down the page and the header still names them — but `separate`
   collapsing with a vertical gap gives every job its own box, and a run with
   an error or a list of events under it is visibly one thing rather than rows
   that happen to be adjacent. A table cannot take a border on its row group in
   any useful way, so the box is drawn on the cells: top and bottom on the
   first and last row of the group, sides on the first and last cell of each. */
.job-table {
  width: 100%;
  border-collapse: separate;
  border-spacing: 0 10px;
  font-size: 12px;
}
.job-table thead th { padding-bottom: 0; }

.job-table th {
  text-align: left;
  padding: 8px 12px;
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-neutral-600);
  border-bottom: var(--rule-hair) solid var(--color-neutral-300);
  white-space: nowrap;
}

.job-table td { padding: 9px 12px; vertical-align: top; }

/* The panel itself. Each job is its own tbody, so "this job" is something the
   markup states rather than something these rules have to infer from
   adjacency — which matters because a run can be three rows and none of the
   boundaries between them are a boundary between jobs. */
.job-group > tr > td {
  background: var(--color-neutral-100);
  border-top: var(--rule-hair) solid var(--color-neutral-300);
  border-bottom: var(--rule-hair) solid var(--color-neutral-300);
}
/* Only the outside of the panel is drawn: the rows inside it are one job. */
.job-group > tr:not(:first-child) > td { border-top: 0; }
.job-group > tr:not(:last-child) > td { border-bottom: 0; }
.job-group > tr > td:first-child {
  border-left: var(--rule-hair) solid var(--color-neutral-300);
  padding-left: 14px;
}
.job-group > tr > td:last-child {
  border-right: var(--rule-hair) solid var(--color-neutral-300);
  padding-right: 14px;
}
.job-group > tr:first-child > td:first-child { border-top-left-radius: var(--radius-lg); }
.job-group > tr:first-child > td:last-child { border-top-right-radius: var(--radius-lg); }
.job-group > tr:last-child > td:first-child { border-bottom-left-radius: var(--radius-lg); }
.job-group > tr:last-child > td:last-child { border-bottom-right-radius: var(--radius-lg); }
/* A note row's own cell spans the panel, so it needs both sides. */
.job-note > td { border-left: var(--rule-hair) solid var(--color-neutral-300);
  border-right: var(--rule-hair) solid var(--color-neutral-300); }
/* A note under its own row is a continuation of it, so it opens with no gap.
   One that opens a job — the re-analyse form — keeps its padding. */
.job-row + .job-note > td { padding-top: 0; }

.job-cell-name { min-width: 180px; }
.job-name { font-size: 12px; font-weight: 700; overflow-wrap: anywhere; }

.job-status {
  font-size: 9.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--color-neutral-600);
}
.job-status[data-tone="running"] { color: var(--color-neutral-800); }
.job-status[data-tone="failed"] { color: var(--color-accent-2); }

.job-stage {
  font-size: 11px;
  color: var(--color-neutral-700);
  line-height: 1.4;
  min-width: 160px;
}

/* The events a recording produced, listed under it. A disclosure rather than
   a popup: the next thing done with this list is opening one of them, and a
   dialog that has to be dismissed first is in the way of that. */
.job-cell-events { white-space: nowrap; }
.job-events { padding: 0; text-decoration: underline; text-underline-offset: 2px; }
.job-events-list { display: grid; gap: 8px; padding: 2px 0 4px; }
.job-event-name { padding: 0; font-size: 12px; font-weight: 700; text-align: left; }
.job-event-meta {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--color-neutral-700);
  margin-top: 2px;
}

/* The figure is the control. Underlined so a number in a table reads as
   something to press rather than as data, and mono like every other figure. */
.job-cell-progress { white-space: nowrap; }
.job-progress { padding: 0; text-decoration: underline; text-underline-offset: 2px; }
.job-pct { font-family: var(--font-mono); font-size: 11px; font-variant-numeric: tabular-nums; }
.job-pct-none { color: var(--color-neutral-500); }

.job-cell-actions { width: 1%; }
.job-actions { display: flex; gap: 10px; align-items: baseline; white-space: nowrap; }

.job-error {
  display: flex;
  gap: 10px;
  align-items: flex-start;
  margin-top: 7px;
  border-left: var(--rule) solid var(--color-accent-2);
  background: var(--color-accent-2-100);
  padding: 8px 10px;
}
.job-error p { font-size: 11px; line-height: 1.45; color: var(--color-accent-2-900); flex: 1; margin: 0; }

/* The re-split dialog. It opens showing what the recording holds now, because
   this is a correction and the first thing anybody needs is what is wrong. */
.resplit-body { padding: 4px 16px 18px; }
.resplit-current {
  display: grid;
  gap: 4px;
  margin-top: 6px;
  padding: 8px 10px;
  background: var(--color-neutral-100);
  border-left: var(--rule) solid var(--color-neutral-500);
}
.resplit-class { display: flex; gap: 10px; align-items: baseline; justify-content: space-between; }
.resplit-class-name { font-size: 12px; font-weight: 700; overflow-wrap: anywhere; }
.resplit-class-meta {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--color-neutral-700);
  white-space: nowrap;
}

/* The progress popup: a small dialog, not the player's full-viewport one. It
   holds four stage meters and a bar, and a dialog sized to the screen for that
   would be mostly empty. */
.progress-body { padding: 14px 16px 20px; }
.progress-state {
  font-size: 9.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-neutral-700);
}

/* Progress */

.meter { flex: 1; height: 4px; background: var(--color-neutral-300); }
.meter i { display: block; height: 4px; background: var(--color-accent); }
.meter-neutral i { background: var(--color-neutral-800); }

.meter-row { display: flex; align-items: center; gap: 9px; margin-top: 8px; }

/* Stage strip: one segment per pipeline stage, widths in proportion to how long
   each stage actually takes, so the bar moves at a roughly constant rate
   through a run rather than sitting still through the hour of analysis. */
.stage-strip { display: flex; gap: 4px; margin-top: 9px; }
.stage { flex-grow: 1; flex-basis: 0; }
.stage[data-state="todo"] { opacity: 0.45; }
.stage-meter { height: 4px; background: var(--color-neutral-300); }
.stage-meter i { display: block; height: 4px; background: var(--color-neutral-800); }
.stage[data-state="active"] .stage-meter i { background: var(--color-accent); }

.stage-label {
  margin-top: 4px;
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-neutral-700);
}

.stage[data-state="active"] .stage-label { color: var(--color-accent); }

.meter-pct {
  font-size: 10.5px;
  font-variant-numeric: tabular-nums;
  color: var(--color-neutral-700);
}

/* Composer */

.composer { flex: none; border-top: var(--rule) solid var(--color-rule); }

.suggestions { padding: 12px 48px 8px; display: flex; flex-wrap: wrap; gap: 7px; }

.composer-inner { padding: 0 48px 18px; }

.composer-box {
  display: flex;
  border: var(--rule) solid var(--color-rule);
  background: var(--color-neutral-100);
}

.composer-input {
  flex: 1;
  border: 0;
  outline: 0;
  background: transparent;
  padding: 13px 14px;
  font-size: 13.5px;
  color: var(--color-text);
  min-width: 0;
}

/* The examples are a hint, not a value waiting to be sent: the brand's light
   weight and a quiet colour keep them from reading as typed text. What
   somebody types sits at the body weight above. */
.composer-input::placeholder {
  font-weight: 300;
  color: var(--color-neutral-600);
  opacity: 1;   /* Firefox dims placeholders by default; the colour is the dimming. */
}

.composer-send {
  border: 0;
  border-left: var(--rule) solid var(--color-rule);
  background: var(--color-accent);
  color: var(--color-on-accent, #fff);
  padding: 0 20px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  cursor: pointer;
}
.composer-send:hover { background: var(--color-accent-600); }
.composer-send:disabled { opacity: 0.45; cursor: not-allowed; }

/* Sign-in */

.signin {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  background: var(--color-bg);
  z-index: 50;
}

.signin-card {
  width: min(420px, 92vw);
  border: var(--rule) solid var(--color-rule);
  background: var(--color-neutral-100);
  padding: var(--space-8);
}

.signin-card h1 { font-size: 28px; margin: 0 0 var(--space-2); }

.signin-alt { display: flex; align-items: center; gap: 10px; margin: 16px 0 12px; }
.signin-alt > span:first-child,
.signin-alt > span:last-child { flex: 1; height: 1px; background: var(--color-neutral-300); }
.signin-card p { font-size: 14px; color: var(--color-neutral-800); margin-bottom: var(--space-6); }

.error-note {
  border-left: var(--rule) solid var(--color-accent-2);
  background: var(--color-accent-2-100);
  color: var(--color-accent-2-900);
  padding: 9px 11px;
  font-size: 12px;
  margin-top: var(--space-3);
}

.hidden { display: none !important; }

@media (max-width: 900px) {
  .transcript, .suggestions, .composer-inner { padding-left: 20px; padding-right: 20px; }
}

/* Slot the live player is re-parented into after each render. */
.player-slot:empty { display: none; }

/* Game details */
/* Two columns, not the moment row's three: a game has no thumbnail to play,
   so borrowing that grid would squeeze the headline into the 72px thumb slot. */
.game-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 12px;
  align-items: center;
  padding: 10px 12px;
}

.game-summary {
  padding: 0 12px 12px;
  font-size: 12.5px;
  line-height: 1.5;
  color: var(--color-neutral-800);
}

/* Aligned under the headline rather than the thumb column, so the outcome
   reads as part of the same statement. */
.game-outcome {
  margin-top: 4px;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--color-accent);
}

.header-select {
  font-family: var(--font-body);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  border: var(--rule) solid var(--color-rule);
  background: transparent;
  color: var(--color-text);
  padding: 6px 8px;
  margin-right: 8px;
  cursor: pointer;
}

/* Firebase restores a stored session asynchronously and reports null first.
   Painting nothing until that resolves stops every reload flashing the sign-in
   card, which reads as having been logged out. */
body:not([data-auth-resolved]) .signin,
body:not([data-auth-resolved]) .shell { visibility: hidden; }

/* Settings */
.modal {
  position: fixed;
  inset: 0;
  background: color-mix(in srgb, var(--color-text) 45%, transparent);
  display: flex;
  align-items: flex-start;
  justify-content: center;
  padding: 8vh 16px;
  z-index: 50;
}

.modal-card {
  width: min(520px, 100%);
  background: var(--color-neutral-100);
  border: var(--rule) solid var(--color-rule);
  max-height: 84vh;
  overflow-y: auto;
}

.modal-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  border-bottom: var(--rule) solid var(--color-rule);
}

.modal-title {
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 14px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.setting { padding: 14px 16px; border-bottom: var(--rule-hair) solid var(--color-neutral-300); }
.setting:last-child { border-bottom: 0; }

.setting-hint {
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--color-neutral-800);
  margin: 4px 0 9px;
}

/* Sessions panel */
.workspace { flex: 1; display: flex; min-height: 0; }

/* The content column: transcript above, composer pinned under it. The composer
   lives here rather than in the shell so it spans the content only — full
   width put a search bar under the session list, which is not what it
   searches. */
.main-area {
  flex: 1;
  min-width: 0;
  min-height: 0;
  display: flex;
  flex-direction: column;
}

.sessions {
  width: 232px;
  flex: none;
  display: flex;
  flex-direction: column;
  border-right: var(--rule) solid var(--color-rule);
  background: var(--color-neutral-100);
  min-height: 0;
}

/* The rail's own header: the brand, and one control. */
.sessions-header {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-4);
  /* Room for the tagline cut to breathe. The plus button stays on its
     centre line. */
  min-height: 66px;
  border-bottom: var(--rule-hair) solid var(--color-neutral-300);
}

/* A plus, not a labelled bar. Starting a session is a frequent, low-stakes
   action; giving it the accent across the full width of the rail spent the
   one scarce colour in the system on the least important thing on screen. */
.sessions-new {
  flex: none;
  width: 26px;
  height: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: var(--rule-hair) solid var(--color-neutral-300);
  border-radius: var(--radius-md);
  background: var(--color-neutral-200);
  color: var(--color-neutral-700);
  cursor: pointer;
  padding: 0;
}
.sessions-new:hover {
  background: var(--color-accent-100);
  border-color: var(--color-accent);
  color: var(--color-accent-700);
}

.sessions-list { flex: 1; overflow-y: auto; min-height: 0; }

/* Recency separator. Sessions are ordered newest first, so these split the
   list where the day does — one label per group, never a label with nothing
   under it. */
.session-group {
  font-family: var(--font-mono);
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--color-neutral-600);
  padding: var(--space-4) var(--space-4) var(--space-2);
}
.session-group:first-child { padding-top: var(--space-3); }

/* ── Account ────────────────────────────────────────────────────────── */

.sessions-footer {
  flex: none;
  position: relative;
  border-top: var(--rule-hair) solid var(--color-neutral-300);
}

.account {
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4);
  border: 0;
  background: transparent;
  cursor: pointer;
  font-family: var(--font-body);
  text-align: left;
  color: var(--color-text);
}
.account:hover { background: var(--color-accent-100); }

.avatar {
  flex: none;
  width: 24px;
  height: 24px;
  border-radius: var(--radius-md);
  border: var(--rule-hair) solid var(--color-neutral-300);
  background: var(--color-neutral-200);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 11px;
  font-weight: 500;
  color: var(--color-neutral-700);
  text-transform: uppercase;
}

.account-name {
  flex: 1;
  min-width: 0;
  font-size: 12px;
  color: var(--color-neutral-800);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.account-chevron { flex: none; color: var(--color-neutral-600); transition: transform 120ms; }
.account[aria-expanded="true"] .account-chevron { transform: rotate(180deg); }

/* Opens upward: the button is at the foot of the rail, so a menu below it
   would be off-screen. */
.account-menu {
  position: absolute;
  left: var(--space-3);
  right: var(--space-3);
  bottom: calc(100% - var(--space-2));
  z-index: 5;
  display: flex;
  flex-direction: column;
  padding: var(--space-1);
  background: var(--color-neutral-100);
  border: var(--rule-hair) solid var(--color-neutral-300);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
}

.account-menu-item {
  border: 0;
  background: transparent;
  text-align: left;
  padding: var(--space-3) var(--space-4);
  border-radius: var(--radius-md);
  font-family: var(--font-body);
  font-size: 12.5px;
  color: var(--color-text);
  cursor: pointer;
}
.account-menu-item:hover { background: var(--color-accent-100); color: var(--color-accent-700); }

/* The row holds the session button and its delete control side by side; the
   delete only appears on hover or focus, so a list of matches is not a row of
   crosses inviting an accident. */
.session-row { display: flex; align-items: stretch; border-bottom: var(--rule-hair) solid var(--color-neutral-300); }
.session-row:hover { background: var(--color-accent-100); }

.session-delete {
  flex: none;
  width: 30px;
  border: 0;
  background: transparent;
  color: var(--color-neutral-700);
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
}

.session-row:hover .session-delete,
.session-delete:focus-visible { opacity: 1; }
.session-delete:hover { color: var(--color-accent); }

.session {
  flex: 1;
  min-width: 0;
  display: block;
  text-align: left;
  border: 0;
  background: transparent;
  padding: 11px 14px;
  cursor: pointer;
  font-family: var(--font-body);
  /* A <button> takes the browser's own text colour, not the page's — black,
     which on the dark rail left every session name unreadable. */
  color: var(--color-text);
}

/* The open session is marked with a rule rather than a fill, so the list keeps
   reading as one column of equal items. */
.session[aria-current="true"] { border-left: 3px solid var(--color-accent); padding-left: 11px; }

.session-name {
  font-size: 12.5px;
  font-weight: 700;
  overflow-wrap: anywhere;
}

.session-meta {
  margin-top: 3px;
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-neutral-700);
}

.session[data-tone="running"] .session-meta { color: var(--color-neutral-800); }
.session[data-tone="failed"] .session-meta { color: var(--color-accent-2); }

.sessions-empty {
  padding: 14px;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--color-neutral-800);
}

@media (max-width: 720px) {
  /* Below this the transcript needs the whole width more than the list does. */
  .sessions { display: none; }
}

/* Details popup: the moment on the left, its record on the right.

   Opening the details of a play is the point at which someone wants to see it,
   so the two sit side by side rather than the facts replacing the footage. The
   card is wider than the settings dialog for that reason — 520px of video and
   a two-column table beside it is neither. */
.modal-card-wide { width: min(1180px, 100%); }

/* The video takes the larger share. The record beside it is a two-column table
   of short values, which reads fine narrow, while a 16:9 frame squeezed to
   half a dialog is the thing someone opened this to look at. */
.detail-split {
  display: grid;
  grid-template-columns: minmax(0, 1.75fr) minmax(0, 1fr);
}

.detail-player {
  border-right: var(--rule) solid var(--color-rule);
  /* The card's own surface: the video carries its matte (.inline-player), and
     the ride's summary sits under it on the same ground as the column beside. */
  background: var(--color-neutral-100);
}
.player-under:not(:empty) { border-top: var(--rule-hair) solid var(--color-neutral-300); }
/* What drives the player, above the player. The rule is on the underside, so
   the chips read as belonging to the video rather than to the title above.
   The padding is its own: the section is a bare panel here rather than one of
   .ride-panel's children, and its 16px sides are that panel's, so the chips
   line up with the ride's summary underneath. */
.player-over:not(:empty) {
  padding: 12px 16px;
  border-bottom: var(--rule-hair) solid var(--color-neutral-300);
}

/* A game record has nothing to play, so it gets the whole width rather than a
   black column where a video would be. */
.detail-split.solo { grid-template-columns: 1fr; }
.detail-split.solo .detail-player { display: none; }

/* Sticky against the card's own scroll, so a long record scrolls past a video
   that stays where it is. The column stretches to the row height by default,
   which is what gives this somewhere to stick. */
.detail-player .player-slot { position: sticky; top: 0; z-index: 1; }
/* With the ride's summary under it, a stuck player would slide that summary
   up behind the video on any screen shorter than both — so it scrolls. The
   chips above it are the same argument from the other side: a sticky element
   with content above it sticks to the top of the card, not under them. */
.detail-player:has(.player-under:not(:empty)) .player-slot,
.detail-player:has(.player-over:not(:empty)) .player-slot { position: static; }

/* Below this the two would each be too narrow to read, so they stack: the
   video first, because it is what was asked for. */
@media (max-width: 760px) {
  .detail-split { grid-template-columns: 1fr; }
  .detail-player {
    border-right: 0;
    border-bottom: var(--rule) solid var(--color-rule);
  }
  .detail-player .player-slot { position: static; }
}

/* ── The player popup is the whole screen ───────────────────────────────────

   A moment is judged by watching it, and every pixel this dialog gave back to
   the page behind it was a pixel off the picture. So the details popup is
   full-bleed: no backdrop margin, no rounded card floating in the middle, the
   viewport's own height. Only this popup — the settings dialog is a short form
   and a full-screen form is a worse form.

   The card is a column: a head that stays, and a split that takes the rest and
   scrolls inside its two halves rather than scrolling the card. Without the
   min-height:0 on the split and on each column, a grid child refuses to shrink
   below its content and the whole thing grows past the viewport, which is the
   one way this can silently stop being full height. */
#details { padding: 0; align-items: stretch; }

#details .modal-card {
  width: 100%;
  max-width: none;
  height: 100dvh;
  max-height: none;
  border: 0;
  border-radius: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}

#details .detail-split { flex: 1; min-height: 0; }
#details .detail-grid,
#details .detail-body { min-height: 0; overflow-y: auto; }

/* The player column is its own column: the chips at the top and the video keep
   their place while the ride's summary under them scrolls. Scrolling the whole
   column instead would take the video off screen, which is the one thing this
   popup exists to show. */
#details .detail-player {
  display: flex;
  flex-direction: column;
  min-height: 0;
  overflow: hidden;
  border-radius: 0;
}
#details .player-over { flex: none; }
#details .player-under { flex: 0 1 auto; min-height: 0; overflow-y: auto; }

/* Full height means the video can have the height rather than an aspect ratio
   deciding it. The frame takes what is left, and the picture is letterboxed
   inside it — object-fit, so a 16:9 recording in a tall column keeps its shape
   instead of stretching. `:not(:empty)` because an empty slot is display:none,
   and a flex child cannot be both. */
#details .detail-player .player-slot:not(:empty) {
  flex: 1;
  min-height: 0;
  display: flex;
  position: static;
}
#details .inline-player { flex: 1; min-height: 0; display: flex; flex-direction: column; }
#details .player-frame { flex: 1; min-height: 180px; }
#details .inline-player video {
  height: 100%;
  aspect-ratio: auto;
  object-fit: contain;
}

/* Stacked, the split scrolls as one column: two independently scrolling halves
   on a phone is two scrollbars nobody asked for, and the video is no longer
   beside the record but above it. */
@media (max-width: 760px) {
  #details .detail-split { overflow-y: auto; }
  #details .detail-player,
  #details .detail-grid,
  #details .detail-body { overflow-y: visible; }
  #details .detail-player,
  #details .detail-player .player-slot:not(:empty),
  #details .inline-player { display: block; }
  #details .player-under { overflow-y: visible; }
  #details .player-frame { min-height: 0; }
  #details .inline-player video { height: auto; aspect-ratio: 16 / 9; }
}

/* ── Taking a moment off the desk ─────────────────────────────────────────── */

/* The two ways out of a moment sit in the popup's head, between the title and
   Close. The title gives way rather than the buttons wrapping: a head that
   reflows as the title changes length moves the buttons under the cursor. */
.modal-actions { display: flex; gap: 8px; align-items: center; margin-left: auto; padding-right: 12px; }

/* The head is a title and its actions, not a toolbar: at the page's default
   button size they crowded the title. One compact size for all of them. */
.modal-head .btn-primary,
.modal-head .btn-quiet,
.modal-head .btn-ghost {
  padding: 3px 10px;
  font-size: 11.5px;
}

/* One width, but only where there is a set to be equal to. On the player the
   head carries Download, Publish and Close and three lengths of word read as
   three sizes of button; on the event record Close stands alone, and padding
   it out to match buttons that are not there is just a wide Close. */
.modal-head:has(#details-actions button) .btn-primary,
.modal-head:has(#details-actions button) .btn-quiet,
.modal-head:has(#details-actions button) .btn-ghost {
  min-width: 74px;
}
#details .modal-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

.publish-panel { padding: 14px 16px 20px; display: flex; flex-direction: column; gap: 6px; }
.publish-panel .field-label { margin-top: 10px; }
.publish-panel .input { width: 100%; }
.share-text { font-family: var(--font-body); line-height: 1.5; resize: vertical; }

/* The trim reads as one row of three readings — in, out, and what they come
   to — because the length is the number being aimed at and putting it beside
   the two that decide it is the whole point of showing it. */
.trim-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 14px;
  padding: 10px 0 4px;
}
.trim-controls { display: flex; align-items: center; gap: 6px; margin-top: 4px; }
.trim-at,
.trim-len {
  font-family: var(--font-mono);
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
}
.trim-len { margin-top: 4px; display: block; }
/* Past what the cut can be, so the figure says so before the button does. */
.trim-len.is-over { color: var(--color-accent-2-900); font-weight: 700; }

.platform-row { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 16px; }
.platform-soon {
  font-size: 11.5px;
  color: var(--color-neutral-600);
  letter-spacing: 0.04em;
}

.share-error {
  font-size: 12px;
  line-height: 1.45;
  color: var(--color-accent-2-900);
  background: var(--color-accent-2-100);
  padding: 8px 10px;
  border-radius: var(--radius-sm);
  margin-top: 10px;
}
/* "Published." and the link it produced, on one line and on one baseline.
   They were a flex row with the default stretch, so the link — which was
   wearing .link-btn, a *button* style at 11px/700 with its own padding — sat
   a few pixels low against 12px body text and read as a second kind of thing.
   A URL the channel gave back is a reading, so it takes the mono and the
   accent, and it is allowed to break rather than push the row wide. */
.share-done {
  font-size: 12px;
  margin-top: 10px;
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  flex-wrap: wrap;
}
.share-link {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--color-accent-700);
  text-decoration: underline;
  text-underline-offset: 2px;
  overflow-wrap: anywhere;
  min-width: 0;
}
.share-link:hover { color: var(--color-accent-900); }

/* ── The channel, in Settings ─────────────────────────────────────────────── */

.yt-line { font-size: 12px; color: var(--color-neutral-700); margin-top: 6px; }
/* Configured and connected are the two facts worth seeing at a glance, so they
   are the only thing here that takes the accent. */
.yt-line.is-on { color: var(--color-accent-900); font-weight: 500; }
.yt-actions { display: flex; align-items: center; gap: 10px; margin-top: 10px; flex-wrap: wrap; }
.yt-details { margin-top: 12px; }
.yt-details summary {
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-neutral-600);
  cursor: pointer;
}
.yt-details .input { width: 100%; margin-bottom: 4px; }
.yt-uri {
  font-family: var(--font-mono);
  font-size: 11px;
  word-break: break-all;
  display: block;
  margin-top: 4px;
}

/* Moment details */
.moment-actions { display: flex; align-items: center; gap: 10px; flex: none; }

/* Two columns: the attribute name, then its value. The name column is sized to
   the longest label rather than a fraction of the width, so the values line up
   and stay readable when a description runs long. */
.detail-grid { display: grid; grid-template-columns: max-content 1fr; }

.detail-key,
.detail-value {
  padding: 9px 14px;
  border-bottom: var(--rule-hair) solid var(--color-neutral-300);
  font-size: 12px;
  line-height: 1.45;
}

.detail-key {
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  font-size: 10px;
  color: var(--color-neutral-700);
  white-space: nowrap;
  border-right: var(--rule-hair) solid var(--color-neutral-300);
}

.detail-value { overflow-wrap: anywhere; }


/* A game's record opened inside the list rather than in the popup. Indented
   and quieter than a row of its own, so the headline above it stays the thing
   being scanned. */
.detail-inline {
  border-top: var(--rule-hair) solid var(--color-neutral-300);
  margin-top: 8px;
  background: var(--color-neutral-100);
}
.detail-inline .detail-key,
.detail-inline .detail-value { padding: 6px 12px; font-size: 11px; }
.detail-inline > :nth-last-child(-n + 2) { border-bottom: 0; }

@media (max-width: 520px) {
  .detail-grid { grid-template-columns: 1fr; }
  .detail-key { border-right: 0; padding-bottom: 0; border-bottom: 0; }
}

/* Pager */
.pager {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 9px 12px;
  border-top: var(--rule-hair) solid var(--color-neutral-300);
}

.pager-count {
  font-size: 10.5px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  color: var(--color-neutral-700);
  font-family: var(--font-mono);
}



/* ── Rounding ───────────────────────────────────────────────────────────
   Arenos radii are small and deliberate (8px cards, 5px inputs/chips, 3px
   controls — see ds/styles.css) rather than zero, so these rules matter now;
   they stay written as tokens rather than literals so a future theme that
   wants a different radius scale only has to override the three variables.

   Containers that round also clip. A row's own background or bottom rule is
   square, so without overflow it draws over the corner it is supposed to sit
   inside — visible as a notch on exactly one row of a list. */

.panel,
.panel-light,
.modal-card,
.dropzone,
.signin-card,
.inline-player,
.detail-player,
.composer-box,
.stage-strip {
  border-radius: var(--radius-lg);
  overflow: hidden;
}

/* ── Search across the desk ─────────────────────────────────────────────── */

/* The panel is the question restated with its scope beside it, so what is
   being searched and where are read together. Chips for the options, because
   they are a small closed set and a pressed chip reads as the current state. */
.search-panel {
  margin-top: var(--space-3);
  padding: var(--space-3);
  background: var(--color-neutral-200);
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-md);
}
.search-query {
  margin: var(--space-2) 0 var(--space-3);
  font-size: 13px;
  font-style: italic;
  color: var(--color-neutral-800);
}
.search-opts { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: var(--space-2); }
.search-panel .field-label { display: block; margin-top: var(--space-3); }

/* A result is a button the width of its row: the whole thing opens. The game
   is the first line, because across the desk a moment without its match is a
   sentence without a subject. */
.search-result {
  display: block;
  width: 100%;
  text-align: left;
  padding: 10px 12px;
  border: 0;
  background: transparent;
  color: var(--color-text);
  cursor: pointer;
  font-family: var(--font-body);
}
.search-result:hover { background: color-mix(in srgb, var(--color-text) 4%, transparent); }
.search-game {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--color-accent-700);
  margin-bottom: 3px;
}
.search-why {
  margin-top: 4px;
  font-size: 10.5px;
  font-style: italic;
  line-height: 1.4;
  color: var(--color-neutral-700);
}

/* Games still analysing, under the desk's key moments: their moments do not
   exist yet, and a list that silently left them out would read as "nothing in
   those". Mono, because it is a list of job names, not prose. */
.desk-running {
  margin-top: var(--space-2);
  font-family: var(--font-mono);
  font-size: 10.5px;
  color: var(--color-neutral-600);
}

/* ── Context links: at upload, and on analyse-again ────────────────────── */

/* The one question in the ingest panel that is a choice rather than a value:
   a checkbox with its label and hint stacked beside it. */
/* ── Adding a video, or booking one ───────────────────────────────────────
   Two panels rather than two tabs of one: a file is here now and a live event
   is a reservation, and they share only the sport, the links and the clips
   question. Both are a grid of labelled fields with one footer that says
   whether the form is answerable yet. */
.ingest-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 12px;
  padding: 16px 18px 0;
}
.ingest-head-text { min-width: 0; }
.ingest-eyebrow {
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-accent);
}
.ingest-lede {
  font-size: 12.5px;
  color: var(--color-neutral-700);
  margin-top: 5px;
}

.ingest-grid {
  display: grid;
  grid-template-columns: minmax(0, 2.4fr) minmax(0, 1fr);
  gap: 12px 16px;
  padding: 15px 18px 18px;
}
.live-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.ingest-field { min-width: 0; }
.ingest-wide { grid-column: 1 / -1; }
.ingest-field .field-label { display: block; margin-bottom: 5px; }
.ingest-field .composer-input { width: 100%; }
.ingest-field .setting-hint { margin-top: 5px; line-height: 1.45; }
/* A path and a timestamp are read character by character. */
.mono-input { font-family: var(--font-mono); font-size: 12px; }

.ingest-sports { display: flex; flex-wrap: wrap; gap: 6px; }

/* One source, chosen — a file, a path, or a playlist. */
.src-btn {
  padding: 6px 12px;
  background: transparent;
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: 7px;
  font-family: var(--font-body);
  font-size: 11.5px;
  font-weight: 500;
  color: var(--color-neutral-700);
  cursor: pointer;
}
.src-btn:hover { border-color: var(--color-accent); }
.src-btn[aria-pressed="true"] { border-color: var(--color-accent); color: var(--color-accent); }
.ingest-field .ingest-sources { margin-bottom: 8px; }

.ingest-clips { display: flex; gap: 9px; align-items: flex-start; cursor: pointer; }
.ingest-clips input { margin-top: 3px; flex: none; }
.ingest-clips-title { display: block; font-size: 12.5px; font-weight: 500; }
.ingest-clips .setting-hint { display: block; margin-top: 2px; }

.ingest-progress { padding: 0 18px 12px; }

.ingest-foot {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  padding: 14px 18px;
  border-top: var(--rule-hair) solid var(--color-rule);
}
/* Whether the form can be sent yet, in words beside the button that sends it.
   A disabled button with nothing next to it says no and not why. */
.ingest-ready { font-size: 11.5px; color: var(--color-neutral-700); }

@media (max-width: 640px) {
  .ingest-grid, .live-grid { grid-template-columns: minmax(0, 1fr); }
}

.ctx-textarea {
  width: 100%;
  margin-top: var(--space-2);
  font-family: var(--font-mono);
  font-size: 11.5px;
  line-height: 1.5;
  resize: vertical;
}

/* The panel sits at the top of the job it belongs to, framed like a card
   inside the card, because it is a mode the job is in rather than a row. */
.reanalyse {
  margin-bottom: var(--space-3);
  padding: var(--space-3);
  background: var(--color-neutral-200);
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-md);
}
.reanalyse .field-label { margin-top: var(--space-3); display: block; }

.ctx-list { display: flex; flex-direction: column; gap: var(--space-1); margin-top: var(--space-1); }
.ctx-row { display: flex; align-items: center; gap: var(--space-3); min-width: 0; }
/* URLs are the model's and the editor's alike: mono, and truncated rather
   than wrapped, because the host and the id are what identify a page. */
.ctx-link {
  font-family: var(--font-mono);
  font-size: 11px;
  min-width: 0;
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-align: left;
}
.ctx-input { flex: 1; font-family: var(--font-mono); font-size: 11.5px; }
.ctx-input.input-bad { border-color: var(--color-accent-2); }
.ctx-muted { font-size: 11px; color: var(--color-neutral-600); font-family: var(--font-mono); }
.ctx-actions { display: flex; align-items: center; gap: var(--space-4); margin-top: var(--space-4); }

/* ── Rides, inside the game popup ─────────────────────────────────────── */

/* The detail grid is key/value; a table of rides needs the whole width, so
   its header and body each span both columns. */
.rides-section { grid-column: 1 / -1; }

/* Numbers in mono, names in the sans: the marks, the placings and the
   timecodes are the model's readings — what the mono face is for — and the
   people are people. Tabular figures keep a column of percentages aligned. */
.rides {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr) max-content max-content max-content minmax(0, 1.2fr);
  column-gap: var(--space-4);
  padding: 0 14px var(--space-3);
  font-size: 12px;
}
.ride { display: contents; }
.ride > span {
  padding: 7px 0;
  border-bottom: var(--rule-hair) solid var(--color-neutral-300);
  min-width: 0;
  align-self: center;
}
.ride:last-child > span { border-bottom: 0; }

.ride-head > span {
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-neutral-600);
}

.ride-num, .ride-total, .ride-place {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
}
.ride-num { color: var(--color-neutral-600); }
.ride-rider { display: block; font-weight: 700; line-height: 1.25; }
.ride-horse {
  display: block;
  font-size: 10.5px;
  color: var(--color-neutral-700);
  font-family: var(--font-mono);
  margin-top: 2px;
}
.ride-test { font-size: 11px; color: var(--color-neutral-700); }

/* Where a total came from, when it was not the screen. */
.ride-tag {
  display: inline-block;
  margin-left: var(--space-2);
  padding: 1px 5px;
  font-family: var(--font-mono);
  font-size: 9px;
  border-radius: var(--radius-sm);
  border: var(--rule-hair) solid var(--color-rule);
  color: var(--color-neutral-700);
  vertical-align: 1px;
}
/* A published figure beside a displayed one, never in its place. */
.ride-published {
  display: block;
  font-size: 10px;
  color: var(--color-neutral-600);
}

/* Failure is not the accent. A total that failed its own check is a warning
   and takes the accent-2 ramp, which reads as an error in both themes rather
   than as a highlight; a confirmation is quiet. */
.ride-check { font-size: 10.5px; line-height: 1.35; color: var(--color-neutral-700); }
.ride-check[data-tone="failed"] { color: var(--color-accent-2); font-weight: 700; }
.ride-check[data-tone="confirmed"] { color: var(--color-neutral-800); }

/* ── A competition day as event → rides → moments ──────────────────────
   The moments card, grouped. The event names what the groups belong to; each
   ride is headed like a row of the ride table — order, rider and horse, the
   result — with its tiles underneath. */
.event-head {
  padding-bottom: var(--space-3);
  margin-bottom: var(--space-3);
  border-bottom: var(--rule-hair) solid var(--color-neutral-300);
}
.event-head-text { min-width: 0; }

.ride-group + .ride-group { margin-top: var(--space-3); }

.ride-group-head {
  display: grid;
  grid-template-columns: max-content minmax(0, 1fr) max-content max-content max-content;
  column-gap: var(--space-4);
  align-items: center;
  padding: var(--space-2) 0;
  margin-bottom: var(--space-3);
  border-bottom: var(--rule-hair) solid var(--color-neutral-300);
  font-size: 12px;
}
.ride-group-head .ride-who { min-width: 0; }

/* Readings — the test, the total, the placing — in the mono, as in the table. */
.ride-group-result {
  font-family: var(--font-mono);
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  color: var(--color-neutral-700);
  text-align: right;
}
.ride-group-result .ride-check { display: block; }

.ride-group-empty {
  font-size: 12px;
  color: var(--color-neutral-600);
  padding-bottom: var(--space-3);
}

/* ── The rides board: who down the left, what across the top ───────────────
   Two axes rather than one column, because a competition day is read both
   ways — which round, and which movement. The rail holds the running order
   and does not move while the pane changes; the filter above the tiles
   crosses every ride at once.

   The tokens here are the design's own: --color-bg is the rail's ground,
   --color-neutral-100 the card, --color-neutral-200 a tile, and
   --color-accent-200 the soft amber wash a selection wears. */
.ride-board {
  display: grid;
  grid-template-columns: minmax(0, 16.6rem) minmax(0, 1fr);
}

/* Forty rides is a scroller, not a taller card: the rail is navigation, and a
   card that grows with the class buries the conversation it sits in. It runs
   the full height of the pane beside it and no further — a rail half the
   height of what it controls reads as a list that ended, and one taller than
   the moments it selects makes the board a different size for every rider.
   The pane decides, because the pane is what is being read.

   `height: 0` with `min-height: 100%` is what makes that true: a grid item
   with a real height takes part in deciding the row's, so a class of forty
   would stretch the board to forty rows and scroll nothing. At zero it takes
   no part, and the minimum then stretches it to whatever the moments came
   to. */
.ride-rail {
  display: flex;
  flex-direction: column;
  height: 0;
  min-height: 100%;
  padding: var(--space-3) var(--space-2) var(--space-4);
  background: var(--color-bg);
  border-right: var(--rule-hair) solid var(--color-rule);
}

.ride-tabs-head {
  flex: 0 0 auto;
  padding: 0 var(--space-2) 9px;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--color-neutral-700);
}

.ride-tabs {
  display: flex;
  flex-direction: column;
  gap: 2px;
  min-height: 0;
  overflow-y: auto;
}

.ride-tab {
  display: flex;
  gap: var(--space-2);
  align-items: baseline;
  width: 100%;
  padding: 6px 9px;
  background: transparent;
  border: var(--rule-hair) solid transparent;
  /* The selected marker is a bar down the inside edge rather than a border
     that appears on all four sides: a box that arrives on selection shifts
     every row below it by its own width. */
  border-left: 2px solid transparent;
  border-radius: 7px;
  text-align: left;
  font-family: var(--font-body);
  color: var(--color-text);
  cursor: pointer;
}
.ride-tab:hover { border-color: var(--color-rule); }
.ride-tab[aria-selected="true"] {
  background: var(--color-accent-200);
  border-color: var(--color-accent-300);
  border-left-color: var(--color-accent);
}
.ride-tab[aria-selected="true"] .ride-rider,
.ride-tab[aria-selected="true"] .list-count { color: var(--color-accent); }
.ride-tab .ride-who { flex: 1; min-width: 0; }
/* One line each: the rail is scanned down, and a name that wraps to two lines
   makes every row below it sit somewhere different from the row above. */
.ride-tab .ride-rider,
.ride-tab .ride-horse {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.ride-tab .ride-rider { font-size: 12.5px; font-weight: 500; }
.ride-tab .ride-horse { font-size: 10.5px; font-variant-numeric: tabular-nums; }

/* The rank sits on the name's line, opposite it, so the rail reads down as
   who and where they stand. A pill because it is a state rather than prose;
   amber when it is known, quiet when it is still to come. */
.ride-name-line { display: flex; align-items: baseline; gap: var(--space-3); min-width: 0; }
.ride-name-line .ride-rider { flex: 1; min-width: 0; }
.ride-rank {
  flex: none;
  padding: 1px 6px;
  border: var(--rule-hair) solid var(--color-accent-300);
  border-radius: var(--radius-sm);
  background: var(--color-accent-100);
  color: var(--color-accent-700);
  font-family: var(--font-mono);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.ride-rank[data-known="false"] {
  border-color: var(--color-neutral-400);
  background: transparent;
  color: var(--color-neutral-700);
}
.ride-found {
  display: block;
  margin-top: 2px;
  font-family: var(--font-mono);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  color: var(--color-neutral-600);
}
.ride-tab[aria-selected="true"] .ride-found { color: var(--color-accent-700); }

.ride-pane { min-width: 0; padding: 14px 20px 22px; }

/* Four across, so a page of twelve is three rows exactly. The shared
   `.tile-row` fills by width instead, which on a wide window fitted five and
   on a narrow one three — and the board's height then changed with the window
   rather than with what was in it, which is what the rail is measured
   against. Under 900px it goes back to filling, because four across a half
   card is four thumbnails nobody can see. */
.ride-pane .tile-row { grid-template-columns: repeat(4, minmax(0, 1fr)); }
@media (max-width: 900px) {
  .ride-pane .tile-row { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
}

/* Who, how it scored, and the way into the whole ride. Wrapping rather than
   three fixed columns: the pane is half a card on a narrow window, and a grid
   there squeezed the rider's name to one word a line and pushed the button
   off the edge. */
.ride-pane-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-4);
  padding-bottom: var(--space-2);
  margin: var(--space-4) 0 var(--space-3);
  border-bottom: var(--rule-hair) solid var(--color-neutral-300);
  font-size: 12px;
}
.ride-pane-head .ride-who { flex: 1 1 11rem; min-width: 0; }
.ride-pane-head .ride-group-result { flex: 0 1 auto; }
.ride-pane-head .play-ride { flex: none; margin-left: auto; }

/* The board is the card's full width, so the list's own padding would inset
   the rail from an edge it is meant to meet. */
.rides-board-card .ride-board { margin: 0 calc(-1 * var(--space-4)) calc(-1 * var(--space-4)); }

/* ── The type axis ────────────────────────────────────────────────────────
   A dropdown, so the second axis stays one control beside the rail rather
   than a row competing with it for the same glance — and the chips beside it
   say what is chosen without it being opened. */
.ride-filter {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
  min-width: 0;
}

/* The pane's two controls: what is shown, and in what order. Side by side
   because they are two halves of one question about this ride, and the sort
   is pushed right so the eye finds the filter first — the filter changes what
   is there, the sort only changes where it is. */
.ride-pane-controls {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  flex-wrap: wrap;
  margin-bottom: var(--space-3);
}
.ride-pane-controls .segmented { flex: none; }
.type-filter { position: relative; }

.type-filter-toggle {
  display: flex;
  align-items: center;
  gap: 9px;
  min-width: 14.4rem;
  padding: 8px 13px;
  background: var(--color-neutral-200);
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-lg);
  text-align: left;
  font-family: var(--font-body);
  font-size: 12.5px;
  color: var(--color-text);
  cursor: pointer;
}
.type-filter-toggle:hover,
.type-filter-toggle[aria-expanded="true"] { border-color: var(--color-accent); }
.type-filter-label { flex: 1; }
.type-filter-caret { font-family: var(--font-mono); font-size: 10px; color: var(--color-neutral-700); }

.type-menu {
  position: absolute;
  z-index: 20;
  top: calc(100% + 6px);
  left: 0;
  width: 17.75rem;
  background: var(--color-neutral-200);
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  overflow: hidden;
}
.type-menu-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 9px 12px;
  border-bottom: var(--rule-hair) solid var(--color-rule);
}
.type-menu-title {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-neutral-700);
}
.type-menu-list { max-height: 16.5rem; overflow-y: auto; padding: 5px; }

.type-opt {
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 9px;
  background: transparent;
  border: 0;
  border-radius: 7px;
  text-align: left;
  font-family: var(--font-body);
  font-size: 12.5px;
  color: var(--color-text);
  cursor: pointer;
}
.type-opt:hover { background: var(--color-neutral-100); }
.type-opt[aria-pressed="true"] { background: var(--color-accent-200); }
.type-opt[aria-pressed="true"] .type-opt-name { color: var(--color-accent); }
.type-opt-name { flex: 1; min-width: 0; }
.type-opt .list-count { flex: none; font-variant-numeric: tabular-nums; }
.type-opt-box {
  flex: none;
  width: 14px;
  height: 14px;
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: 4px;
}
/* The tick is ink on amber, never white: white on this amber is about 1.9:1. */
.type-opt[aria-pressed="true"] .type-opt-box {
  background: var(--color-accent);
  border-color: var(--color-accent);
  box-shadow: inset 0 0 0 2px var(--color-accent), inset 3px -3px 0 -1px var(--color-on-accent);
}

.type-chip {
  display: flex;
  align-items: center;
  gap: 7px;
  padding: 5px 11px;
  background: var(--color-accent-200);
  border: var(--rule-hair) solid var(--color-accent-300);
  border-radius: 20px;
  font-family: var(--font-body);
  font-size: 11.5px;
  color: var(--color-accent);
  cursor: pointer;
}
.type-chip:hover { border-color: var(--color-accent); }
.type-chip-x { font-size: 12px; line-height: 1; }

/* ── Two controls the board introduces ────────────────────────────────────
   The sort as a segmented pair rather than two links: they are one choice
   with two states, and a pair of links reads as two separate actions. */
.segmented {
  display: flex;
  gap: 2px;
  padding: 2px;
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-lg);
}
.seg-btn {
  padding: 5px 11px;
  background: transparent;
  border: 0;
  border-radius: 6px;
  font-family: var(--font-body);
  font-size: 12px;
  color: var(--color-neutral-700);
  cursor: pointer;
}
.seg-btn:hover { color: var(--color-text); }
/* Which one is on has to survive a glance. A neutral-200 fill against a
   neutral ground was a shade apart from the unselected pair, so the control
   read as two labels rather than as a choice already made. This is the chip's
   own selected language — accent edge, accent tint, accent text — reused
   rather than a fourth way of saying the same thing. */
.seg-btn[aria-pressed="true"] {
  background: var(--color-accent-100);
  box-shadow: inset 0 0 0 var(--rule-hair) var(--color-accent);
  color: var(--color-accent-900);
  font-weight: 500;
}

/* Inside a ride, the frame is the thing being judged, so it gets the design's
   16:9 rather than the 56px strip the flat lists use. */
.ride-pane .tile .thumb { height: auto; aspect-ratio: 16 / 9; }

/* The clock on the 16:9 frame takes its own dark ground, as the play button
   does: a still from an arena is not a flat colour, and bare text laid on it is
   legible against a dark crowd and gone against a bright floor. */
.ride-pane .thumb-clock {
  left: 8px;
  bottom: 7px;
  padding: 2px 6px;
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.62);
  font-size: 10.5px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}

/* Narrow, the rail goes above the pane rather than beside it, and shortens:
   a full-height list of rides with the moments below the fold is the single
   column this replaced. */
@media (max-width: 760px) {
  .ride-board { grid-template-columns: minmax(0, 1fr); }
  .ride-rail {
    max-height: 14rem;
    border-right: 0;
    border-bottom: var(--rule-hair) solid var(--color-rule);
  }
}

.not-confirmed { line-height: 1.45; }
.not-confirmed + .not-confirmed { margin-top: var(--space-2); }

/* The popup's two columns share the card's corners rather than having their
   own, so the player rounds on the left only. */
.detail-player { border-radius: var(--radius-lg) 0 0 var(--radius-lg); }
@media (max-width: 760px) {
  .detail-player { border-radius: var(--radius-lg) var(--radius-lg) 0 0; }
}

/* The rounding rule above clips children to the corners with overflow:hidden,
   and that shorthand also resets the overflow-y:auto this card was given when
   it got its max-height — so a record taller than 84vh was cut off with no
   scrollbar and no way to reach the rest. Measured on the game popup: 848px of
   content in a 597px box, 251px of it unreachable.

   Both axes have to stay non-visible or the corners stop clipping, so this
   clips sideways and scrolls down. It has to come after the shared rule to win,
   which is the whole reason the regression was silent. */
.modal-card {
  overflow-x: hidden;
  overflow-y: auto;
}

.chip,
.link-btn,
.file-label,
.session,
.thumb,
.dz-thumb,
.composer-input,
.header-select {
  border-radius: var(--radius-md);
}

.meter,
.meter i,
.stage-meter,
.stage-meter i {
  border-radius: var(--radius-sm);
}

/* The send button is flush against the input's edge, so it keeps the corners
   on that side square whatever the theme does. */
.composer-send { border-radius: 0 var(--radius-md) var(--radius-md) 0; }


/* The game record a finished live event shows in its job row. */
.live-error { margin-bottom: 4px; }
.live-game { margin-top: 10px; padding-top: 10px; border-top: var(--rule-hair) solid var(--color-rule); }


/* The scope card: three doors at the start of a session, then a one-line
   summary of what was chosen. */
/* ── The opener ───────────────────────────────────────────────────────────
   What a session can be about. Three rows, each an icon against the card's
   own ground, then what it is and the ways in. The amber edge is the design's
   own: it marks these as the things this desk does rather than as one more
   card in the transcript. */
.opener { display: flex; flex-direction: column; gap: 12px; margin-top: 16px; }

/* The way back from whatever the opener swapped in. Above the card rather
   than inside it, because all three of them can be swapped in and only one
   had a header to hang it on. */
.opener-back { margin-bottom: 10px; }

/* ── Renaming a match where its name is read ──────────────────────────────
   One name belongs to a match and it is shown in three places, so it is
   editable in all three rather than behind a settings page nobody would look
   for. The pencil stays out of the way until the row is hovered: a title is
   read far more often than it is changed, and a control beside every name
   competes with the names. */
.is-titled { display: flex; align-items: baseline; gap: var(--space-2); min-width: 0; }
.titled-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rename-btn { flex: none; opacity: 0; transition: opacity 90ms linear; }
.is-titled:hover .rename-btn,
.rename-btn:focus-visible { opacity: 1; }

.is-renaming { display: flex; align-items: center; gap: var(--space-2); min-width: 0; }
.rename-input {
  flex: 1;
  min-width: 0;
  font-family: var(--font-body);
  font-size: 12.5px;
  font-weight: 500;
}
.is-renaming .btn-primary { flex: none; }

/* Touch has no hover, so the control cannot hide behind one. */
@media (hover: none) {
  .rename-btn { opacity: 1; }
}

.opener-option {
  display: flex;
  align-items: stretch;
  overflow: hidden;
  background: var(--color-neutral-200);
  border: var(--rule-hair) solid var(--color-rule);
  border-left: 4px solid var(--color-accent);
  border-radius: 10px;
}

.opener-icon {
  flex: none;
  width: 76px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-accent);
}

.opener-text { flex: 1; min-width: 0; padding: 15px 16px; }
.opener-title {
  font-size: 13.5px;
  font-weight: 500;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.opener-desc {
  font-size: 12.5px;
  color: var(--color-neutral-700);
  line-height: 1.5;
  margin-top: 4px;
}
.opener-links { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 13px; }

/* The ways in are the informational blue, not the accent: on a card already
   edged in amber, amber buttons would be the third amber thing in one box and
   none of them would read as the action. */
.opener-link {
  padding: 6px 13px;
  background: transparent;
  border: var(--rule-hair) solid var(--color-info-300);
  border-radius: 7px;
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 500;
  color: var(--color-info);
  cursor: pointer;
}
.opener-link:hover {
  border-color: var(--color-info);
  background: var(--color-info-100);
}

/* Narrow, the icon column is a row's worth of width for one glyph. */
@media (max-width: 560px) {
  .opener-icon { width: 52px; }
  .opener-text { padding: 13px 13px 15px; }
}

.scope-options { display: grid; gap: 8px; margin-top: 10px; }
.scope-option {
  display: flex; flex-direction: column; gap: 3px; text-align: left;
  padding: 10px 12px; cursor: pointer; font: inherit; color: var(--color-text);
  background: var(--color-bg); border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-input, 5px);
}
.scope-option:hover { border-color: var(--color-accent); background: var(--color-accent-100); }
.scope-option-title { font-weight: 600; }
.scope-option-desc { font-size: 11.5px; color: var(--color-neutral-800); line-height: 1.4; }
.scope-panel { margin-top: 10px; }
.scope-summary { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 6px; }
.scope-name { font-weight: 600; }

/* ── Rides loading ─────────────────────────────────────────────────────────
   The wait for an event's rides, said plainly and with something moving: a
   ring beside the words, over an outline of the board it is about to become.
   The outline shimmers so it reads as "on its way", not as an empty board. */
.rides-loading {
  display: grid;
  gap: var(--space-4);
  padding: var(--space-4);
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-lg);
  background: var(--color-neutral-100);
}
.rides-loading-head { display: flex; align-items: center; gap: var(--space-4); }
.rides-loading-title { font-size: 15px; font-weight: 600; }
.rides-loading-hint { margin-top: 2px; font-size: 12px; color: var(--color-neutral-700); }

.loading-ring {
  flex: none;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 3px solid var(--color-neutral-400);
  border-top-color: var(--color-accent);
  animation: loading-spin 0.8s linear infinite;
}
@keyframes loading-spin { to { transform: rotate(360deg); } }

.rides-loading-board {
  display: grid;
  grid-template-columns: minmax(0, 13rem) minmax(0, 1fr);
  gap: var(--space-4);
}
.skel-rail { display: grid; gap: var(--space-3); align-content: start; }
.skel-row { display: grid; gap: 5px; }
.skel-pane {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: var(--space-4);
  align-content: start;
}
.skel-tile { display: grid; gap: 6px; }

.skel {
  display: block;
  height: 10px;
  border-radius: var(--radius-sm);
  background: linear-gradient(90deg,
    var(--color-neutral-200) 0%, var(--color-neutral-300) 50%, var(--color-neutral-200) 100%);
  background-size: 200% 100%;
  animation: skel-shimmer 1.4s ease-in-out infinite;
}
.skel-name { width: 70%; height: 12px; }
.skel-line { width: 90%; }
.skel-short { width: 55%; }
.skel-frame { height: auto; aspect-ratio: 16 / 9; border-radius: var(--radius-md); }
@keyframes skel-shimmer { from { background-position: 100% 0; } to { background-position: -100% 0; } }

@media (prefers-reduced-motion: reduce) {
  .loading-ring, .skel { animation: none; }
}
@media (max-width: 720px) {
  .rides-loading-board { grid-template-columns: 1fr; }
}

/* The same answer over the box the next question is typed into. One line,
   quiet: it qualifies the question rather than competing with it.

   Aligned to the box below it, not to the card: the composer keeps a 48px
   gutter and this had 2px of its own, so the label started forty-odd pixels to
   the left of the box it is about and read as a stray line rather than as a
   label on it. The gutter is the composer's own at every width — two numbers
   that have to match, and they are set together here and in `.composer-inner`
   because nothing else can keep them in step. */
.composer-context {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  /* Five above, six below: the line needs air off the transcript's last card
     more than it needs it off the box, which it belongs to. */
  padding: 5px 48px 6px;
  min-width: 0;
}
@media (max-width: 900px) {
  .composer-context { padding-left: 20px; padding-right: 20px; }
}
.composer-context-label {
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-neutral-600);
}
.composer-context-title {
  font-size: 12px;
  font-weight: 600;
  color: var(--color-accent-700);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* ── Early preview ─────────────────────────────────────────────────────────
   A flag on the frame and a notice under the player. The flag takes its own
   dark ground, as the clock and the play button do — a still from an arena is
   not a flat colour. It is a state, not a warning: amber, not the alert ramp,
   which in this system means something went wrong. */
.thumb-flag {
  position: absolute;
  top: 5px;
  right: 6px;
  z-index: 1;
  max-width: calc(100% - 12px);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  padding: 2px 6px;
  border-radius: 4px;
  background: rgba(0, 0, 0, 0.62);
  color: var(--color-accent);
  font-family: var(--font-mono);
  font-size: 9px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

/* Under the player, where the clip it qualifies has just been watched. */
.preview-notice {
  display: grid;
  gap: 6px;
  margin: 14px 16px 0;
  padding: 10px 12px;
  border: var(--rule-hair) solid var(--color-accent-300);
  border-left: 2px solid var(--color-accent);
  border-radius: var(--radius-md);
  background: var(--color-accent-100);
}
.preview-notice-tag {
  font-family: var(--font-mono);
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--color-accent-700);
}
.preview-notice p {
  margin: 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--color-text);
  max-width: 68ch;
}

/* ── Buttons, on the brand's own terms ─────────────────────────────────────
   One shape and one scale, three weights of emphasis. The moment player used
   three unrelated idioms side by side — an uppercase outline, a sentence-case
   amber outline and a bare text link — at radii (6px, none) the system does
   not have. Arenos gives buttons the 5px step, one hairline, Geist at 500,
   sentence case, and keeps amber scarce: one filled action, the rest quiet.
   Ink on amber, never white. */
.btn-primary,
.btn-quiet,
.btn-ghost {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 6px 13px;
  border-radius: var(--radius-md);
  border: var(--rule-hair) solid transparent;
  background: transparent;
  font-family: var(--font-body);
  font-size: 12px;
  font-weight: 500;
  line-height: 1.4;
  white-space: nowrap;
  cursor: pointer;
}
.btn-primary {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-on-accent);
}
.btn-primary:hover:not(:disabled) { background: var(--color-accent-600); border-color: var(--color-accent-600); }
.btn-primary[aria-pressed="true"] { background: var(--color-accent-700); border-color: var(--color-accent-700); }

.btn-quiet {
  border-color: var(--color-rule);
  color: var(--color-text);
}
.btn-quiet:hover:not(:disabled) { border-color: var(--color-accent); color: var(--color-accent-700); }

.btn-ghost { color: var(--color-neutral-700); }

/* The player's three: Download, Publish and Close. They act on the moment on
   screen and are one kind of thing, so they are one button in three weights
   rather than three idioms — and all three carry the amber, because that is
   what they have in common. The fill still belongs to Publish alone: an amber
   rectangle three times over in one row is emphasis that has stopped meaning
   anything. */
.btn-quiet.detail-action {
  border-color: var(--color-accent-300);
  color: var(--color-accent-900);
}
.btn-quiet.detail-action:hover:not(:disabled) {
  border-color: var(--color-accent);
  background: var(--color-accent-100);
  color: var(--color-accent-900);
}
.btn-ghost:hover:not(:disabled) { color: var(--color-text); }

.btn-primary:disabled,
.btn-quiet:disabled,
.btn-ghost:disabled { opacity: 0.4; cursor: default; }

/* The pager's two steps: the same quiet button, smaller, because they sit
   under a list rather than in a head. */
.btn-step { padding: 4px 10px; font-size: 11.5px; }

/* The one thing a card or a card-sized panel exists to do — start an
   analysis, schedule a live event, open the record. Larger than a button in a
   row of buttons, and the only one of its size on the surface. */
.btn-lg { padding: 9px 18px; font-size: 12.5px; }

/* Play sits in the transport row, so it takes that row's height and radius
   rather than the system's default — the fill is what marks it out, not the
   size. */
.player-play {
  min-width: 68px;
  height: 30px;
  padding: 0 12px;
  border-radius: var(--radius-sm);
  font-size: 11px;
}

/* The ride pane's own play, with the round's length set beside the label. */
.play-ride { gap: 7px; }


/* ── Picking moments, and the reel they become ────────────────────────────
   The pick control is a sibling of the frame rather than a child: the frame
   is a button and a button cannot hold another one. It sits opposite the
   early-preview mark, over the corner of the picture where nothing is ever
   the subject. */
.tile-pick {
  position: absolute;
  top: 6px;
  left: 6px;
  z-index: 3;
  width: 20px;
  height: 20px;
  display: grid;
  place-items: center;
  padding: 0;
  border-radius: var(--radius-sm);
  /* The same two literals the play button on this same frame uses, and for
     the same reason: a thumbnail is dark under either theme, so a control on
     it takes the video's ground rather than the page's. The first version
     used --color-neutral-500 here, which is a theme grey over a photograph —
     it read as a smudge on the picture rather than as a control. */
  background: rgba(0, 0, 0, 0.45);
  border: 1.5px solid rgba(255, 255, 255, 0.85);
  cursor: pointer;
  transition: background 120ms ease, border-color 120ms ease;
}
/* A drawn tick, not a filled box. It is invisible until the tile is hovered
   or picked: an empty box on every tile is a page of empty boxes, and the
   ring alone is enough to say "there is something to press here". */
.tile-pick > span {
  width: 9px;
  height: 5px;
  margin-top: -2px;
  border-left: 2px solid #fff;
  border-bottom: 2px solid #fff;
  transform: rotate(-45deg);
  opacity: 0;
  transition: opacity 120ms ease;
}
.tile:hover .tile-pick > span,
.tile-pick:focus-visible > span { opacity: 0.55; }
.tile-pick[aria-pressed="true"] {
  background: var(--color-accent);
  border-color: var(--color-accent);
}
.tile-pick[aria-pressed="true"] > span {
  opacity: 1;
  /* Ink on amber, never white. */
  border-color: var(--color-on-accent);
}
@media (prefers-reduced-motion: reduce) {
  .tile-pick, .tile-pick > span { transition: none; }
}
.tile[aria-current="true"] { border-color: var(--color-accent); }

/* The bar under the tiles. Present at zero, because a control that appears
   only once you have guessed the gesture that summons it is a control nobody
   finds — but quiet until there is something to act on. */
.reel-bar {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-3);
  padding-top: var(--space-3);
  border-top: var(--rule-hair) solid var(--color-rule);
}
.reel-bar-count {
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--color-neutral-700);
}
/* The Build button beside it is already the amber on this bar; a second one
   two words away is amber competing with itself. */
.reel-bar[data-picked="true"] .reel-bar-count {
  color: var(--color-text);
  font-weight: 500;
}
.reel-bar-actions { display: flex; gap: var(--space-2); margin-left: auto; }

/* ── The editor ───────────────────────────────────────────────────────────
   The whole viewport, like the moment player: a reel is judged by watching it,
   and every pixel given back to the page behind is a pixel off the picture. */
/* The same arrangement #details uses, and for the same reason: the backdrop
   margin is pixels taken off the picture. The padding has to be cleared on the
   modal itself, not just the card — the card cannot fill a box that is inset. */
#reel { padding: 0; align-items: stretch; }

.modal-card-full {
  width: 100%;
  max-width: none;
  height: 100dvh;
  max-height: 100dvh;
  display: flex;
  flex-direction: column;
  border: 0;
  border-radius: 0;
  overflow: hidden;
}
.reel-split {
  flex: 1;
  display: grid;
  grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr);
  /* Both the split and each column, or a grid child refuses to shrink below
     its content and the dialog quietly grows past the viewport. */
  min-height: 0;
}
.reel-stage, .reel-cuts { min-height: 0; overflow-y: auto; }
.reel-cuts { border-left: var(--rule-hair) solid var(--color-rule); padding: var(--space-3); }
/* The trim strip, the reel's name and the shape panel all sit in here, and
   all of them were flush against the window edge: this padding named
   --space-5, which the scale does not have (3, 5, 8, 13, 21, 34 are
   --space-1,2,3,4,6,8), and one invalid value voids the whole declaration. */
.reel-under { padding: var(--space-4) var(--space-6) var(--space-6); }

.reel-meta { display: flex; flex-direction: column; gap: 6px; max-width: 520px; }
.reel-meta-facts {
  display: flex;
  gap: var(--space-3);
  font-family: var(--font-mono);
  font-size: 11px;
  color: var(--color-neutral-700);
}
.reel-len { color: var(--color-neutral-800); }

.reel-cuts-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-bottom: var(--space-3);
}

/* One cut: what it is, where it starts and ends, and where it sits in the
   order. The three rows are deliberate — a cut is read, then trimmed, then
   moved, and putting all of it on one line made none of it findable. */
.reel-cut {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: var(--space-3);
  margin-bottom: var(--space-2);
  background: var(--color-neutral-200);
  border: var(--rule-hair) solid var(--color-rule);
  border-left: 2px solid transparent;
  border-radius: var(--radius-md);
}
.reel-cut[aria-current="true"] { border-left-color: var(--color-accent); }
.reel-cut-top { display: flex; align-items: center; gap: var(--space-2); }
.reel-cut-name {
  flex: 1;
  min-width: 0;
  font-size: 12.5px;
  font-weight: 500;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* A timecode is a model-produced reading, so it is set in the mono. */
.reel-cut-len, .reel-edge-at {
  font-family: var(--font-mono);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--color-neutral-700);
}
.reel-cut-play {
  flex: none;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  padding: 0;
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-sm);
  background: transparent;
  cursor: pointer;
}
.reel-cut-play:hover { border-color: var(--color-accent); }
.reel-cut-play > span {
  width: 0;
  height: 0;
  margin-left: 2px;
  border-left: 7px solid var(--color-text);
  border-top: 4.5px solid transparent;
  border-bottom: 4.5px solid transparent;
}
.reel-cut-edges { display: flex; flex-wrap: wrap; gap: var(--space-3); }
.reel-edge { display: inline-flex; align-items: center; gap: 4px; }
.reel-edge-label {
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-neutral-600);
}
.reel-cut-move { display: flex; gap: var(--space-2); }
.reel-cut-move .btn-step { min-width: 0; }

.reel-render-state {
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-neutral-700);
  margin-right: var(--space-2);
}
/* Ready is the one state worth the accent here: it is the difference between
   a reel you can cut shapes from and one you cannot. */
.reel-render-state[data-state="ready"] { color: var(--color-accent-700); }

@media (max-width: 900px) {
  .reel-split { grid-template-columns: 1fr; }
  .reel-cuts { border-left: 0; border-top: var(--rule-hair) solid var(--color-rule); }
}


/* ── Trimming a moment, before it goes in a reel ──────────────────────────
   A filmstrip of the moment's own window with the chosen range drawn on it.
   Every value here is a token: the strip is one of the few places on the desk
   where amber is load-bearing rather than decorative — the selection and its
   two handles are the control — so nothing else in the panel takes it. */
.trim-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3) 0 var(--space-4);
}
.trim-card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
}
.trim-hint { font-size: 11.5px; color: var(--color-neutral-600); }

.trim-strip {
  position: relative;
  display: flex;
  gap: 2px;
  height: 64px;
  border-radius: var(--radius-md);
  /* A container that rounds also clips, or the selection's square corners
     draw over the corner they are meant to sit inside. */
  overflow: hidden;
  background: var(--color-neutral-900);
  touch-action: none;   /* the handles are dragged, not scrolled */
}
.trim-frame {
  flex: 1;
  min-width: 0;
  background-color: var(--color-neutral-800);
  background-size: cover;
  background-position: center;
  /* The strip is one frame repeated, not nine real ones — the stills are cut
     at the peak and there is only ever the one. Dimming alternate frames is
     what makes it read as a strip of film rather than as a smear. */
  opacity: 0.85;
}
.trim-frame:nth-child(odd) { opacity: 0.7; }

.trim-selection {
  position: absolute;
  top: 0;
  bottom: 0;
  border: var(--rule) solid var(--color-accent);
  background: color-mix(in srgb, var(--color-accent) 18%, transparent);
  box-sizing: border-box;
  pointer-events: none;
}
.trim-handle {
  position: absolute;
  top: -1px;
  bottom: -1px;
  width: 14px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  background: var(--color-accent);
  cursor: ew-resize;
  pointer-events: auto;
}
.trim-handle-in { left: -7px; }
.trim-handle-out { right: -7px; }
.trim-handle .grip {
  width: 2px;
  height: 16px;
  /* Ink on amber, never white. */
  background: var(--color-on-accent);
  box-shadow: 3px 0 var(--color-on-accent), -3px 0 var(--color-on-accent);
}
.trim-handle:focus-visible { outline: 2px solid var(--color-accent-700); outline-offset: 1px; }

/* Timecodes are a reading, so they are set in the mono. */
.trim-ruler {
  display: flex;
  justify-content: space-between;
  font-family: var(--font-mono);
  font-size: 10px;
  font-variant-numeric: tabular-nums;
  color: var(--color-neutral-600);
}

/* The two edges first, then what they add up to, then the way back. On one
   row while it fits and stacked when it does not — the readings are the point
   and a wrapped timecode reads as two numbers. */
.trim-readout {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-4);
  margin-top: var(--space-2);
}
.trim-edge-box {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  padding: 4px 8px;
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-md);
  background: var(--color-neutral-200);
}
.trim-at {
  font-family: var(--font-mono);
  font-size: 11.5px;
  font-variant-numeric: tabular-nums;
  color: var(--color-text);
  min-width: 68px;
  text-align: center;
}
/* The two readings as a pair, label over value, so the numbers line up under
   each other and can be compared at a glance — which is the whole reason both
   are shown. They were on one line at different widths, so they could not be.
   Both values take the same colour: they are the same kind of reading, and
   colouring one of them amber said "changed" in a second, quieter voice than
   the Reset link and the row's own mark already say it. */
.trim-ranges {
  display: inline-grid;
  grid-auto-flow: column;
  gap: 0 var(--space-4);
  white-space: nowrap;
}
.trim-ranges > span { display: grid; gap: 2px; }
.trim-ranges .k {
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-neutral-600);
}
.trim-ranges b {
  font-family: var(--font-mono);
  font-size: 11.5px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--color-neutral-800);
}
.trim-reset { margin-left: auto; }

/* The strip's head carries the nudge step, since that is the fine end of the
   same control the handles are the coarse end of. */
.trim-card-head-right { display: inline-flex; align-items: center; gap: var(--space-3); }
/* A reading, not an action: the mono says it is measured, and the colour is
   the same as every other measured thing on the panel. */
.reel-cuts-total {
  font-family: var(--font-mono);
  font-size: 11px;
  font-variant-numeric: tabular-nums;
  color: var(--color-neutral-800);
}
/* A row says it was trimmed; the strip above says by how much. */
.reel-edge-trimmed {
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--color-neutral-700);
}

/* ── Cutting the reel to another shape ────────────────────────────────────
   The guide is the mockup's idea — the target's window drawn on the source
   frame with its aspect tagged — with one change: it slides. A centre crop of
   a wide arena shot frames an empty half as often as the play. */
.crop-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4) 0 var(--space-3);
  border-top: var(--rule-hair) solid var(--color-rule);
  margin-top: var(--space-3);
}
.crop-source {
  position: relative;
  aspect-ratio: 16 / 9;
  max-width: 320px;
  border-radius: var(--radius-md);
  overflow: hidden;
  /* The video's own ground, which does not follow the page: a frame is a
     picture and a picture sits on black under either theme. */
  background: var(--color-neutral-900);
  cursor: ew-resize;
  touch-action: none;
}
.crop-frame {
  position: absolute;
  inset: 0;
  background:
    repeating-linear-gradient(90deg,
      transparent 0 22px,
      color-mix(in srgb, var(--color-neutral-700) 40%, transparent) 22px 23px);
}
.crop-guide {
  position: absolute;
  top: 0;
  bottom: 0;
  border: var(--rule) solid var(--color-accent);
  background: color-mix(in srgb, var(--color-accent) 16%, transparent);
  box-sizing: border-box;
}
.crop-tag {
  position: absolute;
  top: 5px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--color-accent);
  /* Ink on amber, never white. */
  color: var(--color-on-accent);
  font-family: var(--font-mono);
  font-size: 9px;
  padding: 2px 6px;
  border-radius: var(--radius-sm);
  white-space: nowrap;
}
.crop-hint { font-size: 11px; color: var(--color-neutral-600); }
.crop-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.crop-actions .btn-step { margin-left: auto; }
.crop-done { display: flex; gap: var(--space-2); }
.crop-chip {
  font-family: var(--font-mono);
  font-size: 10px;
  padding: 2px 7px;
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-sm);
  color: var(--color-neutral-700);
}

/* ── While the reel is busy ───────────────────────────────────────────────
   A render is minutes of someone else's encoder and a publish is an upload.
   Both leave the editor looking idle while the thing under it is not, and an
   edit made in that window either applies to a render already superseded or
   is thrown away when the reply lands. So the surface says so, in a band that
   cannot be missed, and everything it would be pointless to press goes flat. */
.reel-busy {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-6);
  background: var(--color-accent-100);
  border-bottom: var(--rule-hair) solid var(--color-accent-300);
  font-size: 12px;
  color: var(--color-accent-900);
}
.reel-busy-spin {
  flex: none;
  width: 13px;
  height: 13px;
  border: 2px solid var(--color-accent-300);
  border-top-color: var(--color-accent);
  border-radius: 50%;
  animation: loading-spin 700ms linear infinite;
}
@media (prefers-reduced-motion: reduce) { .reel-busy-spin { animation: none; } }

/* Disabled is the state that carries this, so it needs to read as disabled
   rather than merely as unclickable — the default 0.4 on a seg-btn or a
   link-btn is too easy to mistake for "quiet". */
#reel[data-busy]:not([data-busy=""]) .seg-btn:disabled,
#reel[data-busy]:not([data-busy=""]) .link-btn:disabled,
#reel[data-busy]:not([data-busy=""]) .btn-ghost:disabled,
#reel[data-busy]:not([data-busy=""]) .btn-quiet:disabled,
#reel[data-busy]:not([data-busy=""]) .btn-primary:disabled {
  opacity: 0.3;
  cursor: default;
}
/* The two dragged controls have no disabled attribute to carry it, so the
   whole panel is dimmed and made inert instead. */
#reel[data-busy]:not([data-busy=""]) .trim-strip,
#reel[data-busy]:not([data-busy=""]) .crop-source {
  opacity: 0.4;
  pointer-events: none;
}

/* What is going up, above the fields, in the same voice as the trim readings. */
.reel-publish-what {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}
.reel-publish-what .k {
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-neutral-600);
}
.reel-publish-what b {
  font-family: var(--font-mono);
  font-size: 11.5px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--color-neutral-800);
}

/* ── The reels on the desk ────────────────────────────────────────────────
   A reel uses the moments' own tile: same grid, same frame, same play button,
   same footer of two facts. They are the same kind of object here — a thing
   you watch and then do something with — and a second card shape for the
   second one would say they are not.

   The frame is the one difference. A moment has one still and a reel is
   several moments, so it shows the first three side by side: that reads as a
   reel without a label saying so. */
.reel-strip {
  position: absolute;
  inset: 0;
  display: flex;
  gap: 1px;
}
.reel-strip-cell {
  flex: 1;
  min-width: 0;
  position: relative;
  overflow: hidden;
  background: var(--color-neutral-900);
}
.reel-strip-cell img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* The strip is one picture cut in three, not three pictures: dimming the
   outer cells lets the first cut read as the subject. */
.reel-strip-cell:not(:first-child) { opacity: 0.75; }

.reel-tile .thumb { position: relative; }
.reel-tile-shapes { display: flex; flex-wrap: wrap; gap: 4px; }
.reel-tile-none {
  font-family: var(--font-mono);
  font-size: 10px;
  color: var(--color-neutral-600);
}
/* Ready is the one state worth the accent: it is the difference between a
   reel you can cut shapes from or publish, and one you cannot. */
.reel-tile .tile-kind[data-state="ready"] { color: var(--color-accent-700); }

/* ── Cutting the reel to another shape ────────────────────────────────────
   The guide is the mockup's idea — the target's window drawn on the source
   frame with its aspect tagged — with one change: it slides. A centre crop of
   a wide arena shot frames an empty half as often as the play. */
.crop-card {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-4) 0 var(--space-3);
  border-top: var(--rule-hair) solid var(--color-rule);
  margin-top: var(--space-3);
}
.crop-source {
  position: relative;
  aspect-ratio: 16 / 9;
  max-width: 320px;
  border-radius: var(--radius-md);
  overflow: hidden;
  /* The video's own ground, which does not follow the page: a frame is a
     picture and a picture sits on black under either theme. */
  background: var(--color-neutral-900);
  cursor: ew-resize;
  touch-action: none;
}
.crop-frame {
  position: absolute;
  inset: 0;
  background:
    repeating-linear-gradient(90deg,
      transparent 0 22px,
      color-mix(in srgb, var(--color-neutral-700) 40%, transparent) 22px 23px);
}
.crop-guide {
  position: absolute;
  top: 0;
  bottom: 0;
  border: var(--rule) solid var(--color-accent);
  background: color-mix(in srgb, var(--color-accent) 16%, transparent);
  box-sizing: border-box;
}
.crop-tag {
  position: absolute;
  top: 5px;
  left: 50%;
  transform: translateX(-50%);
  background: var(--color-accent);
  /* Ink on amber, never white. */
  color: var(--color-on-accent);
  font-family: var(--font-mono);
  font-size: 9px;
  padding: 2px 6px;
  border-radius: var(--radius-sm);
  white-space: nowrap;
}
.crop-hint { font-size: 11px; color: var(--color-neutral-600); }
.crop-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-wrap: wrap;
}
.crop-actions .btn-step { margin-left: auto; }
.crop-done { display: flex; gap: var(--space-2); }
.crop-chip {
  font-family: var(--font-mono);
  font-size: 10px;
  padding: 2px 7px;
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-sm);
  color: var(--color-neutral-700);
}

/* ── While the reel is busy ───────────────────────────────────────────────
   A render is minutes of someone else's encoder and a publish is an upload.
   Both leave the editor looking idle while the thing under it is not, and an
   edit made in that window either applies to a render already superseded or
   is thrown away when the reply lands. So the surface says so, in a band that
   cannot be missed, and everything it would be pointless to press goes flat. */
.reel-busy {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-3) var(--space-6);
  background: var(--color-accent-100);
  border-bottom: var(--rule-hair) solid var(--color-accent-300);
  font-size: 12px;
  color: var(--color-accent-900);
}
.reel-busy-spin {
  flex: none;
  width: 13px;
  height: 13px;
  border: 2px solid var(--color-accent-300);
  border-top-color: var(--color-accent);
  border-radius: 50%;
  animation: loading-spin 700ms linear infinite;
}
@media (prefers-reduced-motion: reduce) { .reel-busy-spin { animation: none; } }

/* Disabled is the state that carries this, so it needs to read as disabled
   rather than merely as unclickable — the default 0.4 on a seg-btn or a
   link-btn is too easy to mistake for "quiet". */
#reel[data-busy]:not([data-busy=""]) .seg-btn:disabled,
#reel[data-busy]:not([data-busy=""]) .link-btn:disabled,
#reel[data-busy]:not([data-busy=""]) .btn-ghost:disabled,
#reel[data-busy]:not([data-busy=""]) .btn-quiet:disabled,
#reel[data-busy]:not([data-busy=""]) .btn-primary:disabled {
  opacity: 0.3;
  cursor: default;
}
/* The two dragged controls have no disabled attribute to carry it, so the
   whole panel is dimmed and made inert instead. */
#reel[data-busy]:not([data-busy=""]) .trim-strip,
#reel[data-busy]:not([data-busy=""]) .crop-source {
  opacity: 0.4;
  pointer-events: none;
}

/* What is going up, above the fields, in the same voice as the trim readings. */
.reel-publish-what {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}
.reel-publish-what .k {
  font-family: var(--font-mono);
  font-size: 9.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-neutral-600);
}
.reel-publish-what b {
  font-family: var(--font-mono);
  font-size: 11.5px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  color: var(--color-neutral-800);
}

/* ── The copy a reel goes out with ────────────────────────────────────────
   Written as the panel opens, and said to be written: a composed description
   and a model's read differently, and only one of them is worth editing
   before it leaves the desk. */
.reel-copy-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-bottom: var(--space-2);
}
.reel-copy-note {
  font-size: 11px;
  line-height: 1.4;
  color: var(--color-neutral-600);
}
/* Hashtags echoed back under the field they were typed in. The field is the
   truth; these are there so a run-together line can be checked at a glance. */
.hashtag-row { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 5px; }
.hashtag-chip {
  font-family: var(--font-mono);
  font-size: 10px;
  padding: 2px 7px;
  border: var(--rule-hair) solid var(--color-rule);
  border-radius: var(--radius-sm);
  color: var(--color-neutral-700);
}
