/* Broadcast — standby holding page.
   One card, always fitting the viewport: every height, pad, gap and size is
   clamped against vh so the card never grows an internal scrollbar. */

html, body { height: 100%; }
body {
  display: block;
  overflow: hidden;
  background: var(--bg);
}

/* ---------- the screen: full-bleed CRT ----------
   The whole viewport is the tube, not a framed card sitting on a page. The bezel
   border and --radius-crt come off with it — there's no frame left to round — and
   .bx-vignette carries the CRT edge falloff on its own. */
.card {
  width: 100%;
  height: 100vh;
  height: 100dvh; /* mobile: discounts the browser chrome, so nothing hides below the fold */
  display: grid;
  grid-template-rows: auto minmax(0, 1fr) auto auto; /* bars · content · ticker · bars */
  border: 0;
  border-radius: var(--radius-0);
  overflow: hidden;
  background: var(--bg-inset);
}

.bars { display: flex; height: clamp(40px, 14vh, 120px); }
.bars > i { flex: 1; }
.bars--foot { height: clamp(18px, 5vh, 40px); }

/* ---------- content ---------- */
.card__body {
  padding: clamp(18px, 4vh, 44px) clamp(24px, 5vw, 48px);
  display: grid;
  align-content: center;
  gap: clamp(10px, 2.4vh, 22px);
  text-align: center;
  overflow: hidden;
}

/* Three-column so the live indicator sits dead centre and the outer two stay pinned
   to the edges — denser operator chrome than the festival's two-column row. */
.meta {
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 16px;
  font-size: var(--text-xs);
  letter-spacing: var(--track-wider);
  color: var(--text-3);
}
.meta > :first-child { text-align: left; }
.meta > :last-child { text-align: right; }

.live {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  color: var(--alert);
}
.live__dot {
  width: 7px;
  height: 7px;
  background: currentColor;
  display: inline-block;
  flex: none;
}

/* ---------- ticker ---------- */
.ticker {
  overflow: hidden;
  border-top: var(--border-thin) solid var(--line);
  background: var(--bg);
  white-space: nowrap;
}
.ticker__row {
  display: inline-block;
  padding: 7px 0;
  font-family: var(--font-terminal);
  font-size: 20px;
  color: var(--accent);
  animation: bx-ticker 38s linear infinite;
}
.ticker:hover .ticker__row { animation-play-state: paused; }
@media (prefers-reduced-motion: reduce) {
  .ticker__row { animation: none; }
}

/* ---------- the lockup: BROADCAST whole, BY cutting in and out ----------
   Page-local, deliberately NOT added to tokens/: the vertical roll is the festival's
   treatment and stays exclusive to it. This arm gets the dropout instead — the master
   mark never breaks, the BY is the part with the bad signal.

   Same house rules as the roll: steps(1, end), nothing tweens, irregular percentages
   on purpose. BY toggles `visibility`, never `display`, so it keeps its box and the
   lockup cannot reflow when the signal drops. */
.mark {
  font-family: var(--font-display);
  letter-spacing: var(--track-display);
  text-transform: uppercase;
  color: var(--accent);
  line-height: 1;
  margin: 0;
  justify-self: center;
  white-space: nowrap;
  font-size: clamp(30px, min(8.4vw, 13vh), 132px);
}
/* BY carries signal green rather than phosphor amber — it reads as a second source
   cutting into the amber master mark, which is the whole idea. Semantic token, not
   the raw palette, so it still resolves correctly if this page ever gains light mode. */
.mark__by {
  position: relative;
  display: inline-block;
  margin-right: 0.16em;
  color: var(--positive);
  animation: bx-by-cut 6s steps(1, end) infinite;
}
.mark__by::before,
.mark__by::after {
  content: attr(data-text);
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* visibility inherits — without this the ghosts vanish with BY and the tear
     never renders. They must outlive the dropout; that IS the effect. */
  visibility: visible;
}
/* Ghost pair re-picked for a green BY: green-on-green would be invisible, so the tear
   throws red and phosphor amber instead — still only palette colours. */
.mark__by::before { color: var(--bx-on-air); animation: bx-by-ghost-a 6s steps(1, end) infinite; }
.mark__by::after  { color: var(--bx-phosphor); animation: bx-by-ghost-b 6s steps(1, end) infinite; }

/* Inverted: BY is ABSENT by default and glitches IN. The station is BROADCAST; the
   BY has to fight its way onto the signal, stutter three times, lock for a beat, then
   lose it again. Entry is the event, so the stutters cluster there. */
@keyframes bx-by-cut {
  0%, 28%   { visibility: hidden; transform: none; }
  29%       { visibility: visible; transform: translateX(-0.03em); }
  30%       { visibility: hidden; transform: none; }
  31%       { visibility: visible; transform: translateX(0.025em); }
  32%       { visibility: hidden; transform: none; }
  33%       { visibility: visible; transform: translateX(-0.015em); }
  34%, 62%  { visibility: visible; transform: none; }
  63%       { visibility: hidden; transform: none; }
  64%       { visibility: visible; transform: translateX(0.02em); }
  65%, 100% { visibility: hidden; transform: none; }
}
/* Ghosts keep visibility:visible, so on the way in they land BEFORE the word does —
   the tear arrives first and BY resolves out of it. */
@keyframes bx-by-ghost-a {
  0%, 27%   { opacity: 0; transform: none; }
  28%       { opacity: 0.85; transform: translateX(-0.09em); }
  30%       { opacity: 0.85; transform: translateX(0.06em); }
  32%       { opacity: 0.85; transform: translateX(-0.05em); }
  34%, 62%  { opacity: 0; transform: none; }
  63%       { opacity: 0.85; transform: translateX(0.07em); }
  65%, 100% { opacity: 0; transform: none; }
}
@keyframes bx-by-ghost-b {
  0%, 27%   { opacity: 0; transform: none; }
  28%       { opacity: 0.7; transform: translateX(0.07em); }
  30%       { opacity: 0.7; transform: translateX(-0.08em); }
  32%       { opacity: 0.7; transform: translateX(0.04em); }
  34%, 62%  { opacity: 0; transform: none; }
  63%       { opacity: 0.7; transform: translateX(-0.06em); }
  65%, 100% { opacity: 0; transform: none; }
}

/* Hover holds the full lockup rather than freezing mid-dropout, so it can always be read */
.mark:hover .mark__by { animation: none; visibility: visible; transform: none; }
.mark:hover .mark__by::before,
.mark:hover .mark__by::after { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .mark__by { animation: none; visibility: visible; transform: none; }
  .mark__by::before, .mark__by::after { display: none; }
}

/* ---------- extra copy this arm carries ---------- */
.blurb--tight { margin-top: -4px; }
.signoff {
  font-size: var(--text-xs);
  color: var(--text-3);
  margin: 0;
}

/* ---------- short viewports ----------
   This arm carries more copy than the festival page plus a ticker row, which
   overflowed the content area at 540px high. Tighten the rhythm rather than let it
   scroll — the card must always fit, and it was measured overflowing by 46px. */
@media (max-height: 640px) {
  .card__body {
    gap: clamp(4px, 1.1vh, 12px);
    padding: clamp(10px, 2vh, 28px) clamp(24px, 5vw, 48px);
  }
  .mark { font-size: clamp(30px, min(8.4vw, 10.5vh), 132px); }
  .blurb { font-size: var(--text-xs); line-height: 1.4; }
  .bars { height: clamp(28px, 8vh, 120px); }
  .ticker__row { padding: 5px 0; font-size: 18px; }
}
/* Below this there genuinely isn't room for the sign-off; the form matters more */
@media (max-height: 520px) {
  .signoff { display: none; }
}

/* Narrow screens: the extra copy wraps to more lines than the festival page's single
   short line, which overflowed by 7px at 375×812 even though the viewport is tall. */
@media (max-width: 460px) {
  .card__body { gap: clamp(6px, 1.6vh, 16px); }
  .blurb { font-size: var(--text-xs); line-height: 1.45; }
  .meta { font-size: 10px; letter-spacing: 0.18em; gap: 8px; }
}

.standby {
  font-family: var(--font-terminal);
  font-size: clamp(20px, 3.6vh, 28px);
  color: var(--text-1);
}

.blurb {
  font-size: var(--text-sm);
  color: var(--text-2);
  max-width: 48ch;
  margin: 0 auto;
}

/* ---------- signup ---------- */
.tune {
  display: flex;
  gap: 10px;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
}
.tune__field {
  display: flex;
  align-items: center;
  gap: 8px;
  background: var(--bg);
  border: var(--border-thin) solid var(--line-strong);
  padding: 9px 12px;
  width: min(320px, 55vw);
}
.tune__field:focus-within { border-color: var(--accent); }
.tune__prompt {
  font-family: var(--font-terminal);
  font-size: 18px;
  color: var(--positive);
  line-height: 1;
}
.tune__input {
  all: unset;
  flex: 1;
  text-align: left;
  font-family: var(--font-mono);
  font-size: var(--text-base);
  color: var(--text-1);
  caret-color: var(--accent);
}
.tune__send {
  font-family: var(--font-mono);
  font-weight: 700;
  font-size: 12px;
  letter-spacing: var(--track-wide);
  text-transform: uppercase;
  background: transparent;
  color: var(--accent);
  border: var(--border-thick) solid var(--accent);
  border-radius: var(--radius-0);
  padding: 9px 20px;
  cursor: pointer;
}
.tune__send:hover {
  background: var(--accent);
  color: var(--accent-contrast);
  box-shadow: var(--glow-accent);
}

/* visibility, not display — revealing this must not reflow the card */
.locked { visibility: hidden; font-size: var(--text-sm); color: var(--text-1); }
.locked__tag { font-family: var(--font-terminal); font-size: 18px; color: var(--positive); }

/* Consent microcopy — "Straight" register, per the voice rules for legal copy */
.consent {
  font-size: var(--text-xs);
  color: var(--text-3);
  max-width: 46ch;
  margin: 0 auto;
  line-height: 1.5;
}
.consent a { color: var(--link); }

/* ---------- grain ---------- */
#grain {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  z-index: 60;
  opacity: .055;
  mix-blend-mode: screen;
}
