:root {
  --bg: #0b0f14;
  --panel: #141a22;
  --panel-2: #1b232d;
  --line: #263140;
  --text: #e7edf5;
  --muted: #93a3b8;
  --accent: #3d8bfd;
  --ok: #35c491;
  --warn: #e8b53c;
  --bad: #e8604c;
  --radius: 10px;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--bg);
  color: var(--text);
  font: 15px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--accent); }

.wrap {
  max-width: 460px;
  margin: 0 auto;
  padding: 12vh 24px 48px;
}

.wrap.wide { max-width: 1120px; padding-top: 32px; }

.brand {
  font-size: 13px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 28px;
}

h1 { font-size: 24px; margin: 0 0 6px; font-weight: 600; letter-spacing: -0.01em; }
h2 { font-size: 16px; margin: 0 0 12px; font-weight: 600; }
p.sub { color: var(--muted); margin: 0 0 28px; }

.card {
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 20px;
}

label {
  display: block;
  font-size: 12px;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--muted);
  margin: 0 0 6px;
}

input[type="text"], input[type="password"], select, textarea {
  width: 100%;
  padding: 11px 12px;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 8px;
  color: var(--text);
  font-size: 15px;
  font-family: inherit;
}

input#roomId { text-transform: uppercase; letter-spacing: 0.18em; font-family: var(--mono); }

input:focus, select:focus { outline: 2px solid var(--accent); outline-offset: 1px; border-color: transparent; }

.field { margin-bottom: 16px; }

button {
  font: inherit;
  font-weight: 600;
  padding: 11px 16px;
  border-radius: 8px;
  border: 1px solid transparent;
  background: var(--accent);
  color: #fff;
  cursor: pointer;
}
button:hover { filter: brightness(1.08); }
button:disabled { opacity: 0.5; cursor: default; }
button.secondary { background: var(--panel-2); color: var(--text); border-color: var(--line); }
button.danger { background: transparent; color: var(--bad); border-color: var(--bad); }
button.full { width: 100%; }
button.sm { padding: 6px 10px; font-size: 13px; }

.msg { margin-top: 14px; font-size: 14px; min-height: 20px; }
.msg.error { color: var(--bad); }
.msg.ok { color: var(--ok); }

.pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 3px 9px;
  border-radius: 999px;
  border: 1px solid var(--line);
  color: var(--muted);
}
.pill .dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
/* The solid border-color is the fallback: an engine without color-mix() drops the second
   declaration and keeps the first, which reads as a slightly stronger version of the same pill
   rather than inheriting the neutral --line border and losing the state colour entirely. */
.pill.live { color: var(--ok); border-color: var(--ok); border-color: color-mix(in srgb, var(--ok) 40%, transparent); }
.pill.warn { color: var(--warn); border-color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, transparent); }
.pill.bad { color: var(--bad); border-color: var(--bad); border-color: color-mix(in srgb, var(--bad) 40%, transparent); }

.mono { font-family: var(--mono); font-size: 13px; word-break: break-all; }

.copyrow { display: flex; gap: 8px; align-items: stretch; margin-bottom: 12px; }
.copyrow .val {
  flex: 1;
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 8px;
  padding: 9px 11px;
  font-family: var(--mono);
  font-size: 12.5px;
  word-break: break-all;
}

table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--line); font-size: 14px; }
th { font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); }
tr.clickable:hover { background: var(--panel-2); cursor: pointer; }

.grid2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 820px) { .grid2 { grid-template-columns: 1fr; } }

.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
.spread { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
.muted { color: var(--muted); }
.small { font-size: 13px; }
.mt { margin-top: 18px; }
.hidden { display: none !important; }

.log { max-height: 220px; overflow-y: auto; font-family: var(--mono); font-size: 12px; }
.log div { padding: 4px 0; border-bottom: 1px solid var(--line); color: var(--muted); }
/* Fixed-width timestamp column so the event names line up and the log scans vertically.
   Tabular numerals stop "Aug 9" and "Aug 19" shifting the whole row. */
.log .logat {
  /* Wide enough for the longest form this renders - "Aug 9, 10:05 AM" - so the event names line
     up in a column. Measured, not guessed: at 116px the two-digit-hour rows pushed past it. */
  display: inline-block; min-width: 132px;
  color: var(--text); opacity: 0.65;
  font-variant-numeric: tabular-nums;
}

/* ---- watch page ---- */
body.watch { display: flex; flex-direction: column; min-height: 100vh; }
.topbar {
  display: flex; justify-content: space-between; align-items: center;
  padding: 12px 20px; border-bottom: 1px solid var(--line); gap: 12px;
}
.topbar .title { font-weight: 600; }
.stage { flex: 1; display: flex; align-items: center; justify-content: center; background: #000; position: relative; }
.stage #video { width: 100%; height: 100%; max-height: calc(100vh - 54px); object-fit: contain; background: #000; }
/* The longhand offsets before each `inset` are the fallback: `inset` landed in Safari 14.1
   and Chrome 87, the same boundary as the aspect-ratio note below, so on the engines this
   console is deliberately still readable on the shorthand is dropped and the longhand holds
   the element to its positioned parent. Engines that understand `inset` apply it second and
   land on exactly the same box. */
.overlay {
  position: absolute; top: 0; right: 0; bottom: 0; left: 0; inset: 0; display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 14px; text-align: center;
  background: radial-gradient(circle at 50% 40%, #121a24, #05080b);
  padding: 24px;
}
.spinner {
  width: 26px; height: 26px; border-radius: 50%;
  border: 2px solid var(--line); border-top-color: var(--accent);
  animation: spin 0.9s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* ---- brand mark ---- */
.logo { display: block; height: 40px; width: auto; margin: 0 0 26px; opacity: 0.95; }
.topbar .logo { height: 26px; margin: 0; }
.steps { margin: 0 0 20px; padding-left: 22px; color: var(--muted); }
.steps li { margin-bottom: 9px; }
.steps strong { color: var(--text); font-weight: 600; }

/* ---- stat tiles ---- */
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(132px, 1fr)); gap: 10px; }
.stat {
  background: var(--bg); border: 1px solid var(--line);
  border-radius: 8px; padding: 12px 14px;
}
.stat .k {
  font-size: 10.5px; text-transform: uppercase; letter-spacing: 0.09em;
  color: var(--muted); margin-bottom: 5px;
}
.stat .v { font-size: 21px; font-weight: 600; letter-spacing: -0.015em; font-variant-numeric: tabular-nums; }
.stat .v.dim { color: var(--muted); font-weight: 400; font-size: 15px; }
.stat .sub2 { font-size: 11px; color: var(--muted); margin-top: 3px; }

/* ---- channel cards (primary / standby) ---- */
.chan { border: 1px solid var(--line); border-radius: var(--radius); padding: 16px; background: var(--panel); }
/* Same fallback pattern as the pills - without it, the card serving viewers is indistinguishable
   from the idle one on an engine that cannot parse color-mix(). */
.chan.active { border-color: var(--accent); border-color: color-mix(in srgb, var(--accent) 55%, transparent); }
.chan h3 {
  margin: 0 0 2px; font-size: 13px; text-transform: uppercase;
  letter-spacing: 0.09em; color: var(--muted); font-weight: 600;
}
.chan .role { font-size: 12px; color: var(--muted); margin-bottom: 14px; }
.chan.active h3 { color: var(--accent); }

/* ---- admin stream preview ---- */
.preview { background: #000; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; position: relative; aspect-ratio: 16 / 9; }
/* Nothing inside this box contributes height - the idle overlay is absolute and the <video> is
   height:100% - so on Safari <= 14 and Chrome <= 87, which ignore aspect-ratio, it collapsed to a
   sliver and the preview was unusable. Scoped to those engines so it can never fight the ratio. */
@supports not (aspect-ratio: 16 / 9) {
  .preview { min-height: 200px; }
}
.preview video { width: 100%; height: 100%; object-fit: contain; background: #000; display: block; }
.preview .idle {
  position: absolute; top: 0; right: 0; bottom: 0; left: 0; inset: 0; display: flex; align-items: center; justify-content: center;
  text-align: center; padding: 12px; font-size: 13px; color: var(--muted);
  background: linear-gradient(160deg, #0e141b, #05080b);
}
.preview .tag {
  position: absolute; top: 8px; left: 8px; z-index: 2;
  font-size: 10px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase;
  padding: 3px 7px; border-radius: 4px; background: rgba(0,0,0,0.65); color: var(--muted);
}
.preview .tag.on { color: var(--ok); }

/* ---- recovery actions ----
   Button on the left, explanation on the right. The button column is fixed so the four actions
   line up as a list you read top to bottom, which is the order they are meant to be tried in. */
.action {
  display: grid;
  grid-template-columns: 240px 1fr;
  gap: 18px;
  padding: 14px 0;
  border-top: 1px solid var(--line);
  align-items: start;
}
.action:first-of-type { border-top: none; padding-top: 0; }
.action-do button { width: 100%; text-align: left; }
.action-why { display: flex; flex-direction: column; gap: 7px; font-size: 13.5px; line-height: 1.5; }
.action-why .mono { font-size: 12.5px; }
@media (max-width: 820px) {
  .action { grid-template-columns: 1fr; gap: 10px; }
  .action-do button { width: auto; }
}

/* ---- sessions table ---- */
.sessions td { vertical-align: top; font-variant-numeric: tabular-nums; }

/* Copyable shell commands. The MP4 recipe is two lines, so newlines have to survive - without
   pre-wrap the sync and the ffmpeg call render as one unrunnable line. Was scoped to `.sessions`,
   which meant it silently did nothing in the recordings panel. */
.cmd {
  font-family: var(--mono); font-size: 11.5px; color: var(--muted);
  background: var(--bg); border: 1px solid var(--line); border-radius: 6px;
  padding: 6px 8px; margin-top: 6px;
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.archivedrow td { opacity: 0.55; }

/* `p.sub` (0,0,1,1) outranks `.mt` (0,0,1,0), so the utility class was silently doing
   nothing on the join and 404 pages. Match the specificity rather than reaching for
   !important. */
p.sub.mt { margin-top: 22px; }

/* The preview video fades in once the player reports PLAYING, so the first frame arrives
   instead of a black rectangle snapping into place. The idle overlay sits on top until then. */
.preview video { opacity: 0; transition: opacity 220ms ease-out; }
.preview video.ready { opacity: 1; }
.preview .idle { transition: opacity 160ms ease-out; }
.preview .idle.hidden { display: none !important; }

/* ---- standby screen (shown until the session is live) ---- */
.standby { position: absolute; top: 0; right: 0; bottom: 0; left: 0; inset: 0; overflow: hidden; background: var(--bg); }
.standby.hidden { display: none !important; }
.standbyloop {
  position: absolute; top: 0; right: 0; bottom: 0; left: 0; inset: 0; width: 100%; height: 100%;
  object-fit: cover; pointer-events: none; opacity: 1;
}
.standbybody {
  position: absolute; top: 0; right: 0; bottom: 0; left: 0; inset: 0; z-index: 2;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 10px; text-align: center; padding: 32px;
  /* The loop is deliberately soft, but text still needs guaranteed contrast over it. */
  background: radial-gradient(ellipse 70% 55% at 50% 50%, rgba(6,9,13,0.62) 0%, rgba(6,9,13,0.34) 60%, rgba(6,9,13,0.10) 100%);
}
.standbykicker {
  font-size: clamp(24px, 4.2vw, 40px); font-weight: 600; letter-spacing: -0.02em;
  text-shadow: 0 2px 18px rgba(0,0,0,0.55);
}
.standbytitle {
  font-size: clamp(14px, 1.7vw, 18px); color: var(--text); opacity: 0.9;
  max-width: 24em; text-shadow: 0 1px 12px rgba(0,0,0,0.6);
}
.standbymsg {
  font-size: clamp(13px, 1.5vw, 16px); color: var(--muted);
  max-width: 26em; text-shadow: 0 1px 12px rgba(0,0,0,0.6);
}
.standbyfoot {
  display: flex; align-items: center; gap: 10px; margin-top: 14px;
  font-size: 12.5px; color: var(--muted);
}
.standbyfoot .spinner { width: 15px; height: 15px; border-width: 2px; }

/* The loop is decorative. Anyone who has asked for less motion gets the poster frame instead. */
@media (prefers-reduced-motion: reduce) {
  .standbyloop { display: none; }
  .standby { background: var(--bg) url('/standby-poster.jpg') center/cover no-repeat; }
}

/* Neither standby source could be decoded (a Chromium build without H.264 and without VP9).
   Fall back to the poster still rather than showing black. */
.standby.noloop .standbyloop { display: none; }
.standby.noloop { background: var(--bg) url('/standby-poster.jpg') center/cover no-repeat; }

/* ---- encoder specs: the info affordance on a channel card ---- */
/*
 * Only rendered while something is actually streaming to that slot, so it doubles as a signal:
 * an "i" next to the pill means IVS has a session it can describe. It is a real <button> rather
 * than a styled span so it lands in the tab order and answers to Enter and Space for free.
 */
.infobtn {
  width: 20px; height: 20px; flex: 0 0 20px; padding: 0;
  border-radius: 50%; border: 1px solid var(--line);
  background: var(--panel-2); color: var(--muted);
  font: 600 12px/1 var(--mono); cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
}
.infobtn:hover { color: var(--text); border-color: var(--accent); }
.infobtn:focus { outline: 2px solid var(--accent); outline-offset: 2px; }
.infobtn:focus:not(:focus-visible) { outline: none; }
.infobtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---- modal ---- */
/*
 * A custom overlay, not window.confirm() or <dialog>. window.* dialogs block the event loop, and
 * during an event that freezes the 5-second poll behind the dialog - the ingest pill stops moving
 * and the operator cannot tell whether the stream died or the page did. <dialog> would be fine on
 * current browsers but its ::backdrop and default sizing are inconsistent on the older Safari and
 * Chrome builds this console is deliberately still readable on.
 */
.modalback {
  position: fixed; top: 0; right: 0; bottom: 0; left: 0; inset: 0; z-index: 60;
  background: rgba(4, 7, 11, 0.72);
  display: flex; align-items: flex-start; justify-content: center;
  padding: 6vh 16px 24px; overflow-y: auto;
}
.modal {
  background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius);
  width: 100%; max-width: 640px; box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5);
}
.modal-head {
  display: flex; justify-content: space-between; align-items: flex-start; gap: 12px;
  padding: 18px 20px 14px; border-bottom: 1px solid var(--line);
}
.modal-head h2 { margin: 0 0 3px; }
.modal-body { padding: 18px 20px 20px; }
.modal-x {
  flex: 0 0 auto; background: none; border: none; color: var(--muted);
  font-size: 22px; line-height: 1; padding: 2px 6px; cursor: pointer; border-radius: 6px;
}
.modal-x:hover { color: var(--text); }
.modal-x:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.modal-x:focus:not(:focus-visible) { outline: none; }
.modal-x:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* ---- key/value spec rows ---- */
.speclabel {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.09em;
  color: var(--muted); margin: 20px 0 8px; font-weight: 600;
}
.speclabel:first-child { margin-top: 0; }
.kv { display: grid; grid-template-columns: minmax(120px, 34%) 1fr; gap: 1px 14px; font-size: 13.5px; }
.kv dt { color: var(--muted); padding: 5px 0; }
.kv dd { margin: 0; padding: 5px 0; font-variant-numeric: tabular-nums; }
.kv dd.mono { font-size: 12.5px; }
/* Anything IVS simply does not report reads as absent, not as zero or broken. */
.kv dd.na { color: var(--muted); font-style: italic; font-variant-numeric: normal; }

/* ---- check rows ---- */
.check {
  display: flex; gap: 10px; padding: 9px 0; border-top: 1px solid var(--line);
  font-size: 13.5px; line-height: 1.5;
}
.check:first-of-type { border-top: none; }
.check .mark {
  flex: 0 0 auto; width: 16px; height: 16px; margin-top: 3px; border-radius: 50%;
  display: inline-flex; align-items: center; justify-content: center;
  font: 700 10px/1 var(--mono); color: var(--bg);
}
.check.ok   .mark { background: var(--ok); }
.check.warn .mark { background: var(--warn); }
.check.bad  .mark { background: var(--bad); }
.check .ct { font-weight: 600; }
.check.warn .ct { color: var(--warn); }
.check.bad  .ct { color: var(--bad); }
.check .cd { color: var(--muted); }
