/* ===========================================================================
   STREAM — the window, the state, the facts, the paywall gate, the player.

   Appended to site.css's language rather than a second stylesheet, because
   these are the same materials: the same plate, the same flame, the same rule.
   A second file would have let them drift.

   The one thing that IS new here is the state machine, and it is drawn rather
   than written: a live broadcast gets a slow ring around the flame and the
   recording dot, an off broadcast gets a cold wick, and a check gets a scan
   line travelling across the frame. Nothing else on the site moves on its own,
   and neither does anything here except to answer the question "is it on".
   =========================================================================== */

/* ---------------------------------------------------------------------------
   THE STATE
   --------------------------------------------------------------------------- */

.state {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--face-mono);
  font-size: var(--t-mono);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin: 0;
}

.state--sm {
  letter-spacing: 0.14em;
}

.state[data-on="true"] {
  color: var(--flame);
}

/* The ring. It is the only ornament on the site and it exists because the
   question it answers — is this happening now — is the one thing a visitor
   came to find out. */
.state[data-on="true"] .flame::before {
  animation: flicker 4s var(--ease) infinite;
}

.state[data-on="true"] .flame::after {
  animation: ring 3.2s var(--ease) infinite;
}

.flame::after {
  opacity: 0;
}

@keyframes ring {
  0% {
    opacity: 0.5;
    transform: scale(0.72);
  }
  70%,
  100% {
    opacity: 0;
    transform: scale(1.5);
  }
}

/* Checking: the state text itself breathes, so the difference between "we are
   asking" and "the answer is no" is visible before the words resolve. */
.state[data-state-mode="checking"] .state__text {
  animation: breathe 1.5s var(--ease) infinite;
}

@keyframes breathe {
  0%,
  100% {
    opacity: 0.45;
  }
  50% {
    opacity: 1;
  }
}

/* ---------------------------------------------------------------------------
   THE STAGE
   A plate at 16:9 with a scrim, holding the poster until the live feed is
   confirmed. The poster is the real camera frame, so the window is never an
   empty black rectangle and the page never changes height when the video
   arrives.
   --------------------------------------------------------------------------- */

.stage {
  position: relative;
  aspect-ratio: 16 / 9;
  max-block-size: min(62vh, 36rem);
  background: var(--ground-deep);
  border: 1px solid var(--rule);
  border-radius: var(--plate-radius);
  overflow: hidden;
}

.stage__frame {
  position: absolute;
  inset: 0;
}

.stage__poster,
.stage__video {
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  filter: saturate(0.84) contrast(1.03) brightness(0.72);
}

/* The video is ALWAYS laid out and rendered, sitting behind the poster.
   It was `display: none` until the picture was confirmed playing, which is a
   chicken-and-egg: Chromium will not start a muted video that is not being
   rendered, so `play()` rejected, so `data-playing` never became true, so the
   video stayed hidden. Measured: readyState 4 and 11.7s buffered, paused at
   0.00.

   So: video always present at z-index 0, poster on top at z-index 1, and the
   poster is made invisible the moment the picture is actually playing. If play
   never happens the poster is simply still there, which is correct — the
   broadcast can be live while this browser declines to show it. */
.stage__video {
  position: absolute;
  inset: 0;
  z-index: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  filter: saturate(0.9) contrast(1.02) brightness(0.94);
}

.stage__poster {
  position: relative;
  z-index: 1;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  filter: saturate(0.84) contrast(1.03) brightness(0.72);
  transition: opacity var(--t-slow) var(--ease);
}

.stage[data-playing="true"] .stage__poster {
  opacity: 0;
  pointer-events: none;
}

/* The scrim.

   Its job is not to darken the picture for looks. It is to make text legible
   over VIDEO, which is arbitrary content: the moment the feed is live the
   backdrop is whatever a camera pointed at a grave happens to show, and a
   flat design cannot rely on that being dark. Screenshot evidence: a grey
   "08:00 — 18:00" over a bright colour-bar test pattern, effectively invisible.

   So the top and bottom bands are held near-opaque out to where the controls
   are, and only the middle — where there is no text — is left open. The
   controls sit in the top ~13% and the bottom ~15%. */
.stage::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    to bottom,
    rgba(7, 6, 5, 0.82) 0%,
    rgba(7, 6, 5, 0.7) 16%,
    rgba(7, 6, 5, 0.3) 34%,
    rgba(7, 6, 5, 0.24) 55%,
    rgba(7, 6, 5, 0.62) 78%,
    rgba(7, 6, 5, 0.86) 100%
  );
  transition: opacity var(--t-slow) var(--ease);
}

.stage[data-stage="live"]::after {
  opacity: 0.94;
}

/* Belt and braces. The scrim handles most of it; a tight shadow handles the
   case where the video is bright enough to defeat the scrim anyway, which is a
   thing that happens once a week. */
.stage__top,
.stage__bottom,
.stage__gate {
  text-shadow: 0 1px 3px rgba(7, 6, 5, 0.95), 0 0 12px rgba(7, 6, 5, 0.8);
}

.stage__top .meta {
  color: var(--ink-2);
}

.stage__top,
.stage__gate,
.stage__bottom {
  position: absolute;
  z-index: 2;
  inset-inline: clamp(0.9rem, 2.4vw, 1.6rem);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
  pointer-events: none;
}

.stage__top {
  inset-block-start: clamp(0.9rem, 2.4vw, 1.4rem);
}

.stage__bottom {
  inset-block-end: clamp(0.9rem, 2.4vw, 1.4rem);
}

.stage__gate {
  inset-block-start: 50%;
  translate: 0 -50%;
  flex-direction: column;
  align-items: center;
  text-align: center;
  pointer-events: auto;
}

.stage__gate[hidden] {
  display: none;
}

.stage__note {
  font-family: var(--face-ui);
  font-size: var(--t-small);
  line-height: 1.6;
  color: var(--ink-2);
  max-inline-size: 44ch;
  margin: 0 0 var(--s3);
  text-shadow: 0 1px 12px rgba(7, 6, 5, 0.9);
}

.btn--lg {
  padding: 1.05rem 2rem;
  min-block-size: 3.4rem;
  font-size: 0.95rem;
}

/* The scan line while the manifest is being checked. It is the site saying
   "asking", in the site's own material. */
.stage[data-stage="checking"] .stage__frame::before {
  content: "";
  position: absolute;
  z-index: 1;
  inset-inline: 0;
  block-size: 28%;
  background: linear-gradient(
    to bottom,
    rgba(232, 163, 61, 0) 0%,
    rgba(232, 163, 61, 0.09) 60%,
    rgba(232, 163, 61, 0.16) 100%
  );
  animation: scan 1.9s var(--ease) infinite;
}

@keyframes scan {
  0% {
    transform: translateY(-100%);
  }
  100% {
    transform: translateY(460%);
  }
}

/* When there is no broadcast the gate is not hidden — it is replaced by the
   reason. An empty window with a disabled button is the worst of both. */
.stage[data-stage="off"] .stage__gate {
  opacity: 1;
}

@media (max-width: 40rem) {
  .stage {
    aspect-ratio: 4 / 3;
  }
  .stage__note {
    font-size: var(--t-micro);
    margin-bottom: var(--s2);
  }
}

/* ---------------------------------------------------------------------------
   THE FACTS
   Four items, each a key in Plex Mono and a value in the text face. They are a
   table, not cards: hairlines between, no shadows, no boxes.
   --------------------------------------------------------------------------- */

.facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr));
  gap: 0;
  border-top: 1px solid var(--rule);
}

.fact {
  padding: 0.9rem var(--s3) 0.9rem 0;
  border-bottom: 1px solid var(--rule);
  border-inline-end: 1px solid var(--rule);
}

.fact[hidden] {
  display: none;
}

.facts > .fact:last-child {
  border-inline-end: 0;
}

.fact__k {
  font-family: var(--face-ui);
  font-size: var(--t-micro);
  color: var(--ink-3);
  margin: 0 0 0.3rem;
}

.fact__v {
  font-family: var(--face-mono);
  font-size: 1.05rem;
  letter-spacing: 0.02em;
  color: var(--ink);
  margin: 0;
  font-variant-numeric: tabular-nums;
}

.fact[data-live="true"] .fact__v {
  color: var(--flame);
}

@media (max-width: 40rem) {
  .facts {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .fact {
    border-inline-end: 0;
  }
}

/* ---------------------------------------------------------------------------
   THE GATE
   The paywall panel. It is not a modal: it sits under the window, so the
   window itself stays visible while a visitor reads what stands between them
   and it. That is the honest arrangement — the thing is right there.
   --------------------------------------------------------------------------- */

.gate {
  margin-top: var(--s3);
  border-top: 2px solid var(--flame-deep);
  padding-top: var(--s3);
  max-inline-size: 54ch;
}

.gate[hidden] {
  display: none;
}

.gate__title {
  font-family: var(--face-display);
  font-size: var(--t-2);
  line-height: 1.2;
  margin: 0 0 0.6rem;
}

.gate__body {
  font-family: var(--face-ui);
  font-size: var(--t-micro);
  line-height: 1.68;
  color: var(--ink-2);
  margin: 0 0 var(--s3);
}

.gate__amount {
  font-family: var(--face-mono);
  font-size: 1.6rem;
  color: var(--flame);
  margin: 0 0 var(--s2);
}

.gate__amount span {
  font-size: 0.8rem;
  color: var(--ink-3);
  margin-inline-start: 0.4rem;
}

/* ---------------------------------------------------------------------------
   THE PLAYER
   Its own window: fixed, full-viewport, above everything, with the page dimmed
   behind it. Focus is trapped and returned. It is a dialog, so a screen reader
   announces it as one and Escape closes it.
   --------------------------------------------------------------------------- */

.player {
  position: fixed;
  inset: 0;
  z-index: 95;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto;
  background: #050403;
  padding: clamp(0.6rem, 1.6vw, 1.2rem);
}

.player[hidden] {
  display: none;
}

.player__bar,
.player__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
  padding-block: 0.4rem;
  /* Opaque, and the stage below clips.
     The video is 16:9; the window is whatever shape the visitor's screen is. On
     a screen shorter than 16:9 the video's own height exceeds its grid row and
     it painted straight over the footer — "Вернуться к эфиру" came out white
     on whatever the camera happened to be showing. Same fault as the overlay
     on the page stage: text over video has no guaranteed background, so it is
     given one rather than left to luck. */
  background: #050403;
  position: relative;
  z-index: 1;
}

.player__tools {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

.player__stage {
  position: relative;
  display: grid;
  place-items: center;
  min-block-size: 0;
  overflow: hidden;
}

.player__stage video {
  max-inline-size: 100%;
  max-block-size: 100%;
  inline-size: auto;
  block-size: auto;
  object-fit: contain;
  background: #000;
}

.player__waiting {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: var(--face-mono);
  font-size: var(--t-mono);
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-4);
  pointer-events: none;
}

.player__waiting[hidden] {
  display: none;
}

.player .iconbtn {
  background: rgba(12, 10, 9, 0.7);
}

/* The sound toggle: unmuted shows waves, muted hides them. Two paths swapped by
   class, so the icon and the aria-pressed state can never disagree. */
.iconbtn .wave {
  opacity: 0;
}

.iconbtn[aria-pressed="true"] .wave {
  opacity: 1;
}

.iconbtn[aria-pressed="true"] .mute {
  display: none;
}

.iconbtn[aria-pressed="false"] .wave {
  display: none;
}

/* ---------------------------------------------------------------------------
   REDUCED MOTION
   The ring, the scan and the state pulse all stop. The states remain
   distinguishable by colour and by their text, which is the point.
   --------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  .state[data-on="true"] .flame::before,
  .state[data-on="true"] .flame::after,
  .state[data-state-mode="checking"] .state__text,
  .stage[data-stage="checking"] .stage__frame::before {
    animation: none !important;
  }
  .state[data-state-mode="checking"] .state__text {
    opacity: 1;
  }
  .stage[data-stage="checking"] .stage__frame::before {
    display: none;
  }
}
