:root {
  --bg: #0d0f12;
  --fg: #e8eaed;
  --dim: #9aa0a6;
  --line: #23262b;
  --pass: #3ea76a;
  --fail: #d1495b;
  --slate: #0b3d91; /* the blue. it is a decision, not an error. */
}
* { box-sizing: border-box; }
body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font: 15px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
}
header {
  padding: 12px 16px;
  border-bottom: 1px solid var(--line);
  display: flex;
  gap: 16px;
  align-items: center;
}
header h1 { font-size: 15px; margin: 0; letter-spacing: .06em; text-transform: uppercase; }
header .sub { color: var(--dim); font-size: 13px; }
main { padding: 16px; }
button {
  font: inherit; padding: 10px 16px; border-radius: 8px; border: 1px solid var(--line);
  background: #1b1e23; color: var(--fg); cursor: pointer;
}
button:disabled { opacity: .45; cursor: default; }
input {
  font: inherit; padding: 10px 12px; border-radius: 8px;
  border: 1px solid var(--line); background: #15181c; color: var(--fg); width: 100%;
}
label { display: block; margin: 12px 0 4px; color: var(--dim); font-size: 13px; }

/* --- viewer ------------------------------------------------------------- */
.stage { position: relative; width: 100%; aspect-ratio: 16/9; background: #000; overflow: hidden; }
.stage video { width: 100%; height: 100%; object-fit: contain; display: block; }
/*
 * Four states, not two. `live` is the only one that shows the BROADCAST, and it
 * is now the only one the page can reach by having seen a frame. Wave 5, D1.
 *
 * Scoped to `#air` on 2026-08-29. As `.stage video` it also hid the publisher's
 * own camera, so anyone publishing while the slate was up lost the one picture
 * they can actually act on: their own framing, which is what the whole
 * two-picture layout exists to show them.
 */
body:not([data-state="live"]) #air { display: none; }
#slate {
  position: absolute; inset: 0; display: none;
  background: var(--slate); color: #fff;
  flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 24px;
}
body[data-state="slate"] #slate { display: flex; }
body[data-state="wait"] #slate { display: flex; }

/*
 * Deliberately NOT var(--slate). The blue is a broadcast decision and this is
 * an instrument failure; giving them the same colour would merge the two things
 * this page exists to keep apart.
 */
#dark {
  position: absolute; inset: 0; display: none;
  background: #17191d; color: var(--fg);
  flex-direction: column; align-items: center; justify-content: center;
  text-align: center; padding: 24px; border: 1px dashed var(--dim);
}
body[data-state="dark"] #dark { display: flex; }
body[data-state="connecting"] #dark { display: flex; }
body[data-state="connecting"] #dark .mark::after { content: ' ...'; }
body[data-state="connecting"] #dark .why { visibility: hidden; }
#dark .mark { font-size: clamp(16px, 3vw, 26px); font-weight: 600; letter-spacing: .03em; color: var(--dim); }
#dark .why { margin-top: 10px; color: var(--dim); font-size: clamp(13px, 1.6vw, 16px); max-width: 34em; }
#slate .mark { font-size: clamp(20px, 4vw, 40px); font-weight: 600; letter-spacing: .04em; }
#slate .why { margin-top: 10px; opacity: .85; font-size: clamp(13px, 1.6vw, 17px); }
#slate .cta {
  margin-top: 28px; padding: 12px 20px; border: 1px solid rgba(255,255,255,.5);
  border-radius: 8px; color: #fff; text-decoration: none;
}
.footer { padding: 10px 16px; color: var(--dim); font-size: 13px; display: flex; gap: 16px; flex-wrap: wrap; }
.footer #delay { margin-left: auto; }

/* --- publisher ---------------------------------------------------------- */
#preview { width: 100%; border-radius: 10px; background: #000; aspect-ratio: 16/9; object-fit: cover; }
.verdict { margin: 14px 0 6px; padding: 12px; border-radius: 8px; font-weight: 600; text-align: center; }
.verdict.pass { background: rgba(62,167,106,.15); color: var(--pass); border: 1px solid var(--pass); }
.verdict.fail { background: rgba(209,73,91,.12); color: var(--fail); border: 1px solid var(--fail); }
/* Not a ruling. The page cannot reach the switchboard, so it has nothing to
   report either way, and must not keep showing the last thing it heard. */
.verdict.unknown { background: rgba(154,160,166,.12); color: var(--dim); border: 1px dashed var(--dim); }
.offline { background: rgba(154,160,166,.12); color: var(--dim); border-bottom: 1px dashed var(--dim); padding: 8px 12px; text-align: center; }
body[data-onair="true"] .verdict { background: var(--fail); color: #fff; }
/* The camera picker and the line that says which one is live. D10. */
.camera-row { display: flex; align-items: center; gap: 10px; margin: 12px 0 4px; }
.camera-row label { margin: 0; white-space: nowrap; }
.camera-row select {
  font: inherit; flex: 1; min-width: 0; padding: 8px 10px; border-radius: 8px;
  border: 1px solid var(--line); background: #15181c; color: var(--fg);
}
#camera-active { color: var(--dim); font-size: 13px; }

#reasons { margin: 0; padding-left: 20px; color: var(--dim); font-size: 13px; }
#status { color: var(--dim); font-size: 13px; margin-top: 10px; }

/* --- the three artifacts a publisher receives ----------------------------- */
/*
 * Different LEFT RULE, different label colour, different weight. The acceptance
 * criterion is that ruling and coaching are separable "without reading them",
 * so the separation has to survive being skimmed: a coach's suggestion set in
 * the same weight as a rubric ruling teaches that advice is binding and rulings
 * are negotiable, and both lessons are wrong.
 */
#feedback { margin-top: 18px; border-top: 1px solid var(--line); padding-top: 12px; }
.fb-title { font-size: 13px; color: var(--dim); text-transform: uppercase; letter-spacing: .08em; margin: 0 0 10px; }
.artifact { border-left: 3px solid var(--line); padding: 8px 0 8px 12px; margin: 0 0 14px; }
.artifact h3 { margin: 0 0 6px; font-size: 14px; }
.artifact ul { margin: 0; padding-left: 0; list-style: none; font-size: 13px; }
.artifact li { display: flex; gap: 8px; flex-wrap: wrap; padding: 2px 0; }

.artifact.ruling { border-left-color: var(--fail); }
.artifact.ruling h3 { color: var(--fg); }
.gate-name { color: var(--dim); min-width: 84px; text-transform: uppercase; font-size: 11px; letter-spacing: .06em; padding-top: 2px; }
.gate-line { flex: 1; min-width: 180px; }
.gate-ok .gate-line { color: var(--pass); }
.gate-no .gate-line { color: var(--fail); }
.gate-by { color: var(--dim); font-size: 11px; }
.appeal { color: var(--dim); font-size: 12px; margin: 8px 0 0; }

.artifact.fate { border-left-color: var(--slate); }
.fate-line { margin: 0; font-size: 13px; }
.fate-context { color: var(--dim); font-size: 12px; margin: 6px 0 0; }

/* Deliberately the quietest of the three. It is advice, not a ruling. */
.artifact.coaching { border-left-color: var(--line); }
.artifact.coaching h3 { color: var(--dim); font-size: 12px; text-transform: uppercase; letter-spacing: .06em; }
.coach-quiet { color: var(--dim); font-size: 12px; font-style: italic; margin: 0; }
.coach-act, .coach-hold { margin: 0; font-size: 13px; color: var(--fg); }
.coach-about { color: var(--dim); font-size: 11px; }

/* --- the director's per-tile record and editorial calls ------------------- */
/*
 * `good` and `bad` are the editor's own judgment and are deliberately quiet:
 * they are not the Take button, they commit nothing to air, and there is no
 * control anywhere on this page that accepts the machine's proposal in bulk.
 * Proposal 11.3 keeps the editorial agent off the commit path, and a UI control
 * is a commit path, so the absence is the feature.
 */
.tile-controls { display: flex; gap: 6px; align-items: center; }
.tile-controls button { flex: 0 0 auto; }
.tile-controls button:first-child { flex: 1; }
.label-good, .label-bad { font-size: 12px; padding: 6px 10px; opacity: .8; }
.label-good:hover { border-color: var(--pass); color: var(--pass); opacity: 1; }
.label-bad:hover { border-color: var(--fail); color: var(--fail); opacity: 1; }
.fate { color: var(--dim); font-size: 11px; font-style: italic; }

label.inline { display: flex; align-items: center; gap: 8px; margin: 12px 0 4px; }
label.inline input { width: auto; }

/* --- director ----------------------------------------------------------- */
.onair { padding: 10px 16px; font-weight: 600; letter-spacing: .04em; }
.onair.live { background: var(--fail); color: #fff; }
.onair.slate { background: var(--slate); color: #fff; }
.controls { display: flex; gap: 14px; align-items: center; padding: 12px 16px; border-bottom: 1px solid var(--line); }
#grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 14px; }
.tile { border: 1px solid var(--line); border-radius: 10px; overflow: hidden; background: #14171b; }
.tile.live { border-color: var(--fail); box-shadow: 0 0 0 2px rgba(209,73,91,.35); }
.tile.failing { opacity: .6; }
.tile video { width: 100%; aspect-ratio: 16/9; background: #000; object-fit: cover; display: block; }
.tile .meta { padding: 10px; display: grid; gap: 3px; font-size: 13px; }
.tile .op, .tile .bits { color: var(--dim); }
.tile .num { font-weight: 700; }
.tile .fails { color: var(--fail); font-size: 12px; }
.tile button { width: calc(100% - 20px); margin: 0 10px 10px; }

/* --- audience rating ----------------------------------------------------- */
.rate { display: flex; gap: 10px; align-items: center; padding: 12px 16px; flex-wrap: wrap; }
.rate .ask { color: var(--dim); font-size: 13px; }
.rate button { padding: 8px 18px; }
.rate button.chosen { border-color: var(--fg); }
.rate .tally { color: var(--dim); font-size: 12px; margin-left: auto; }
/* Nothing to rate unless a picture is actually on this screen. */
body:not([data-state="live"]) .rate { display: none; }

/* --- delay audit (director only) ----------------------------------------- */
.audit { padding: 8px 16px; font-size: 13px; border-bottom: 1px solid var(--line); }
.audit.ok { color: var(--dim); }
.audit.bad { background: var(--fail); color: #fff; font-weight: 600; }
.tile .gates { color: var(--dim); font-size: 11px; font-family: ui-monospace, monospace; }

/* --- publisher feedback --------------------------------------------------- */
#reasons { list-style: none; padding: 0; margin: 12px 0 0; }
#reasons li { padding: 10px 12px; border-radius: 8px; background: #15181c; margin-bottom: 8px; }
#reasons li.ok { background: rgba(62,167,106,.12); color: var(--pass); }
#reasons .why { display: block; color: var(--fail); font-size: 13px; }
#reasons .fix { display: block; color: var(--fg); font-size: 14px; margin-top: 4px; }
#reasons li.observed { background: #14181f; border-left: 3px solid var(--dim); }
#reasons li.observed .why { color: var(--dim); }

/* --- the merged public page (wave 5, call 2) --------------------------------
 *
 * Full bleed. Nothing here reserves a band of the screen except the video, and
 * every control floats over it inside the safe area, because the publisher's
 * form put its primary action seventh in a vertical stack and that action fell
 * below the fold on any window under 600px.
 *
 * Measured before this replaced it, at 375 wide: Go live sat at 635px, the
 * "locking the phone ends your feed" warning at 664px, and a real phone loses
 * 110-150px of an 812px screen to browser chrome.
 */
body[data-state] .stage {
  position: fixed; inset: 0; width: 100%; height: 100dvh;
  aspect-ratio: auto; background: #000; overflow: hidden;
}
/*
 * The swap, which is the whole point of two pictures on one screen.
 *
 * This was a placeholder comment in an empty rule until 2026-08-29: `data-main`
 * flipped, `mainView()` returned the right string, all 25 of its assertions
 * passed, and NOTHING MOVED. The decision was tested and the rendering was not,
 * which is issue 010 exactly. Measured in a browser: both rects identical
 * before and after the state changed.
 *
 * Driven by class, not by DOM position: the two videos are siblings and stay
 * put. Reparenting a <video> can interrupt playback, and a swap that costs a
 * frame is one nobody uses twice.
 */
/*
 * The swap trades two PANES, not two videos.
 *
 * Trading only the <video> elements shipped on 2026-08-29 and looked correct in
 * a computed-style check: `#air` and `#me` did exchange sizes. What it missed is
 * that the slate is not an overlay on the page, it is what the broadcast
 * currently IS. It stayed at `inset: 0; z-index: 1` and painted straight over a
 * publisher's own camera whenever the broadcast was slated, which is the state a
 * publisher spends most of their time in.
 *
 * So the broadcast's picture and both of its stand-ins live in one pane and move
 * together. Reported by an operator looking at a blue screen where their camera
 * should have been, after a test of mine said the swap worked.
 */
.pane { position: absolute; display: block; background: #000; overflow: hidden; }
#broadcast > video, #broadcast > #slate, #broadcast > #dark {
  position: absolute; inset: 0; width: 100%; height: 100%;
}
#air, #me { object-fit: cover; }

/* The large pane, whichever it currently is. */
body[data-main="broadcast"] #broadcast,
body[data-main="camera"] #me {
  inset: 0; width: 100%; height: 100%; border-radius: 0; z-index: 0; box-shadow: none;
}

/* The small one, in the thumb-reachable corner. */
body[data-main="broadcast"] #me,
body[data-main="camera"] #broadcast {
  left: auto; top: auto;
  right: calc(12px + env(safe-area-inset-right));
  bottom: calc(184px + env(safe-area-inset-bottom));
  width: 108px; height: 152px; border-radius: 14px; z-index: 4;
  box-shadow: 0 6px 24px rgba(0,0,0,.55);
}
/* The slate's type has to shrink with its pane, or it overflows the tile. */
body[data-main="camera"] #slate .mark, body[data-main="camera"] #dark .mark { font-size: 11px; }
body[data-main="camera"] #slate .why, body[data-main="camera"] #dark .why { display: none; }

/* Not publishing: there is no second pane, so the broadcast is simply the page. */
body[data-publish="watching"] #me { display: none; }
body[data-publish="watching"] #broadcast {
  inset: 0; width: 100%; height: 100%; border-radius: 0; z-index: 0; box-shadow: none;
}

/* The tap target, exactly over the small picture. */
#tile {
  position: absolute;
  right: calc(12px + env(safe-area-inset-right));
  bottom: calc(184px + env(safe-area-inset-bottom));
  width: 108px; height: 152px; border-radius: 14px; overflow: hidden;
  z-index: 5; cursor: pointer;
}

.tile-swap {
  position: absolute; inset: auto 0 0 0; border: 0; border-radius: 0;
  background: rgba(0,0,0,.45); color: #fff; font-size: 11px; padding: 4px 0;
}

/*
 * Scrims, not glass. §7.1 correction 1: a live backdrop-filter over video on a
 * phone that is simultaneously encoding WebRTC degrades fps and thermalHint,
 * which are gate 0 metrics, so the interface would be the reason the feed was
 * refused. Gate G6.
 */
.scrim { position: absolute; left: 0; right: 0; pointer-events: none; z-index: 2; }
.scrim-top { top: 0; height: 140px; background: linear-gradient(to bottom, rgba(0,0,0,.62), transparent); }
.scrim-bottom { bottom: 0; height: 300px; background: linear-gradient(to top, rgba(0,0,0,.72), transparent); }

.pill {
  position: absolute; z-index: 3;
  top: calc(12px + env(safe-area-inset-top)); left: calc(14px + env(safe-area-inset-left));
  font-size: 12px; letter-spacing: .08em; text-transform: uppercase; color: #fff;
}
.credit {
  position: absolute; z-index: 3;
  left: calc(14px + env(safe-area-inset-left)); right: 92px;
  bottom: calc(96px + env(safe-area-inset-bottom)); color: #fff;
}
#credit-line { font-size: 16px; font-weight: 600; }
.credit-sub { font-size: 13px; color: rgba(255,255,255,.72); margin-top: 2px; }
.chevron {
  margin-top: 8px; background: none; border: 0; padding: 0;
  color: rgba(255,255,255,.72); font-size: 13px; text-decoration: underline;
}

/*
 * The thumb arc. Roughly 75% of phone interactions are single-thumbed and the
 * top 40% of the screen is a dead zone, so every control anyone uses is here.
 */
.rail {
  position: absolute; z-index: 3;
  right: calc(10px + env(safe-area-inset-right));
  bottom: calc(184px + env(safe-area-inset-bottom));
  display: none; flex-direction: column; gap: 14px; align-items: center;
}
body[data-publish="watching"] .rail { display: flex; }
/*
 * §7 rule 1: every persistent element is a video, an action available NOW, or
 * the identity of what you are watching. A rating control on the slate is none
 * of those: there is no picture to rate, and a permanently disabled button is
 * clutter that teaches people the control is broken.
 */
body:not([data-state="live"]) #rate-good,
body:not([data-state="live"]) #rate-bad { display: none; }
.rail-btn {
  background: none; border: 0; padding: 0; color: #fff;
  display: flex; flex-direction: column; align-items: center; gap: 2px; min-width: 56px;
}
.rail-btn:disabled { opacity: .35; }
.rail-btn .glyph { font-size: 22px; line-height: 1; }
.rail-label { font-size: 11px; letter-spacing: .04em; }
.rail-btn.chosen { color: #fff; text-shadow: 0 0 10px rgba(255,255,255,.9); }

/* Exactly one high-contrast control per state. Everything else is a ghost. */
.primary {
  position: absolute; z-index: 3;
  left: calc(16px + env(safe-area-inset-left)); right: calc(16px + env(safe-area-inset-right));
  bottom: calc(28px + env(safe-area-inset-bottom));
  padding: 16px; border: 0; border-radius: 999px;
  background: #fff; color: #101215; font-size: 16px; font-weight: 600;
}
.primary[data-kind="stop"] { background: var(--fail); color: #fff; }
.note {
  position: absolute; z-index: 3;
  left: calc(16px + env(safe-area-inset-left)); right: calc(16px + env(safe-area-inset-right));
  bottom: calc(4px + env(safe-area-inset-bottom));
  color: rgba(255,255,255,.72); font-size: 12px; text-align: center; min-height: 16px;
}

/* Slate and dark fill the whole stage now, not a 16:9 letterbox in a page. */
#slate, #dark { z-index: 1; }
#slate .mark { font-size: clamp(28px, 9vw, 64px); }

/*
 * The sheet: a partial-height overlay on a broadcast that keeps playing, which
 * is the comments-sheet idiom rather than the feed swipe. Scrolling away from a
 * live stream means leaving the broadcast.
 */
.sheet {
  position: fixed; z-index: 5; left: 0; right: 0; bottom: 0;
  max-height: 62dvh; overflow-y: auto;
  background: #0f1114; border-top: 1px solid var(--line);
  border-radius: 16px 16px 0 0; padding: 16px 16px calc(24px + env(safe-area-inset-bottom));
}
.sheet-close { float: right; background: none; border: 0; color: var(--dim); font-size: 13px; }
.sheet-title { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: var(--dim); margin: 0 0 8px; }
.sheet-line { margin: 0 0 6px; font-size: 14px; }
/* The account links in every header. Pushed right so the page's own title keeps
   the position a reader looks at first. */
header nav { margin-left: auto; display: flex; gap: 14px; align-items: center; font-size: 13px; }
header nav a { color: var(--dim); text-decoration: none; border-bottom: 1px solid transparent; }
header nav a:hover { color: var(--fg); border-bottom-color: var(--dim); }
header nav .nav-you { color: var(--dim); }

/* The account page: a form, which nothing else here is. */
/* `label`, `label.inline` and the base `input` rule are all defined above and
   already correct: a second copy of a rule is how two files disagreed about one
   number three times in this repository. The account form reuses them. */
label.inline { margin-top: 14px; color: var(--fg); }
.row { display: flex; gap: 10px; margin-top: 16px; }
button.secondary { background: transparent; border: 1px solid var(--line); color: var(--dim); }
button.secondary:hover { color: var(--fg); }
.mono { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; }
table { width: 100%; border-collapse: collapse; margin-top: 10px; font-size: 13px; }
th { text-align: left; color: var(--dim); font-weight: 500; padding: 6px 8px 6px 0; }
td { padding: 6px 8px 6px 0; border-top: 1px solid var(--line); }

/* The account form. Narrow because a full-width login on a monitor reads as a
   page that lost its content. */
.card.narrow { max-width: 460px; }
.card h2 { margin: 0 0 6px; font-size: 16px; }
.small { font-size: 12px; }
button.wide { width: 100%; margin-top: 16px; padding: 10px; }
.switch { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--line); font-size: 13px; }
.switch a { color: var(--fg); }
.msg { min-height: 18px; margin: 10px 0 0; font-size: 13px; color: var(--dim); }
.msg.bad { color: var(--fail); }

/* The account link on the public page. Mirrors the state pill on the other
   side, inside the same safe area, and reserves no band of its own. */
.account-corner {
  position: absolute; z-index: 3;
  top: calc(12px + env(safe-area-inset-top)); right: calc(14px + env(safe-area-inset-right));
  display: flex; gap: 12px; align-items: center; font-size: 12px;
}
.account-corner a { color: rgba(255,255,255,.72); text-decoration: none; }
.account-corner a:hover { color: #fff; }
.account-corner .nav-you { color: rgba(255,255,255,.55); }

/* --- the account nav, at phone width -----------------------------------------
   Written desktop-first and it showed: at 375px the header nav wrapped the
   handle onto three lines, clipped "settings" and pushed "sign out" off the
   screen entirely, and on the broadcast page it collided with the state pill.

   A phone is the publisher's context, so this is the case that matters most.
   The handle is the first thing to go: it is identity rather than navigation,
   and the person holding the phone already knows who they are. */
@media (max-width: 620px) {
  header { flex-wrap: wrap; row-gap: 4px; }
  header nav { gap: 10px; font-size: 12px; }
  header nav .nav-handle { display: none; }
}

/* On the broadcast page the nav shares the top edge with the state pill, which
   is the one label that must never be crowded: it is how a viewer knows whether
   anything is on air. Below 620px the account links collapse to one word. */
@media (max-width: 620px) {
  .account-corner { gap: 10px; font-size: 11px; }
  .account-corner .nav-handle, .account-corner .nav-you { display: none; }
}

/* Links inside a card. Without this they render as the browser default, which
   on a dark page is an unreadable blue: "View your profile" on the account form
   was the one link in the product that looked broken. */
main a { color: var(--fg); text-decoration: none; border-bottom: 1px solid var(--dim); }
main a:hover { border-bottom-color: var(--fg); }

/* An input with a control inside it, for show/hide password. */
.with-affix { position: relative; display: flex; align-items: center; }
.with-affix input { padding-right: 72px; }
.affix {
  position: absolute; right: 6px; padding: 6px 10px; font-size: 12px;
  background: transparent; border: 1px solid var(--line); color: var(--dim);
  border-radius: 6px; cursor: pointer;
}
.affix:hover { color: var(--fg); }
