/* Beatomat: light chassis, dark screens, one type family, one accent used
   only for the live signal. Every display in this app — the two YouTube
   iframes, the countdown panel — stays dark on purpose: that is what turns a
   black video rectangle from a hole in the page into the machine's screen. */
:root {
  --paper: #f1f2ef;
  --card: #ffffff;
  --ink: #14160f;
  --ink-2: #5f6459;
  --hair: #dcded8;
  --screen: #14160f;
  --accent: #0b5c50;
  --face: 'Archivo', ui-sans-serif, system-ui, sans-serif;
  --lift: 0 1px 2px rgba(20, 22, 15, .05), 0 12px 28px -18px rgba(20, 22, 15, .35);
  --lift-2: 0 2px 4px rgba(20, 22, 15, .06), 0 30px 60px -30px rgba(20, 22, 15, .45);
}

* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  background: var(--paper);
  color: var(--ink);
  font-family: var(--face);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
img { display: block; max-width: 100%; }
/* Inherit the colour, drop the underline — site.css does the same. Without
   this the account corner's "Sign in" is permanently underlined while every
   other link on the deck is not. */
a { color: inherit; text-decoration: none; }

/* ── the status lamp ─────────────────────────────────────
   Hollow ring when idle, filled with --accent and given a soft glow when a
   session is running. Used both on the gate mark and the header wordmark. */
.mark { display: inline-flex; align-items: center; gap: 1px; font-weight: 700; font-size: 20px; letter-spacing: -.02em; }
.mark .ring { width: 15px; height: 15px; border-radius: 50%; border: 3px solid var(--ink); margin: 0 1.5px; flex: none; }
.mark.live .ring {
  border-color: var(--accent);
  background: var(--accent);
  box-shadow: 0 0 0 3px rgba(11, 92, 80, .16);
}

/* The shared-password gate's styles used to sit here. The gate is gone —
   accounts replaced it and the deck is open — so its rules went with it. */

/* The skip link. Off-screen until it takes focus, then the first thing on the
   page — without it every keyboard user tabs through the whole header on every
   page before reaching anything they came for. */
.skip {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 50;
  background: var(--card);
  color: var(--ink);
  border-radius: 0 0 10px 0;
  padding: 12px 18px;
  font-size: 14px;
  font-weight: 500;
  box-shadow: var(--lift-2);
}
.skip:focus {
  left: 0;
}

/* ── header ───────────────────────────────────────────── */
.top { display: flex; align-items: center; gap: 36px; padding: 26px 0 30px; }

/* The wordmark is a link back to the library here. `a { color: inherit }`
   above leaves the underline, which the mark must not have. */
a.mark { text-decoration: none; }
a.mark:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; border-radius: 4px; }

.nav { display: flex; gap: 26px; font-size: 15px; color: var(--ink-2); }
.nav a { text-decoration: none; }
.nav a:hover { color: var(--ink); }

/* The session lamp: how long this set has been running. Hidden until
   something has actually played, because "0m" is not a session. */
.session {
  margin: 0 0 0 auto;
  display: inline-flex; align-items: center; gap: 10px;
  font-size: 13.5px; color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
.session[hidden] { display: none; }
.session .dot {
  width: 8px; height: 8px; border-radius: 50%; background: var(--accent); flex: none;
  box-shadow: 0 0 0 4px rgba(11, 92, 80, .28);
  animation: beat 2.4s ease-in-out infinite;
}
@keyframes beat { 0%, 100% { opacity: 1 } 50% { opacity: .35 } }

/* The account corner, as nav.js builds it. site.css styles the same markup for
   the library pages, but the deck does not load site.css — its component rules
   are scoped under `.site`, and pulling them in here would let `.site .mark`
   outrank this file's own `.mark` and quietly restyle the wordmark. Fifteen
   duplicated lines is the cheaper of the two mistakes. */
.acct { display: inline-flex; align-items: center; gap: 10px; font-size: 14px; }
/* The session lamp pushes the right-hand end of the header over, and the
   account corner rides along beside it. With no session running there is
   nothing there to do the pushing, so the corner takes the job itself. */
.top:has(.session[hidden]) .acct { margin-left: auto; }
.acct-in { color: var(--ink-2); white-space: nowrap; }
.acct-in:hover { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
.acct-who {
  color: var(--ink-2); max-width: 22ch; overflow: hidden;
  text-overflow: ellipsis; white-space: nowrap;
}
.acct-out {
  border: 1px solid var(--hair); background: transparent; border-radius: 999px;
  padding: 7px 14px; font: inherit; font-size: 13.5px; color: var(--ink-2); cursor: pointer;
}
.acct-out:hover { border-color: var(--ink-2); color: var(--ink); }
.acct-out:focus-visible, .acct-in:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }

/* The deck is a short page on a tall screen: two players, a handful of queue
   tiles and one line of speech do not reach the bottom of a laptop display,
   and everything piling up at the top with 500px of nothing under it is what
   made this page feel unfinished. The column is the height of the viewport
   and the machine's line is pushed to the end of it, so the page reads as
   deck at the top and speech just above the input you answer it with. */
.wrap {
  max-width: 1240px;
  margin: 0 auto;
  /* The bottom padding is what keeps the last line clear of the fixed dock,
     so it is measured against the dock (form, note and its own padding) and
     not picked to look right in a mock with more content in it. */
  padding: 0 40px 160px;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}
.said { margin-top: auto; }
.sr { position: absolute; left: -9999px; }

/* ── the console ──────────────────────────────────────────
   Two channel strips and a mixer between them. Neither strip ever moves: the
   engine swaps which deck is on air at every transition, and an iframe cannot
   follow it — re-parenting one reloads it and restarts playback. Making the
   decks equals rather than a big one and a small one turns that constraint
   into the layout the page wanted anyway. */
.console {
  display: grid;
  grid-template-columns: 1fr 220px 1fr;
  gap: 20px;
  align-items: start;
  margin-bottom: 30px;
}

.channel { min-width: 0; }

/* Every player is a screen, and screens are dark — the rule that turns the
   two mandatory YouTube iframes into a design element instead of a gap in a
   light page. #deckA/#deckB are placeholder divs the YouTube IFrame API
   replaces with iframes at runtime, so we style the wrapper, not the div. */
.screen {
  position: relative;
  background: var(--screen);
  border-radius: 16px;
  overflow: hidden;
  box-shadow: var(--lift-2);
  aspect-ratio: 16 / 10;
  /* The on-air ring. A transition is eight seconds long, so this crossfades
     rather than snapping: the treatment moves at the speed the sound does. */
  outline: 0 solid transparent;
  transition: outline-color .5s ease, outline-width .5s ease, box-shadow .5s ease;
}
.channel.on .screen {
  outline: 3px solid var(--accent);
  outline-offset: 0;
  box-shadow: var(--lift-2), 0 0 0 8px rgba(11, 92, 80, .12);
}
/* A deck holding nothing keeps its screen — a screen is dark whether or not
   there is anything on it, and fading the whole channel turned that dark into
   a grey rectangle on a light page. What dims is the strip below it, which
   really is saying less. */
.channel:not(.loaded) .ch-title,
.channel:not(.loaded) .ch-by,
.channel:not(.loaded) .ch-time { opacity: .45; }
.channel:not(.loaded) .screen { box-shadow: var(--lift); }

/* The pointer does not reach the players. Beatomat drives playback — a stray
   click pausing a deck mid-set is a bug, and on hover YouTube draws its own
   title bar and buttons straight over ours. The controls are already off
   (controls: 0 in deck.js); this stops the chrome that ignores that. */
/* `#deckA`, not `#deckA iframe`: YT.Player replaces the placeholder div with
   an iframe carrying the same id, so the descendant selector described an
   element that never exists and the players stayed clickable — which is why
   they kept drawing their own title bar and buttons over ours on hover. */
#deckA, #deckB {
  display: block; width: 100%; height: 100%;
  pointer-events: none;
}

.screen .tag {
  position: absolute; top: 12px; left: 12px; z-index: 3;
  background: rgba(20, 22, 15, .72); color: rgba(241, 242, 239, .9);
  font-size: 11px; font-weight: 600; letter-spacing: .08em; text-transform: uppercase;
  padding: 5px 9px; border-radius: 5px;
}
.screen .live-tag {
  position: absolute; top: 12px; right: 12px; z-index: 3;
  display: inline-flex; align-items: center; gap: 7px;
  background: rgba(241, 242, 239, .94); color: var(--ink);
  font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  padding: 5px 10px; border-radius: 5px;
}
.screen .live-tag[hidden] { display: none; }
.screen .live-tag i {
  width: 6px; height: 6px; border-radius: 50%; background: var(--accent); display: block;
  animation: beat 2.4s ease-in-out infinite;
}

/* The artwork layer sits over the iframe only when its slot holds an Audius
   track (Audius has no video, so the iframe underneath renders nothing).
   Hidden by default; ui.js adds .show when there is cover art to display,
   and .blank when the slot holds nothing at all — see renderSlotArt. */
.art {
  display: none;
  position: absolute;
  inset: 0;
  background-size: cover;
  background-position: center;
}
/* The dark underneath the cover art is not decoration: artwork is fetched
   from Audius' own hosts and arrives a moment after the class does, and for
   that moment a transparent layer shows the YouTube placeholder through. */
.art.show { display: block; background-color: var(--screen); }

/* The empty screen. Covers the YouTube API's grey-and-red placeholder with
   our own dark, and marks it with the ring from the wordmark. */
.art.blank {
  display: block;
  background-color: var(--screen);
  background-image: radial-gradient(circle at 50% 50%, rgba(11, 92, 80, .30), rgba(11, 92, 80, 0) 62%);
}
.art.blank::after {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  width: 22px; height: 22px;
  margin: -11px 0 0 -11px;
  border: 3px solid rgba(241, 242, 239, .28);
  border-radius: 50%;
}

/* ── what each strip says about its deck ──────────────── */
.ch-title {
  margin: 14px 0 2px; font-size: 19px; font-weight: 600;
  letter-spacing: -.015em; line-height: 1.2; overflow-wrap: anywhere;
}
.ch-by { margin: 0; font-size: 14px; color: var(--ink-2); overflow-wrap: anywhere; }
.ch-credit { margin: 4px 0 0; font-size: 12.5px; min-height: 18px; }
.ch-credit a { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.ch-bar {
  margin-top: 12px; height: 4px; border-radius: 2px; overflow: hidden;
  background: var(--hair);
}
.ch-bar i { display: block; height: 100%; width: 0; background: var(--ink-2); }
.channel.on .ch-bar i { background: var(--accent); }
.ch-time {
  display: flex; justify-content: space-between;
  margin-top: 6px; font-size: 12px; color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}

/* ── the mixer ────────────────────────────────────────── */
.mixer {
  background: var(--screen);
  border-radius: 16px;
  box-shadow: var(--lift-2);
  padding: 18px 18px 20px;
  display: flex;
  flex-direction: column;
  gap: 16px;
  color: var(--paper);
}
.mix-label {
  margin: 0; font-size: 13px; line-height: 1.35;
  color: rgba(241, 242, 239, .62); text-align: center; min-height: 36px;
}
.mix-label b { color: var(--paper); font-weight: 600; }

/* Meters either side of the fader, so the whole transition reads as one
   movement: one channel falls, the other rises, the fader travels between. */
.deckmeters { display: grid; grid-template-columns: 12px 1fr 12px; gap: 12px; align-items: end; }
.meter {
  height: 92px; border-radius: 6px; overflow: hidden;
  background: rgba(241, 242, 239, .12);
  display: flex; align-items: flex-end;
}
.meter i {
  display: block; width: 100%; height: 0;
  background: linear-gradient(to top, var(--accent), #2f9c8a);
}
/* A measured meter is drawn brighter than an inferred one, so the two are not
   claiming the same thing. */
.meter.measured i { background: linear-gradient(to top, #2f9c8a, #7fd3c1); }

.fader { padding-bottom: 4px; position: relative; }
.fader-track {
  position: relative; height: 8px; border-radius: 4px;
  background: rgba(241, 242, 239, .14); overflow: hidden;
}
.fader-fill { position: absolute; inset: 0 auto 0 0; width: 0; background: rgba(11, 92, 80, .8); }
.fader-knob {
  /* Rests in the middle: before anything plays there is no ratio between two
     silences, and a knob parked hard left would claim deck A is up. */
  position: absolute; top: -4px; left: 50%;
  width: 18px; height: 26px; margin-left: -9px;
  border-radius: 5px; background: var(--paper);
  box-shadow: 0 2px 6px rgba(0, 0, 0, .45);
}
.fader-knob::after {
  content: ""; position: absolute; inset: 11px 4px auto; height: 3px;
  border-radius: 2px; background: rgba(20, 22, 15, .45);
}
.fader-ends {
  display: flex; justify-content: space-between; margin-top: 10px;
  font-size: 11px; letter-spacing: .1em; color: rgba(241, 242, 239, .5);
}

/* The bass swap. Lit means that deck still has its low end; dark means the
   mixer has taken it out — the oldest trick in the job and, until now,
   completely invisible. */
.lows { display: flex; gap: 8px; }
.low {
  flex: 1; text-align: center; font-size: 10.5px; letter-spacing: .08em;
  text-transform: uppercase; padding: 6px 4px; border-radius: 6px;
  background: rgba(11, 92, 80, .55); color: var(--paper);
  transition: background .25s ease, color .25s ease, opacity .25s ease;
}
.low.out { background: rgba(241, 242, 239, .10); color: rgba(241, 242, 239, .72); }
/* A deck whose backend has no filter to cut with says so instead of claiming
   either state. See renderMixer: only a deck that can actually EQ gets a lamp
   that means anything. */
.low.na { background: transparent; border: 1px dashed rgba(241, 242, 239, .18); color: rgba(241, 242, 239, .45); }
.low.idle { opacity: .35; }

.controls { display: flex; flex-direction: column; gap: 10px; margin-top: auto; }
.mix-btn {
  border: 0; border-radius: 999px; padding: 12px 16px;
  background: var(--accent); color: #fff;
  font-family: var(--face); font-size: 15px; font-weight: 600; cursor: pointer;
}
.mix-btn:hover:not(:disabled) { background: #0a4c43; }
.mix-btn:disabled { background: rgba(241, 242, 239, .12); color: rgba(241, 242, 239, .4); cursor: default; }
.lengths { display: flex; gap: 6px; }
.lengths button {
  flex: 1; border: 1px solid rgba(241, 242, 239, .18); background: none;
  color: rgba(241, 242, 239, .7); border-radius: 8px; padding: 7px 4px;
  font-family: var(--face); font-size: 12.5px; cursor: pointer;
}
.lengths button[aria-pressed="true"] {
  background: rgba(241, 242, 239, .14); color: var(--paper); border-color: transparent;
}

/* The play control for a mix the browser would not start on its own. Covers
   the whole console, because with two equal decks there is no single screen
   for it to sit on. */
/* Absolutely positioned over the console rather than placed in it: as a grid
   item it claimed a row of its own and pushed the decks down the page. */
.console { position: relative; }
.cue {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  border: 0;
  border-radius: 16px;
  background: rgba(20, 22, 15, .55);
  backdrop-filter: blur(2px);
  color: var(--paper);
  font: inherit;
  font-size: 15px;
  font-weight: 500;
  cursor: pointer;
}
.cue[hidden] { display: none; }
.cue-mark {
  width: 62px; height: 62px; border-radius: 50%;
  background: var(--accent);
  box-shadow: 0 0 0 6px rgba(11, 92, 80, .22);
  position: relative;
}
/* The triangle, drawn rather than shipped as an asset, nudged right so it
   sits on the circle's optical centre. */
.cue-mark::after {
  content: "";
  position: absolute;
  top: 50%; left: 50%;
  margin: -11px 0 0 -7px;
  border-style: solid;
  border-width: 11px 0 11px 18px;
  border-color: transparent transparent transparent var(--paper);
}
.cue:hover .cue-mark { background: #0a4c43; }
.cue-text { max-width: 28ch; text-align: center; }

/* The library mix on air, under the console rather than on a screen: it
   belongs to the set, not to either deck. */
.mix-name {
  margin: 0 0 26px; font-size: 12.5px; letter-spacing: .04em;
  text-transform: uppercase; color: var(--ink-2);
}
.mix-name:empty { display: none; }

/* ── the set, to scale ────────────────────────────────────
   Two lanes, because two decks: each track sits on the deck that will play it,
   and where two blocks overlap is exactly where the crossfade is. The playhead
   is the only thing that moves, and it moves because the set does. */
.timeline {
  position: relative;
  height: 46px;
  margin: 0 0 30px;
  border-radius: 10px;
  background: var(--card);
  box-shadow: var(--lift);
  overflow: hidden;
}
.timeline[hidden] { display: none; }
.lanes { position: absolute; inset: 8px 10px; }
.block {
  position: absolute;
  height: 12px;
  border-radius: 3px;
  background: var(--accent);
  opacity: .85;
}
.block.a { top: 0; }
.block.b { top: 18px; background: #2f9c8a; }
.played {
  position: absolute;
  top: 0; bottom: 0; left: 0;
  width: 0;
  background: rgba(20, 22, 15, .06);
}
.played[hidden] { display: none; }
.playhead {
  position: absolute;
  top: 4px; bottom: 4px;
  width: 2px;
  margin-left: 10px;
  background: var(--ink);
  border-radius: 1px;
  box-shadow: 0 0 0 3px rgba(241, 242, 239, .8);
}
.playhead[hidden] { display: none; }

/* ── later in the set ─────────────────────────────────── */
.sec { display: flex; align-items: baseline; gap: 16px; margin: 0 0 16px; }
/* `display: flex` outranks the browser's own [hidden] rule, so a section this
   file lays out has to hide itself. */
.sec[hidden] { display: none; }
.sec h2 { font-size: 20px; letter-spacing: -.015em; margin: 0; font-weight: 600; }
.sec .hint { margin-left: auto; font-size: 13.5px; color: var(--ink-2); }
.queue { display: grid; grid-template-columns: repeat(auto-fill, minmax(196px, 1fr)); gap: 14px; }
.q {
  display: flex; gap: 12px; align-items: center;
  background: var(--card); border-radius: 12px;
  padding: 9px 12px 9px 9px; box-shadow: var(--lift); min-width: 0;
}
.q img, .q .placeholder {
  width: 44px; height: 44px; border-radius: 8px; object-fit: cover;
  flex: none; background: var(--screen);
}
.q .t { min-width: 0; }
.q .t b {
  display: block; font-weight: 600; font-size: 14px; line-height: 1.2;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.q .t span {
  display: block; color: var(--ink-2); font-size: 12.5px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.more {
  display: flex; align-items: center; justify-content: center;
  border: 1px dashed var(--hair); border-radius: 12px;
  color: var(--ink-2); font-size: 14px;
}

/* ── what the machine said ────────────────────────────── */
.said {
  /* margin-top is set to auto above, which pushes this to the bottom of the
     column; 34px is what it falls back to when the content is tall enough to
     fill the viewport on its own. */
  margin-top: 34px;
  background: var(--card); border-radius: 16px;
  padding: 22px 26px; box-shadow: var(--lift);
  display: flex; gap: 16px; align-items: flex-start;
}
.said .ring2 {
  width: 16px; height: 16px; border-radius: 50%;
  border: 4px solid var(--accent); flex: none; margin-top: 5px;
}
.said p { margin: 0; font-size: 19px; line-height: 1.45; max-width: 60ch; }
.said .ts { color: var(--ink-2); font-size: 13px; margin-top: 7px; }

/* ── the input, fixed. the disclosure lives here because
      Article 50(1) is about the interaction, not the catalogue. ── */
.dock {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  background: linear-gradient(180deg, rgba(241, 242, 239, 0), var(--paper) 22%);
  padding: 26px 40px 22px;
}
.dock .inner { max-width: 1240px; margin: 0 auto; }
.dock form {
  display: flex; align-items: center; gap: 12px;
  background: var(--card);
  border: 1px solid var(--hair);
  border-radius: 999px;
  padding: 6px 8px 6px 22px;
  box-shadow: var(--lift-2);
}
.dock form:focus-within { border-color: var(--accent); }
.dock input {
  flex: 1; border: 0; outline: none; background: none;
  color: var(--ink); font-family: var(--face); font-size: 17px; padding: 14px 0;
}
.dock input::placeholder { color: #8d9186; }
.dock button {
  border: 0; border-radius: 999px;
  background: var(--accent); color: #fff;
  font-family: var(--face); font-size: 15px; font-weight: 500;
  padding: 13px 24px; cursor: pointer;
}
.dock button:hover { background: #0a4c43; }
.note {
  display: flex; align-items: center; gap: 8px; justify-content: center;
  margin: 11px 0 0; font-size: 12.5px; color: var(--ink-2);
}
.note i { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); flex: none; }

/* ── focus visibility on every control ────────────────── */
a:focus-visible, button:focus-visible, input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}
/* Except the dock's input, which lives inside a 999px pill that already shows
   focus by colouring its border. A square 2px ring drawn inside a rounded pill
   is two focus states at once, and the square one pokes out of the cap. */
.dock input:focus-visible { outline: none; }

/* The mixer is a dark panel, and --accent on it is 2.4:1 — a focus ring you
   cannot see is not a focus ring. On the mixer the ring is paper. */
.mixer button:focus-visible { outline-color: var(--paper); }

@media (max-width: 1000px) {
  /* The mixer drops under the two decks rather than between them. It stays a
     mixer — the fader still travels A to B — but a 220px column between two
     decks stops being readable long before the decks do. */
  .console { grid-template-columns: 1fr 1fr; gap: 16px; }
  .mixer { grid-column: 1 / -1; order: 3; }
  /* The fader stops being a fader at 800px wide. Held to the width it reads
     at, centred, with the meters beside it. */
  .deckmeters { grid-template-columns: 14px minmax(0, 380px) 14px; justify-content: center; }
  .meter { height: 56px; }
  .controls { flex-direction: row; align-items: center; }
  .mix-btn { flex: 0 0 auto; }
  .lengths { flex: 1 1 auto; }
}

@media (max-width: 900px) {
  .wrap, .dock { padding-left: 20px; padding-right: 20px; }
  .wrap { padding-bottom: 190px; }
  .nav { display: none; }
  /* The heading and its hint stop sharing a line: at this width the heading
     wraps to two lines and the hint sits in the gap beside it. */
  .sec { flex-direction: column; align-items: flex-start; gap: 4px; }
  .sec .hint { margin-left: 0; }
  .ch-title { font-size: 17px; }
  .said p { font-size: 17px; }
  .dock { padding: 20px 20px 18px; }
  .dock input { font-size: 16px; }
}

@media (max-width: 560px) {
  /* One deck above the other. Both stay on the page: two decks is the whole
     idea, and a phone that shows one is showing a player. */
  .console { grid-template-columns: 1fr; }
  /* The mixer stays between the decks, where a mixer is. `.channel + .channel`
     does not match deck B — the mixer is between them in the DOM — so the
     order is set on the deck itself. */
  .mixer { order: 2; }
  #chanB { order: 3; }
  .screen { aspect-ratio: 16 / 9; }
  .ch-title { font-size: 16px; }
}

@media (max-width: 380px) {
  .wrap, .dock { padding-left: 14px; padding-right: 14px; }
}

@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}
