/* demo.css — page-specific styles for the switch-player demo page.
   Loaded AFTER product-theme.css; uses its tokens, never overrides them.

   Radius language follows the theme: 4px cards, 6–9px boxes/inputs,
   999px interactive pills. Motion is one-shot only (see theme rule 5) —
   nothing on this page loops.

   ── The one deliberate exception to the theme's status-colour rule ──────
   product-theme.css rule 6 bans green/red/yellow as status colours, and
   every status surface on this page obeys it: .status-msg, .security-badge,
   .copy-btn, the log and the API panel all stay on the two-hue
   --accent-orange / --accent-blue system.

   .switch-grid is the exception, and only .switch-grid. It is a production
   switcher's tally bank — a picture of a piece of equipment, not a UI status
   indicator — and it is scoped the same way the theme already lets the video
   monitor opt out with its own near-black #050607 ("the frame around video
   content, not a themed UI surface"). Operators read a tally wall by colour
   before they read anything else, so green/red earns its place here and
   nowhere else on the page.

   The mapping lives in exactly two tokens, --tally-live and --tally-standby,
   so swapping to hardware convention (red = on air) is a one-line change.
   Decision + reasoning: 2026-09-23, with Keith. */

/* Email inputs are covered by product-theme.css itself as of 2026-09-07 — the
   hand-copied block that used to live here is exactly the duplicate that
   change was made to remove. Do not add it back. */

/* ── Micro-label — the theme's small-caps data label ───────────────────── */

.micro {
  font-family: var(--font-heading);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}

/* ── Environment selector ──────────────────────────────────────────────── */
/* A dropdown, not a wall of URLs: the environment is a one-click decision and
   the hosts behind it only matter when a review app has moved. */

.env-pick {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 14px;
}
.env-pick select {
  background: var(--panel-inset);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text-primary);
  font-family: var(--font-body);
  font-size: 15px;
  padding: 9px 12px;
  min-width: 260px;
}
.env-pick select:hover { border-color: var(--accent-blue); }
#envAdvanced { margin-bottom: 4px; }

/* ── Connect form ──────────────────────────────────────────────────────── */
/* The theme's .field puts a 200px label column beside every control, which is
   right for a settings page and far too airy for two short credentials. Scoped
   to this station only: labels above, fields side by side, sign-in on the same
   line. The theme's own <640px rule stacks .field the same way — this is that
   shape, applied at every width because these inputs are short. */

.connect-auth {
  display: flex;
  align-items: flex-end;
  gap: 14px;
  max-width: 860px;
}
.connect-fields {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 14px;
  flex: 1;
  min-width: 0;
}
/* An author `display` beats the UA stylesheet's [hidden] rule, so any toggled
   grid container needs this to honour the attribute. */
.connect-fields[hidden] { display: none; }

#st-connect .connect-fields .field { display: block; margin-bottom: 0; }
#st-connect .connect-fields .field label {
  display: block;
  flex: none;
  padding-top: 0;
  margin-bottom: 5px;
  font-family: var(--font-heading);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}
#st-connect .connect-fields .field input { font-size: 14.5px; padding: 9px 12px; }
.connect-auth .btn { flex: 0 0 auto; }

.connect-note {
  max-width: 860px;
  margin: 10px 0 0;
  font-size: 11.5px;
  color: var(--text-muted);
}

@media (max-width: 640px) {
  .connect-auth { flex-direction: column; align-items: stretch; }
  .connect-fields { grid-template-columns: minmax(0, 1fr); }
}

/* Hairline between choosing where to connect and who as — one station, two
   thoughts. A rule rather than a nested card, per the theme's section rhythm. */
.connect-rule {
  border-top: 1px solid var(--border);
  margin: 18px 0 16px;
}

/* ── Security badge ────────────────────────────────────────────────────── */

.security-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 13px;
  border-radius: 999px;
  font-size: 13px;
  font-weight: 600;
}
.security-badge.secure   { background: var(--accent-orange); color: var(--text-on-accent); }
.security-badge.unsecure { background: var(--accent-blue);   color: var(--text-on-accent); }
/* A session signed in from a URL token — visible so it is never mistaken for
   an ordinary interactive sign-in. */
.security-badge.urltoken {
  background: transparent;
  border: 1px solid var(--accent-orange);
  color: var(--accent-orange);
}

/* ── Number picker ─────────────────────────────────────────────────────── */

.number-picker { display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap; }

.number-picker button {
  width: 40px;
  height: 40px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--panel-inset);
  color: var(--text-secondary);
  font-family: var(--font-heading);
  font-size: 16px;
  font-weight: 700;
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
}
.number-picker button:hover  { border-color: var(--accent-blue); color: var(--accent-blue); }
.number-picker button:active { transform: translateY(1px); }
.number-picker button.active {
  border-color: var(--accent-orange);
  /* Mixed from the token, not a frozen rgba — the theme's accents changed on
     2026-09-07 and a hardcoded copy would have stayed on the old hex. */
  background: color-mix(in srgb, var(--accent-orange) 22%, transparent);
  color: var(--text-primary);
}

/* ── Mode toggle (existing / create) ───────────────────────────────────── */

.mode-toggle {
  display: inline-flex;
  margin-bottom: 16px;
  border: 1px solid var(--border);
  border-radius: 999px;
  overflow: hidden;
}
.mode-toggle button {
  padding: 8px 20px;
  background: transparent;
  border: none;
  color: var(--text-muted);
  font-family: var(--font-heading);
  font-size: 11.5px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.mode-toggle button:hover  { color: var(--text-primary); }
.mode-toggle button.active { background: var(--accent-orange); color: var(--text-on-accent); }

/* A note under a field rather than above it: it explains what the field does
   with what you type, which is only interesting once you are looking at it. */
.field-note {
  margin: 6px 0 0;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--text-muted);
}

/* The API's own word for this step, carried in the title so the mapping is
   stated where someone first meets it rather than in a note further down. */
.title-alt {
  font-weight: 400;
  color: var(--text-muted);
}

/* A quiet aside inside a help line — the UI says "stream group", the API says
   "room", and the examples show the API's word. Saying so once, where the two
   meet, is cheaper than leaving someone to spot the mismatch in a curl line. */
.nb { color: var(--text-muted); }
.nb code {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-secondary);
}

/* ── Create options: per-angle name, tags + transcodes ──────────── */
/* Laid out to match the chroma setup tuner's target rows, so the two pages read
   as one product: labelled fields, then the chips themselves with "tap to add",
   and the long tail behind "Show all". Nothing hides behind a summary.

   Two lines, because three things do not fit on one. Name and tags are the
   angle's identity and share the top line at EQUAL width — neither is the more
   important of the pair. The chips take a line of their own, so expanding one
   angle's list cannot shove its neighbours' fields around. */

/* Doubled class: the base .stream-row rule sits later in this file and would
   otherwise win the template on equal specificity. */
.stream-row.stream-row--create {
  grid-template-columns: 30px minmax(0, 1fr) minmax(0, 1fr) auto;
  row-gap: 10px;
  padding: 10px 12px;
}
.stream-row--create .angle-no { grid-row: 1; }
.stream-row--create .abr-line { grid-column: 2 / -1; }

.field-inline {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
}
/* Fixed label width so the two INPUTS come out identical: "View name" is a
   wider word than "Tags", and equal columns with unequal labels give unequal
   fields, which is the thing this layout exists to avoid. */
.field-inline .micro { flex: 0 0 68px; white-space: nowrap; }
.field-inline input {
  flex: 1 1 auto;
  min-width: 0;
}
.stream-row--create .opt-input {
  font-family: var(--font-mono);
  font-size: 12.5px;
}

/* The label and its own sub-label stack, the way the tuner heads its column. */
.abr-line {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  min-width: 0;
}
.abr-head {
  display: inline-flex;
  flex-direction: column;
  gap: 1px;
  flex: 0 0 auto;
  padding-top: 3px;
}
.micro--dim { color: var(--text-muted); opacity: 0.75; font-size: 9.5px; letter-spacing: 0.04em; }

/* Curated by default. Expanded it SCROLLS rather than growing: this org has 71
   profiles, and laid out flat they would push every other angle off screen. */
.abr-box { display: flex; gap: 6px; flex-wrap: wrap; flex: 1 1 auto; min-width: 0; }
.abr-box.expanded {
  max-height: 132px;
  overflow-y: auto;
  padding: 6px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 8px;
  scrollbar-color: var(--border) transparent;
}
.abr-box.expanded::-webkit-scrollbar { width: 8px; }
.abr-box.expanded::-webkit-scrollbar-thumb { background: var(--border); border-radius: 999px; }

/* Chip shape follows the tuner exactly — 7px radius, resolution over bitrate,
   centred — but on this theme's tokens rather than its hardcoded hexes. */
.abr-chip {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  line-height: 1.12;
  padding: 4px 11px;
  border-radius: 7px;
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text-muted);
  font-family: var(--font-heading);
  cursor: pointer;
  transition: background 0.14s ease, color 0.14s ease, border-color 0.14s ease;
}
.abr-chip:hover  { border-color: var(--accent-orange); color: var(--text-primary); }
.abr-chip:active { transform: translateY(1px); }
.abr-chip__res { font-size: 12px; font-weight: 700; }
.abr-chip__br  { font-size: 8.5px; font-weight: 600; opacity: 0.8; letter-spacing: 0.02em; }
.abr-chip.on {
  border-color: var(--accent-orange);
  background: color-mix(in srgb, var(--accent-orange) 16%, transparent);
  color: var(--accent-orange);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent-orange) 35%, transparent);
}

.abr-more {
  flex: 0 0 auto;
  padding: 5px 11px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text-secondary);
  font-family: var(--font-heading);
  font-size: 11.5px;
  font-weight: 700;
  white-space: nowrap;
  cursor: pointer;
}
.abr-more:hover  { border-color: var(--accent-blue); color: var(--text-primary); }
.abr-more:active { transform: translateY(1px); }

.create-foot {
  display: flex;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
  padding: 2px 8px 0;
}
.create-foot .opt-hint { flex: 1; min-width: 0; font-size: 11.5px; color: var(--text-muted); }

@media (max-width: 760px) {
  .stream-row.stream-row--create { grid-template-columns: 30px minmax(0, 1fr) auto; }
  .stream-row--create .field-inline { grid-column: 2 / -1; }
  .stream-row--create .abr-line { flex-wrap: wrap; }
}

/* ── Stream rows ───────────────────────────────────────────────────────── */

.stream-rows { display: flex; flex-direction: column; gap: 8px; margin: 14px 0; }

.stream-row {
  display: grid;
  grid-template-columns: 30px 150px 1fr auto;
  gap: 10px;
  align-items: center;
  padding: 6px 8px;
  background: var(--panel-inset);
  border: 1px solid var(--border);
  border-radius: 8px;
}

/* Angle number chip — same numbering the tally buttons and the 1–6 keyboard
   shortcuts use, so a row and its button are obviously the same thing. */
.stream-row .angle-no {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  border-radius: 999px;
  border: 1px solid var(--border);
  background: var(--panel-soft);
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-muted);
}

.stream-row input {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 6px;
  color: var(--text-secondary);
  padding: 7px 10px;
  font-size: 13.5px;
  font-family: var(--font-body);
  width: 100%;
  min-width: 0;
}
.stream-row input::placeholder { color: var(--text-muted); }
/* A bare `input list=` gives no sign a list exists — Chrome only reveals its
   button on hover and the other browsers show nothing at all. The caret is
   drawn in, so the field looks like what it is: type here, or open the list. */
.stream-row input[data-role="stream"] {
  font-family: var(--font-mono);
  font-size: 12.5px;
  padding-right: 26px;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path d='M1 1l4 4 4-4' fill='none' stroke='%239aa1a8' stroke-width='1.6' stroke-linecap='round'/></svg>");
  background-repeat: no-repeat;
  background-position: right 9px center;
}
.stream-row input[data-role="stream"]::-webkit-calendar-picker-indicator { opacity: 0; }
.stream-row input[data-role="stream"]:hover,
.stream-row input[data-role="stream"]:focus { border-color: var(--accent-blue); outline: none; }

/* Each row is its own grid, so an `auto` last column is sized per row and only
   row 1 carries the DEFAULT tag — which raggeds every row differently. Fix the
   column width so the pickers line up down the list. */
.stream-row > *:nth-child(4) {
  min-width: 62px;
  text-align: right;
}

/* A configured row shows its result rather than a control.

   Wrapping, not one rigid line. The parts carry white-space: nowrap and inherit
   min-width: 0 from the rule that keeps every row shrinkable — which together
   let a part shrink past its own text, so on a narrower window the key drew
   straight over the SERVER label beside it. Each part now refuses to shrink and
   moves to the next line instead, which is what "never truncated" has to mean
   at a width where the line simply does not hold. Doubled class: the shared
   .pick-cell rule sits later in this file. */
.pick-cell.pick-cell--done {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* Tighter between wrapped lines than along one — a wrapped row should read as
     one row that ran on, not as two. */
  gap: 6px 16px;
}
/* Key and server take exactly the room they need and are never cut: they are
   the two values you retype into an encoder, and half a hostname is worse than
   useless. */
.done-part {
  display: flex;
  align-items: center;
  gap: 8px;
  white-space: nowrap;
  flex: 0 0 auto;
  max-width: 100%;
}
/* The badges are the part that gives: they take the space left on the line, and
   drop to a line of their own once less than about 240px of it remains. A tag
   on a second line is still readable; a truncated key is not. */
.pick-cell--done .made-with { flex: 1 1 240px; justify-content: flex-end; min-width: 0; }


/* Every part of a finished row is named, in the same words the create row used
   to ask for it — View name, Key, Tags, Transcodes — so the row reads back as
   an answer to the form rather than as a line of unlabelled values. */
/* Three ranks on this row, so a label is never mistaken for a value and a value
   is never mistaken for a control: captions sit back, values come forward, and
   the single action carries the accent. */
.pick-cell--done .micro { flex: 0 0 auto; opacity: 0.75; }
.pick-cell--done .row-url { color: var(--text-secondary); }
.made-with .micro { margin-left: 4px; }
/* The finished row keeps the create row's labelled name field, so the two
   states of the same row line up instead of jumping. Its column has to grow to
   carry the label too — at the base 150px the field left about 75px for the
   name, which is not enough to read one, let alone edit it.
   Doubled class: the base .stream-row rule sits later in this file. */
.stream-row.stream-row--done { grid-template-columns: 30px minmax(0, 250px) minmax(0, 1fr) auto; }
.stream-row--done .field-inline .micro { flex: 0 0 68px; }
.stream-row--done input[readonly] { cursor: default; }

/* Below this the name field and the results stop fitting side by side — the
   results wrap internally long before the row does, which reads as clutter
   rather than as a full line. Give them a line each: name on top, key, server
   and badges underneath, left-aligned now that they own the width. */
@media (max-width: 1180px) {
  .stream-row.stream-row--done { grid-template-columns: 30px minmax(0, 1fr) auto; }
  /* Explicitly on the first line beside the name. Left to auto-placement it
     lands on a line of its own, which reads as a third fact about the angle
     rather than as a mark on it. */
  .stream-row--done .active-tag { grid-column: 3; grid-row: 1; }
  .stream-row--done .pick-cell--done { grid-column: 1 / -1; }
  .pick-cell--done .made-with { flex: 1 1 200px; justify-content: flex-start; }
  /* The field is for a short label, so it keeps its size rather than stretching
     the width of the row just because the width is there. */
  .stream-row--done .field-inline { max-width: 340px; }
}

/* Last resort, at a width where the server URL alone is wider than the row:
   break it across lines. Still whole, still copyable in one click — the only
   thing lost is the single line, which there is no longer room for. */
@media (max-width: 620px) {
  /* Each part gets its own line at this width — there is no pairing left to
     preserve, and a line each is easier to read off to someone. */
  .pick-cell--done .done-part,
  .pick-cell--done .made-with { flex: 1 1 100%; }
  .pick-cell--done .done-part { white-space: normal; }
  .pick-cell--done .row-url { white-space: normal; word-break: break-all; }
  .pick-cell--done .row-stream { white-space: normal; word-break: break-all; }
}

/* The ingest URL fills the gap between the key and the badges. Flat text, not a
   pill: it is there to be READ — to check the host is the one you expect — and
   only sometimes to be copied, so it should not compete with the button beside
   it that is always an action. */
.row-url {
  flex: 0 0 auto;
  padding: 2px 0;
  border: 0;
  background: none;
  text-align: left;
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-muted);
  white-space: nowrap;
  cursor: pointer;
}
.stream-row .row-stream:hover  { color: var(--accent-blue-hover); text-decoration: underline; }
.stream-row .row-stream:active { transform: translateY(1px); }
.stream-row .row-stream.copied { color: var(--accent-blue); text-decoration: none; }
.row-url:hover  { color: var(--text-secondary); text-decoration: underline; }
.row-url--err { color: var(--accent-orange); cursor: default; white-space: normal; }
.row-url--err:hover { color: var(--accent-orange); text-decoration: none; }
.row-url:active { transform: translateY(1px); }
.row-url.copied { color: var(--accent-blue); text-decoration: none; }

/* What the angle was created with, read back on the finished row. Read-only:
   these are facts about a stream that now exists, not controls — so no pill
   shape, no hover, nothing that invites a click that would do nothing. */
.made-with {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-wrap: wrap;
  min-width: 0;
}
.made-tag,
.made-rend {
  display: inline-flex;
  align-items: center;
  padding: 2px 8px;
  border-radius: 4px;
  font-size: 10.5px;
  font-weight: 600;
  white-space: nowrap;
}
.made-tag {
  border: 1px solid var(--border);
  background: var(--panel);
  color: var(--text-muted);
  font-family: var(--font-mono);
}
/* The view name goes in as a tag automatically. Given the same face as the tags
   you typed, the row reads as though you chose it — a dashed edge says it was
   added for you without inventing a fourth colour. */
.made-tag--auto {
  border-style: dashed;
  color: var(--text-secondary);
}
/* Renditions carry the accent the chips used when they were chosen, so the
   finished row and the form that produced it read as the same thing. */
.made-rend {
  border: 1px solid color-mix(in srgb, var(--accent-orange) 45%, transparent);
  background: color-mix(in srgb, var(--accent-orange) 12%, transparent);
  color: var(--accent-orange);
  font-family: var(--font-heading);
}
.made-note {
  font-size: 10.5px;
  color: var(--text-muted);
  white-space: nowrap;
}
/* The key copies on click, exactly like the server URL beside it — one
   interaction for the two values you paste into an encoder, and no separate
   COPY button competing with the labels around it for attention. */
.stream-row .row-stream {
  flex: 0 0 auto;
  padding: 2px 0;
  border: 0;
  background: none;
  text-align: left;
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--text-primary);
  white-space: nowrap;
  cursor: pointer;
}
.stream-row .row-pending {
  font-size: 13px;
  color: var(--text-muted);
}
.stream-row input[readonly] {
  background: var(--panel-soft);
  border-color: transparent;
  color: var(--text-primary);
}

.stream-row .active-tag {
  font-family: var(--font-heading);
  font-size: 10.5px;
  font-weight: 700;
  color: var(--accent-orange);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  white-space: nowrap;
}

/* The picker cell: one combobox — type a name or open the list — with a live
   reading of what the typed value resolves to. */
.pick-cell { display: flex; align-items: center; gap: 10px; min-width: 0; }
.pick-cell .pick-hint {
  /* Fixed width so the fields line up down the column: the hint texts are
     different lengths, which otherwise raggeds every row differently. */
  flex: 0 0 176px;
  font-size: 11.5px;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.pick-cell .pick-hint--live { color: var(--accent-blue); }
.pick-cell .pick-hint--err  { color: var(--accent-orange); }

.picker-bar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-bottom: 12px;
}
.picker-bar .picker-count { font-family: var(--font-mono); font-size: 11.5px; color: var(--text-muted); }

@media (max-width: 720px) {
  /* Two columns, explicit placement: the number keeps its chip beside the
     label, the picker and the tag each take the full width underneath.
     Without the explicit placement the picker lands in the 34px column. */
  .stream-row { grid-template-columns: 34px minmax(0, 1fr); }
  .stream-row .angle-no { grid-column: 1; }
  .stream-row > input[data-role="label"] { grid-column: 2; }
  .stream-row > *:nth-child(3) { grid-column: 1 / -1; }
  /* The picker stacks; the finished cell does not. It is already a wrapping
     row, and in a column its badge basis would be read as a HEIGHT — 200px of
     empty space under every row. */
  .pick-cell:not(.pick-cell--done) { flex-direction: column; align-items: stretch; gap: 6px; }
  .pick-cell .pick-hint { flex: 0 0 auto; align-self: flex-start; }
  .stream-row > *:nth-child(4) { grid-column: 1 / -1; }
  .stream-row .active-tag:empty { display: none; }
}

/* ── Ingest info panel ─────────────────────────────────────────────────── */

.ingest-panel {
  position: relative;
  margin: 16px 0;
  padding: 16px;
  background: var(--panel-inset);
  border: 1px solid var(--border);
  /* One accent edge, appearing exactly once on this surface. */
  border-top: 2px solid var(--accent-blue);
  border-radius: 8px;
}

.ingest-panel h3 {
  font-family: var(--font-heading);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin: 0 0 12px;
}

.ingest-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 7px 0;
  border-bottom: 1px solid var(--border);
  font-size: 13px;
}
.ingest-row:last-child { border-bottom: none; }
.ingest-row .ingest-label { flex: 0 0 110px; color: var(--text-muted); font-weight: 600; font-size: 12px; }
.ingest-row .ingest-value {
  flex: 1;
  font-family: var(--font-mono);
  font-size: 12.5px;
  color: var(--text-primary);
  word-break: break-all;
  min-width: 0;
}

/* ── Player ────────────────────────────────────────────────────────────── */

/* Near-black on purpose: this is the frame around video content, not a
   themed UI surface — same reasoning as the theme's .monitor. */
.player-frame {
  position: relative;
  width: 100%;
  /* Capped so the tally bank stays visible alongside the picture. A switcher
     where you cannot see the buttons and the programme at the same time is
     the wrong shape, however big the video is. */
  max-width: 720px;
  aspect-ratio: 16 / 9;
  /* Near-black on purpose: the frame around video content, not a themed UI
     surface. The bezel is what stops it reading as a hole cut in the page —
     an inner hairline to catch the edge, a dark rim, and real elevation. */
  background: #050607;
  border: 1px solid #090c0f;
  border-radius: 10px;
  overflow: hidden;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.05),
    inset 0 1px 0 rgba(255, 255, 255, 0.07),
    0 1px 0 rgba(255, 255, 255, 0.04),
    0 18px 48px rgba(0, 0, 0, 0.55);
}
.player-frame iframe { width: 100%; height: 100%; border: 0; display: block; }

.player-frame .player-empty {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  text-align: center;
  padding: 24px;
  color: var(--text-muted);
  font-size: 13px;
}

/* One-shot cut confirmation on the frame — fires when the picture actually
   changes, which is a different moment from the button taking. */
.player-frame.took::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  border: 2px solid var(--tally-live-edge);
  border-radius: 8px;
  animation: cut-flash 240ms ease-out;
}
@keyframes cut-flash { from { opacity: 1; } to { opacity: 0; } }

.player-url-box {
  max-width: 720px;
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 14px 0;
  padding: 12px 14px;
  background: var(--panel-inset);
  border: 1px solid var(--border);
  border-radius: 8px;
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--text-secondary);
}
.player-url-box a {
  color: var(--accent-blue);
  text-decoration: none;
  flex: 1;
  min-width: 0;
  word-break: break-all;
}
.player-url-box a:hover { text-decoration: underline; }

/* ── Tally bank — see the carve-out note at the top of this file ───────── */

/* The carve-out's scope is the control station and nothing else: the tally
   bank, plus the player frame's cut confirmation, which is the same signal
   rendered on the video. Declared here rather than on :root so these hues
   cannot reach any other status surface on the page. */
#st-control {
  --tally-live:       #1e8a48;   /* white-on-fill contrast 4.4:1 */
  --tally-live-edge:  #2fbf6a;
  --tally-standby:    #d94f45;
  /* Six standby buttons outlined in full red is a wall of alarm and makes the
     one live button work harder to stand out. The dot and the STANDBY word
     keep the red identity; the keyline sits back. */
  --tally-standby-edge: rgba(217, 79, 69, 0.42);   /* on --panel-inset, contrast 4.5:1 */
}

.switch-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin: 16px 0;
}

.switch-btn {
  display: inline-flex;
  align-items: flex-start;
  gap: 10px;
  min-width: 156px;
  padding: 11px 16px;
  border-radius: 8px;
  border: 1px solid var(--tally-standby-edge);
  background: var(--panel-inset);
  color: var(--text-primary);
  font-family: var(--font-heading);
  text-align: left;
  cursor: pointer;
  transition: border-color 0.14s ease, background 0.14s ease;
}
.switch-btn:hover:not(:disabled):not(.live) { background: var(--panel-soft); }
.switch-btn:active:not(:disabled) { transform: translateY(1px); }

/* Styled as the keycap it is — same shape as the <kbd> in the hint below the
   bank, so the "or press 1–6" instruction points at something recognisable. */
.switch-btn .tally-no {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  flex: 0 0 22px;
  margin-top: 1px;               /* sits on the label's line, not the block's middle */
  border-radius: 4px;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-bottom-width: 2px;
  background: rgba(255, 255, 255, 0.07);
  font-family: var(--font-mono);
  font-size: 11px;
  color: inherit;
}
.switch-btn .tally-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.switch-btn .tally-label {
  font-size: 14px;
  font-weight: 700;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.switch-btn .tally-stream {
  font-family: var(--font-mono);
  font-size: 10.5px;
  color: var(--text-muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.switch-btn .tally-cue,
.switch-btn .tally-state {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

/* Static dot — the theme forbids looping "breathe"/"ping" motion, and fill
   weight carries on-air perfectly well without it. */
.switch-btn .tally-dot {
  width: 7px;
  height: 7px;
  flex: 0 0 7px;
  border-radius: 50%;
  background: currentColor;
}
.switch-btn .tally-line { display: flex; align-items: center; gap: 6px; }

/* STANDBY */
.switch-btn .tally-state { color: var(--tally-standby); }
.switch-btn .tally-dot   { color: var(--tally-standby); }

/* ON AIR */
.switch-btn.live {
  background: var(--tally-live);
  border-color: var(--tally-live-edge);
  color: #fff;
  cursor: default;
  /* Lit from above and standing off the page — the on-air button should look
     like the one thing on the bank that is switched on. */
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.22),
    0 6px 18px rgba(30, 138, 72, 0.3);
}
.switch-btn.live .tally-state { color: rgba(255, 255, 255, 0.85); }
.switch-btn.live .tally-dot   { color: #fff; }
/* White-on-green, not a dark blob: the chip has to stay readable on the fill. */
.switch-btn.live .tally-no {
  background: rgba(255, 255, 255, 0.2);
  border-color: rgba(255, 255, 255, 0.34);
}

/* Mid-PUT — neutral, so a request in flight never reads as either tally state */
.switch-btn.switching {
  border-color: var(--border-strong);
  background: var(--panel-soft);
  cursor: progress;
}
.switch-btn.switching .tally-state { color: var(--text-muted); }
.switch-btn.switching .tally-dot   { color: var(--text-muted); }

/* Expected on air, but the room has not said so yet — a restored room can be
   on any angle, and there is no route to ask it. Neutral like a switch in
   flight, but still clickable: clicking is how you force the room onto it. */
.switch-btn.unconfirmed {
  border-color: var(--border-strong);
  background: var(--panel-soft);
}
.switch-btn.unconfirmed .tally-state { color: var(--text-muted); }
.switch-btn.unconfirmed .tally-dot   { color: var(--text-muted); }
/* No "Cut to" swap on hover: it may already be the live one. */
.switch-btn.unconfirmed:hover { border-color: var(--border-strong); }

.switch-btn.failed { animation: tally-fail 240ms ease-out; }
@keyframes tally-fail {
  0%   { border-color: var(--tally-standby); }
  40%  { border-color: #fff; }
  100% { border-color: var(--tally-standby); }
}

.switch-btn:disabled:not(.live) { opacity: 0.5; cursor: not-allowed; }

.tally-hint { font-size: 11.5px; color: var(--text-muted); margin: 0 0 4px; }
.tally-hint kbd {
  display: inline-block;
  font-family: var(--font-mono);
  font-size: 10.5px;
  line-height: 1.4;
  color: var(--text-primary);
  background: var(--panel-soft);
  border: 1px solid var(--border-strong);
  border-bottom-width: 2px;
  border-radius: 4px;
  padding: 1px 6px;
  margin: 0 1px;
}

/* ── API examples ──────────────────────────────────────────────────────── */

.api-panel { margin-top: 20px; }

.api-block { margin-bottom: 14px; }
.api-block__head {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 6px;
}
.api-block__title {
  font-family: var(--font-heading);
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.api-block__note { font-size: 11.5px; color: var(--text-muted); }

.api-pre {
  margin: 0;
  padding: 12px 14px;
  background: var(--panel-inset);
  border: 1px solid var(--border);
  border-radius: 8px;
  font-family: var(--font-mono);
  font-size: 12px;
  line-height: 1.65;
  color: var(--text-primary);
  white-space: pre;
  overflow-x: auto;
  /* Capped so several open snippets stay a quiet side path rather than a
     wall of chrome competing with the control surface above. */
  max-height: 260px;
  overflow-y: auto;
}
.api-pre .tok-kw  { color: var(--accent-blue); }
.api-pre .tok-str { color: var(--text-secondary); }
.api-pre .tok-com { color: var(--text-muted); font-style: italic; }
.api-pre .tok-num { color: var(--accent-orange); }

/* ── SSE observer strip ────────────────────────────────────────────────── */

.sse-strip {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  margin: 14px 0 0;
  padding: 8px 12px;
  background: var(--panel-soft);
  border: 1px solid var(--border);
  border-left: 3px solid var(--accent-blue);
  border-radius: 6px;
  font-size: 12px;
  color: var(--text-primary);
}
.sse-strip.attn { border-left-color: var(--accent-orange); }
.sse-strip .sse-seq { font-family: var(--font-mono); color: var(--text-muted); }
.sse-strip .sse-note { color: var(--text-muted); font-size: 11.5px; margin-left: auto; }

/* ── Copy button ───────────────────────────────────────────────────────── */

.copy-btn {
  background: var(--panel);
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-muted);
  padding: 4px 11px;
  font-size: 11px;
  font-family: var(--font-body);
  cursor: pointer;
  white-space: nowrap;
  transition: color 0.15s ease, border-color 0.15s ease;
}
.copy-btn:hover  { color: var(--accent-blue); border-color: var(--accent-blue); }
.copy-btn:active { transform: translateY(1px); }
/* Confirm is blue, never a green tick — green is reserved for the tally bank. */
.copy-btn.copied { color: var(--accent-blue); border-color: var(--accent-blue); }

/* ── Status messages ───────────────────────────────────────────────────── */

.status-msg { font-size: 13px; margin-top: 8px; }
.status-msg.ok      { color: var(--accent-blue); }
.status-msg.err     { color: var(--accent-orange); }
.status-msg.loading { color: var(--text-muted); }

/* ── Themed scrollbars ─────────────────────────────────────────────────── */

.api-pre, .log-box, .stream-rows {
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}
.api-pre::-webkit-scrollbar, .log-box::-webkit-scrollbar { width: 8px; height: 8px; }
.api-pre::-webkit-scrollbar-thumb, .log-box::-webkit-scrollbar-thumb {
  background: var(--border);
  border-radius: 999px;
}
.api-pre::-webkit-scrollbar-track, .log-box::-webkit-scrollbar-track { background: transparent; }

/* ── Reduced motion ────────────────────────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  .player-frame.took::after,
  .switch-btn.failed { animation: none; }
  .number-picker button, .switch-btn, .copy-btn, .mode-toggle button {
    transition: none;
  }
  .number-picker button:active, .switch-btn:active, .copy-btn:active {
    transform: none;
  }
}

/* ── Overflow guard ────────────────────────────────────────────────────── */
/* Long unbroken values (stream names, player URLs, curl lines) must scroll
   inside their own box, never widen the page. */
.stream-row, .ingest-row, .player-url-box, .sse-strip { min-width: 0; }
.stream-row > *, .pick-cell > * { min-width: 0; max-width: 100%; }

/* ── Page width ────────────────────────────────────────────────────────── */
/* Wider than the theme's 1040px .container because the programme picture is the
   point of this page, so the page is built on the theme's .workbench column
   (1520px) instead. The header and footer follow automatically — theme rule 9
   widens them wherever a .workbench is present, which is why this file no
   longer hand-writes a .header-inner/.container/.footer-inner override.
   .workbench is width only, so the column's own stacking comes from here. */

.workbench {
  display: flex;
  flex-direction: column;
  gap: 20px;
  padding-bottom: 40px;
}

/* ── Control surface (post-setup focus) ───────────────────────────────── */
/* Once the room exists the page stops being a form: stations 1–4 collapse to
   their one-line summaries (the theme's own journey behaviour) and this
   station owns the screen — player, then the buttons directly beneath it. */

/* The bar rides in the station head's trailing `auto` column, on the title
   line. It carries no surface of its own there — the buttons' own keylines
   are enough, and a filled panel inside a header would read as a second
   header. The row it used to occupy below is now picture. */
/* Moving the bar into the head made it visible before there is a room to act
   on — a head shows whatever its station's state, unlike a body. Nothing here
   means anything until setup has run. */
#st-control[data-state="locked"] .control-bar,
#st-control[data-state="locked"] .station__summary { display: none; }

.control-bar {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 10px;
  flex-wrap: wrap;
}
/* Room id sits in the station summary, under the title — the one place it
   belongs, rather than once there and again in the bar beside it. */
#st-control .control-room {
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--text-primary);
  background: var(--panel-inset);
  border: 1px solid var(--border);
  border-radius: 4px;
  padding: 2px 8px;
}
/* Grouped by what the buttons are for — sharing the room, versus driving the
   player in front of you. A flat row of identical pills makes you read every
   label to find the one you want. */
.control-bar .control-group { display: flex; align-items: center; gap: 6px; }
.control-bar .control-group + .control-group {
  padding-left: 12px;
  margin-left: 6px;
  border-left: 1px solid var(--border);
}
.control-bar .btn-sm-inline {
  margin-left: 6px;
  padding: 7px 16px;
  font-size: 12.5px;
}
/* Below this the three head columns stop fitting side by side, so the bar
   drops to its own row — still indented under the title, not the marker. */
@media (max-width: 1080px) {
  #st-control > /* Room above a station when the page scrolls to it, so it does not land hard
   against the top of the window. */
.station { scroll-margin-top: 18px; }

.station__head { grid-template-columns: 34px 1fr; row-gap: 10px; }
  #st-control .control-bar { grid-column: 2 / -1; justify-content: flex-start; }
}
@media (max-width: 720px) {
  .control-bar .control-group + .control-group { padding-left: 0; margin-left: 0; border-left: 0; }
  .control-bar .btn-sm-inline { margin-left: 0; }
}

/* The player is the hero here — no 720px cap, it takes the column, and gets a
   heavier bezel to match the extra size. */
.player-frame--control {
  /* Bound the picture by the height actually available, not just the column
     width: 16:9 at full width pushes the tally bank below the fold on a
     laptop, which is the one thing this layout must not do. Capping the
     WIDTH from the remaining height keeps the aspect ratio intact. */
  /* Budget measured in the browser, not guessed: 389px of chrome above the
     picture and below it through the tally bank, once the control bar moved
     up onto the station's title line. */
  max-width: min(100%, calc((100vh - 395px) * 16 / 9));
  min-width: 360px;
  margin-inline: auto;
  border-radius: 12px;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.06),
    inset 0 1px 0 rgba(255, 255, 255, 0.08),
    0 1px 0 rgba(255, 255, 255, 0.05),
    0 26px 70px rgba(0, 0, 0, 0.62);
}
.player-frame--control.took::after { border-radius: 12px; }

/* Buttons sit directly under the picture: one glance covers both, centred so
   the bank grows outward in both directions as angles are added. */
.player-frame--control + .switch-grid {
  margin-top: 14px;
  margin-bottom: 6px;
  justify-content: center;
}
.tally-foot {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;
  flex-wrap: wrap;
  margin-top: 8px;
}
.tally-foot .status-msg { margin: 0; }
.tally-foot .tally-hint { margin: 0; }
.player-frame--control + .switch-grid + .tally-hint { margin-bottom: 10px; }

/* Programme identification over the picture, the way a monitor carries it. */
.player-overlay {
  position: absolute;
  top: 12px;
  left: 12px;
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 12px;
  border-radius: 999px;
  background: rgba(5, 6, 7, 0.68);
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  pointer-events: none;   /* never steal a click from the player beneath */
}
.player-overlay .player-tag {
  font-family: var(--font-heading);
  font-size: 9.5px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--tally-live-edge);
}
.player-overlay .player-angle {
  font-family: var(--font-heading);
  font-size: 13px;
  font-weight: 700;
  color: #fff;
}
.player-overlay .player-src {
  font-family: var(--font-mono);
  font-size: 11px;
  color: rgba(255, 255, 255, 0.55);
}

/* ── Log ───────────────────────────────────────────────────────────────── */

.log-stats {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-muted);
  margin-bottom: 8px;
}
/* Two hues only — the tally bank is the page's one green/red exception. */
.log-line--ok  .evt { color: var(--accent-blue); }
.log-line--err .evt { color: var(--accent-orange); }
.log-line--sse .evt { color: var(--text-muted); }
/* API traffic is the substrate everything else sits on — present but quiet, so
   the cuts and the SSE events still read first. */
.log-line--api .evt { color: var(--text-muted); font-family: var(--font-mono); font-size: 11.5px; }
.log-line--cut .evt { color: var(--text-primary); font-weight: 600; }
.log-actions { display: flex; gap: 10px; flex-wrap: wrap; }

/* ── Drawer contents ───────────────────────────────────────────────────── */

.drawer-panel__body { padding: 18px; overflow-y: auto; }
.drawer-section {
  font-family: var(--font-heading);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin: 22px 0 10px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}
.drawer-panel__body > .drawer-section:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.drawer-panel__body .ingest-panel:first-child { margin-top: 0; }
.drawer-panel__body .station__help { max-width: none; margin: 10px 0 0; }

/* ── Narrow screens ────────────────────────────────────────────────────── */
/* Placed last on purpose: these override base rules defined further up, and
   an @media block does not raise specificity — only source order decides. */
@media (max-width: 720px) {
  .ingest-row { flex-wrap: wrap; row-gap: 2px; }
  .ingest-row .ingest-label { flex: 1 0 100%; }
  .ingest-row .ingest-value { flex: 1 1 auto; }
  .switch-btn { flex: 1 1 100%; min-width: 0; }
  .api-block__head { flex-wrap: wrap; }
}

/* ── Tally hover: say what the click will do ───────────────────────────── */
/* Resting states stay static (theme rule 5); this is a hover affordance, not
   an animation. */
.switch-btn .tally-cue { display: none; color: var(--text-primary); }
.switch-btn:hover:not(.live):not(.switching):not(.unconfirmed) .tally-state { display: none; }
.switch-btn:hover:not(.live):not(.switching):not(.unconfirmed) .tally-cue   { display: block; }
/* Hover previews the cut: the edge takes the on-air green, so the button
   shows what it is about to become. */
.switch-btn:hover:not(.live):not(.switching):not(.unconfirmed) {
  border-color: var(--tally-live-edge);
  background: var(--panel-soft);
}
.switch-btn.live .tally-stream { color: rgba(255, 255, 255, 0.72); }

/* ── Focused mode ──────────────────────────────────────────────────────── */
/* Once the room is up, the setup steps are history: they stay visible as the
   theme requires, but compressed hard so the picture and the tally bank own
   the screen. Clicking a completed step opens it again. */

/* A completed step is re-openable whether or not the page has focused down —
   the affordance should not appear only after the room exists. */
.station[data-state="done"] > .station__head { cursor: pointer; }
.station[data-state="done"] > .station__head:focus-visible {
  outline: 2px solid var(--accent-blue);
  outline-offset: -2px;
  border-radius: 4px;
}
.station:not([data-state="done"]) > .station__head { cursor: default; }

.journey.focused .station[data-state="done"] > .station__head {
  padding-top: 7px;
  padding-bottom: 7px;
}
.journey.focused .station[data-state="done"] .station__head > div:last-child {
  display: flex;
  align-items: baseline;
  gap: 10px;
  flex-wrap: wrap;
}
.journey.focused .station[data-state="done"] .station__title { font-size: 13.5px; }
.journey.focused .station[data-state="done"] .station__summary { font-size: 12px; margin: 0; }
.journey.focused .station[data-state="done"] > .station__head:hover .station__title {
  color: var(--accent-blue);
}
/* The marker's ring is sized for the taller head; shrink it to match. */
.journey.focused .station[data-state="done"] .station__marker {
  width: 24px;
  height: 24px;
  font-size: 12px;
}
.journey.focused .station[data-state="done"][data-open="1"] > .station__head { cursor: pointer; }


/* ── Viewer page (view.html) ───────────────────────────────────────────── */
/* The audience's side of the demo: two thirds picture, one third the switching
   traffic that produced it. Same chrome as every other page, so it can be put
   on a second screen next to the control page without looking like a tool. */

.view-stage {
  display: grid;
  grid-template-columns: 2fr minmax(300px, 1fr);
  /* An auto row would grow with the log and push the footer off screen — the
     box only scrolls internally once its row is capped to the space there is. */
  grid-template-rows: minmax(0, 1fr);
  gap: 18px;
  /* Stretch, not start: the log is the picture's companion and should run the
     full height beside it, not stop a third of the way down. */
  align-items: stretch;
  margin-top: 4px;
}
.view-player { display: flex; flex-direction: column; min-width: 0; min-height: 0; }

/* This page fills the window: the picture and the log take whatever is left
   between the header and the footer instead of sitting in the top corner of an
   empty page. The theme already makes <body> a flex column with main:flex:1, so
   the chain only has to be carried down from there — and every link needs
   min-height:0, or a flex item refuses to shrink below its content and the
   whole column overflows the viewport. */
/* Column, not a bare flex: as a ROW container main made .workbench a row item
   and its own width/auto-margins stopped centring it, so the page lost its
   gutters. A column keeps the workbench's width rule intact and only hands it
   the leftover height. */
/* The theme gives body min-height: 100vh, which grows with content — so every
   rule below it stretched to the window but nothing capped it, and a long log
   pushed the footer off screen instead of scrolling in its own box. A real
   height is what makes the fill work in both directions. */
.view-page { height: 100vh; height: 100dvh; overflow: hidden; }
.view-page main { display: flex; flex-direction: column; min-height: 0; }
.view-page .workbench { flex: 1; display: flex; flex-direction: column; min-height: 0; }
.view-page .view-stage { flex: 1; min-height: 0; }

/* Sized from the HEIGHT available, not the width: centred and left to grow,
   with 16:9 deriving the width. Stretching it to the column instead would
   letterbox the picture inside its own frame. */
.player-frame--view {
  flex: 1 1 auto;
  align-self: center;
  width: auto;
  max-width: 100%;
  min-height: 0;
  min-width: 320px;
  border-radius: 12px;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, 0.06),
    inset 0 1px 0 rgba(255, 255, 255, 0.08),
    0 26px 70px rgba(0, 0, 0, 0.62);
}
.player-frame--view.took::after { border-radius: 12px; }

.view-foot {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  margin-top: 10px;
  font-size: 12px;
  color: var(--text-muted);
}
/* On air, in the tally bank's own green — this is the same piece of equipment
   language as .switch-grid, and the carve-out documented at the top of this
   file covers it for the same reason. */
.view-tally {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-family: var(--font-heading);
  font-size: 13px;
  font-weight: 700;
  color: var(--text-secondary);
}
.view-tally .view-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--border-strong);
}
.view-tally.on { color: #2fbf6a; }
.view-tally.on .view-dot { background: #2fbf6a; }
.view-room { font-family: var(--font-mono); font-size: 11.5px; }
.view-cuts { margin-left: auto; font-family: var(--font-mono); font-size: 11.5px; }

.view-log {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: 0;
  padding: 14px 16px;
  background: var(--panel);
  border: 1px solid var(--border);
  border-top: 2px solid var(--accent-blue);
  border-radius: 4px;
}
.view-log__head { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.view-log__head h2 {
  flex: 1;
  margin: 0;
  font-family: var(--font-heading);
  font-size: 15px;
  font-weight: 700;
  color: var(--text-primary);
}
.view-log__lead {
  margin: 4px 0 10px;
  font-size: 11.5px;
  line-height: 1.45;
  color: var(--text-muted);
}
.view-log__box {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 10px;
  background: var(--panel-inset);
  border: 1px solid var(--border);
  border-radius: 8px;
  scrollbar-color: var(--border) transparent;
}
.view-log__box::-webkit-scrollbar { width: 8px; }
.view-log__box::-webkit-scrollbar-thumb { background: var(--border); border-radius: 999px; }
.view-log__foot { display: flex; gap: 8px; margin-top: 10px; }

/* Connection state as a dot. Static — theme rule 5 forbids a looping pulse,
   and colour plus the word beside it carries the state perfectly well. */
.sse-dot {
  width: 8px; height: 8px;
  border-radius: 50%;
  background: var(--text-muted);
}
.sse-dot[data-state="live"]         { background: var(--accent-blue); }
.sse-dot[data-state="waiting"],
.sse-dot[data-state="connecting"]   { background: var(--text-muted); }
.sse-dot[data-state="reconnecting"],
.sse-dot[data-state="closed"],
.sse-dot[data-state="no log"],
.sse-dot[data-state="no group"]     { background: var(--accent-orange); }

.view-log .log-line--cut   .evt { color: var(--text-primary); font-weight: 700; }
.view-log .log-line--ok    .evt { color: var(--accent-blue); }
.view-log .log-line--err   .evt { color: var(--accent-orange); }
.view-log .log-line--api   .evt { color: var(--text-muted); }
.view-log .log-line--muted .evt { color: var(--text-muted); }

/* Each line carries three layers: the sentence, the values an integrator would
   check, and the raw payload behind a toggle. Spacing does the separating — a
   log that boxed every entry would read as a list of cards, not a transcript.

   One column edge for all three. The timestamp sits in a fixed gutter and a
   hanging indent pulls it back out, so the sentence, its fields and its payload
   all begin at the same x and a wrapped sentence does not run back underneath
   the clock. Indent rather than a grid, because the payload toggle has to stay
   on the sentence's own line and a grid would give it a row of its own. */
.view-log .log-line {
  padding: 5px 0 5px 74px;
  text-indent: -74px;
  font-family: var(--font-mono);
  font-size: 11.5px;
  line-height: 1.65;
  border-bottom: 1px solid rgba(255, 255, 255, 0.04);
}
.view-log .log-line:last-child { border-bottom: 0; }
.view-log .log-line .ts {
  display: inline-block;
  width: 64px;
  margin-right: 10px;
  text-indent: 0;
  color: var(--text-muted);
}
/* One size and one face for every layer — the type does not rank the lines,
   the colour does, and a log that changed font between a sentence and its
   values read as two logs stacked. */
.view-log .log-line .evt { font-family: var(--font-mono); font-size: 11.5px; }

.log-fields {
  display: grid;
  /* A fixed caption column, not auto: with auto, every line sized its own and
     the values stepped in and out down the log. One edge for the names and one
     for the values is the whole point of showing them as fields. */
  grid-template-columns: 146px minmax(0, 1fr);
  gap: 1px 12px;
  margin: 3px 0 0;
  text-indent: 0;
}
@media (max-width: 1240px) {
  /* Below this the caption column costs more than it buys — the values get so
     little width they wrap mid-token. Stack instead. */
  .log-fields { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .log-fields .log-v { margin-bottom: 2px; }
}
.log-k {
  font-size: 10.5px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.log-v {
  font-family: var(--font-mono);
  font-size: 11.5px;
  color: var(--text-secondary);
  word-break: break-word;
}
.view-log .log-line--cut .log-v,
.view-log .log-line--ok  .log-v { color: var(--text-primary); }

/* Quiet until wanted: the payload is for the one person in the room who asks
   what actually came over the wire. */
.log-raw-btn {
  margin-left: 8px;
  padding: 0 6px;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: 0.04em;
  color: var(--text-muted);
  background: none;
  border: 1px solid var(--border);
  border-radius: 999px;
  cursor: pointer;
  transition: color 120ms ease, border-color 120ms ease;
}
.log-raw-btn:hover { color: var(--text-secondary); }
.log-raw-btn.on { color: var(--accent-blue); border-color: var(--accent-blue); }

.log-raw {
  margin: 6px 0 0;
  text-indent: 0;
  padding: 8px 10px;
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.55;
  color: var(--text-secondary);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  overflow-x: auto;
  white-space: pre;
  scrollbar-color: var(--border) transparent;
}
.log-raw[hidden] { display: none; }
.log-raw { cursor: pointer; }
.log-raw:hover { border-color: var(--text-muted); }
.log-raw.copied { border-color: var(--accent-blue); color: var(--accent-blue); }

/* The example calls carry the accent the rest of the page gives to things you
   act on — a payload is something that happened, an api call is something you
   can run. Same block, one edge of colour to tell them apart. */
.log-curl {
  border-left: 2px solid var(--accent-blue);
  color: var(--text-secondary);
  /* Wrapped, not scrolled sideways: in a third of a page a curl line would
     otherwise show its first forty characters and hide the rest. Wrapping is
     visual only — what gets copied keeps its real newlines and backslashes. */
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}
.log-curl:hover { border-color: var(--border); border-left-color: var(--accent-blue); }

/* Silence is the only liveness signal EventSource gives us — the keepalive is
   a comment and never reaches JS — so the page reports the silence honestly. */
/* Sits with the log's buttons rather than under the title: it changes every
   second, and a ticking number beside the heading pulls the eye off the log. */
.log-age {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 10.5px;
  color: var(--text-muted);
  align-self: center;
}

@media (max-width: 1000px) {
  /* Stacked, the fill-the-window idea stops helping — the two would each get
     half a phone screen. Back to normal flow, picture first. */
  .view-page { height: auto; overflow: visible; }
  .view-page main { display: block; }
  .view-page .workbench,
  .view-page .view-stage { display: block; }
  .view-stage { grid-template-columns: minmax(0, 1fr); }
  .player-frame--view { width: 100%; max-width: 100%; aspect-ratio: 16 / 9; }
  .view-log { margin-top: 18px; }
  .view-log__box { min-height: 220px; max-height: 320px; }
}

/* ── Inside the Builder Hub (?embed=1) ─────────────────────────────────── */
/* The hub shows the header, footer and sign-in; step 1 keeps its done line
   ("Signed in as …"). The frame is as tall as the page, so the body must not
   hold it at 100vh, or it never shrinks again. */
.is-embed .header,
.is-embed .footer,
.is-embed .connect-rule,
.is-embed .connect-auth { display: none; }
.is-embed body { min-height: 0; }
/* Fixed means fixed to that tall frame: the hub's embed.js moves the How it
   works drawer (#drawer, aria-modal, the fixed overlay) onto the part of the
   frame the visitor sees; its panel and backdrop fill that overlay. No
   --hub-view-* offset on the panel here, or it would be moved twice. */
