/* The listener radio — Sodium & Bakelite rev.03, height-budgeted.
 *
 * The front of the object. Tokens and shared primitives live in tokens.css;
 * this file is only the modules of this one surface.
 *
 * THE LAYOUT IS DRIVEN BY VIEWPORT HEIGHT, NOT WIDTH. Six modules: five are
 * constants at their tier and the hero is flex:1, so the column cannot
 * overflow by construction. The chassis is exactly the viewport and clips,
 * so a mistake shows as clipping rather than as a scrollbar.
 *
 * Budget, measured in the browser, fixed + hero + body padding = viewport:
 *   compact 553:  24 body + 24 pad + 264 fixed + 40 gaps + hero 199 = 551
 *   mid     664:  24 + 32 + 290 + 50 + hero 266 = 662
 *   tall    727:  24 + 36 + 314 + 60 + hero 291 = 725
 * (2px under each: the dial frame carries a 1px border top and bottom that
 * the design's arithmetic did not count. The hero absorbs it.)
 *
 * Floor is 500px: below it the hero hits its 132px minimum and we no longer
 * have a design. Every touch target stays 44px at every tier — the compact
 * tier spends its savings on type and air, never on hit area.
 */

* { box-sizing: border-box; }

:root {
  /* Tier: COMPACT, under 600px tall. */
  --l-pad: 12px;
  --l-gap: 8px;
  --l-plate: 34px;
  /* How far above the hero the card's top edge sits: the maker's line, the
     gap under it, and the case's padding. Artwork starts here, so a face
     runs to the top of the card instead of stopping short of it. */
  --art-top: calc(-1 * (var(--l-plate) + var(--l-gap) + var(--l-pad)));
  --l-freq: 52px;
  --l-show: 19px;
  --l-now: 11.5px;
  --l-glass: 68px;
  --l-key: 44px;
  --l-rail: 44px;
  --l-bar: 56px;
  --l-hero-min: 132px;
  --l-mhz: 9px;
  /* Below about 17px Nastaliq's diagonal clips and stops being readable, so
     in compact the script survives by changing face, not by shrinking. */
  --l-kick-family: var(--f-naskh);
  --l-kick-size: 13px;
  --l-kick-line: 1.8;
}
@media (min-height: 600px) {
  :root {
    --l-pad: 16px; --l-gap: 10px; --l-plate: 38px;
    --l-freq: 66px; --l-show: 23px; --l-now: 12px;
    --l-glass: 80px; --l-key: 48px; --l-rail: 46px; --l-bar: 60px;
    --l-hero-min: 150px; --l-mhz: 10px;
    --l-kick-family: var(--f-nastaliq); --l-kick-size: 19px; --l-kick-line: 1.9;
  }
}
@media (min-height: 720px) {
  :root {
    --l-pad: 18px; --l-gap: 12px; --l-plate: 40px;
    --l-freq: 78px; --l-show: 27px; --l-now: 12.5px;
    --l-glass: 92px; --l-key: 52px; --l-rail: 48px; --l-bar: 64px;
    --l-hero-min: 168px; --l-mhz: 10px;
    --l-kick-family: var(--f-nastaliq); --l-kick-size: 21px; --l-kick-line: 1.95;
  }
}

html { height: 100%; }
body {
  margin: 0;
  height: 100dvh;
  /* The page itself never scrolls. This is the constraint, stated once. */
  overflow: hidden;
  /* The one light source: upper-left, off-page, sodium. */
  background: var(--lampfall), var(--ground-0);
  color: var(--ink-2);
  font: var(--t-body)/1.5 var(--f-sans);
  -webkit-font-smoothing: antialiased;
  display: flex;
  justify-content: center;
  padding: 12px;
  padding-top: max(12px, env(safe-area-inset-top));
  padding-bottom: max(12px, env(safe-area-inset-bottom));
  user-select: none;
  -webkit-user-select: none;
  touch-action: manipulation;
}

/* The media carriers have no controls. Keep them rendered enough for mobile
   preload heuristics without letting them affect the chassis layout. */
#chan-els {
  position: fixed;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.filters { position: absolute; }
.sr-only {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%); overflow: hidden; white-space: nowrap;
}

/* ---------- the case ---------- */

.chassis {
  position: relative;
  width: 100%;
  max-width: 390px;
  height: 100%;
  /* Tall tier needs 314 fixed + 60 gaps + 36 padding + 320 hero = 730. Past
     that the object stops growing and centres itself: a small set on a big
     screen, not a stretched one. */
  max-height: 730px;
  margin: auto;
  display: flex;
  flex-direction: column;
  gap: var(--l-gap);
  padding: var(--l-pad);
  overflow: hidden;
  border: 1px solid var(--hair);
  border-radius: var(--r-chassis);          /* moulded top, cut base */
  background: radial-gradient(90% 40% at 30% -6%, oklch(0.30 0.045 70 / .5) 0%, transparent 60%),
              linear-gradient(180deg, var(--ground-1), var(--ground-0));
  box-shadow: var(--lift), var(--lit-top), var(--shade-bot);
}

/* Age, generated: a turbulence layer at 3.5%. Texture, not a film-grain
   effect — it never animates. */
.grain {
  position: absolute;
  inset: 0;
  opacity: 0.035;
  mix-blend-mode: overlay;
  filter: url(#sb-grain);
  background: var(--ink-3);
  pointer-events: none;
}

/* ---------- 1. maker's line ---------- */

.makers {
  height: var(--l-plate);
  flex: none;
  display: flex;
  align-items: center;
  gap: 9px;
  border-bottom: 1px solid var(--hair-soft);
}
.makers .wordmark {
  flex: none;
  font-size: 10px; font-weight: 700; font-stretch: 76%;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-1);
}
/* Urdu leads the station's own voice; it is not a translation of the mark. */
.makers .kicker {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  direction: rtl;
  text-align: right;
  font-family: var(--l-kick-family);
  font-size: var(--l-kick-size);
  line-height: var(--l-kick-line);
  color: var(--lamp);
}
.makers #clock {
  flex: none;
  font-family: var(--f-mono);
  font-size: 9.5px;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.makers .spool {
  width: 18px; height: 18px;
  flex: none;
  display: inline-flex; align-items: center; justify-content: center;
  border: 1px solid var(--hair-soft);
  border-radius: 50%;
}
.makers .spool i { width: 2px; height: 7px; background: var(--ink-4); }
/* Turns only while audio is flowing; stops dead when it is not. With the
   lamp, it is the only motion on the page. */
body.on .makers .spool { animation: sb-spool 9s linear infinite; }
body.on .makers .spool i { background: var(--ink-3); }

/* ---------- 2. hero (the elastic module) ---------- */

.hero {
  flex: 1;
  min-height: var(--l-hero-min);
  /* The slack absorber, but not without limit: past this the chassis stops
     rather than letting two rows of type float in a void. */
  max-height: 320px;
  position: relative;
  display: flex;
  flex-direction: column;
  /* Every channel sets its type at the foot of the band — numerals left,
     name right — so the art above keeps the faces clear. */
  justify-content: flex-end;
  gap: 6px;
}
/* Channel art. Sits inside the hero, before the lamp and before the rows, so
   DOM order alone keeps it behind both — the same idiom .lamp already uses,
   and the reason nothing here needs a z-index.
 *
 * `screen` is the whole trick. A print that is monochrome on a near-black
 * ground contributes nothing from its dark half and lets only the lit side of
 * the subject come through, so she is lit by the page's own sodium lamp
 * rather than pasted on top of it. A plain low-opacity image instead greys
 * the ground down and reads as a smudge. Pick art that suits this: high
 * contrast, dark surround, one clear subject.
 *
 * Only the universal mechanics live here. Framing — where the picture sits,
 * how it is masked, and how the type moves out of its way — belongs to each
 * artwork, because no two posters put their subject in the same place. See
 * the per-artwork block below and copy its shape for the next one. */
.art {
  position: absolute;
  inset: 0;
  background-repeat: no-repeat;
  /* Prints are neutral grey and read faintly blue against neutrals at hue
     52-64. Warm into the ramp before blending. */
  filter: grayscale(1) sepia(0.6) saturate(1.3) brightness(0.94) contrast(1.05);
  mix-blend-mode: screen;
  opacity: 0;
  transition: opacity 1.1s ease;
  pointer-events: none;
}

/* --- Fairouz, currently 106.2 --------------------------------------------
   Keyed by artist, not by frequency: the deck can move a set to another
   channel and the face should follow the music, not the dial position.

   Her portrait is upright with the face high, so the frame sits her to the
   right and the type sits at the foot of the band, as on every channel. The
   frequency used to land across her mouth; now it sits under her chin and
   her face is clear from the hairline down. */
body[data-art="fairouz"] .art {
  background-image: url("../img/fairouz.jpg");
  inset: var(--art-top) -18px -54px -26px;
  background-position: right -4% top 2%;
  background-size: auto 104%;
  /* One radial, not a composite of several: mask-composite is still uneven
     on iOS Safari, and a fallback there would union the layers and put a
     hard rectangle straight back. Full alpha across the face, transparent
     well inside every edge so the photo never shows a border. */
  -webkit-mask-image: radial-gradient(62% 56% at 70% 46%, #000 0%, rgba(0, 0, 0, 0.92) 44%, transparent 82%);
  mask-image: radial-gradient(62% 56% at 70% 46%, #000 0%, rgba(0, 0, 0, 0.92) 44%, transparent 82%);
  opacity: 0.62;
}

/* --- Pakistani pop, currently 89.4 ---------------------------------------
   A whole scene rather than a portrait: cassette deck, Strat, dupatta, the
   city behind. It arrived far brighter than the Fairouz print and would have
   fogged the page under `screen`, so the tone curve was crushed into the file
   itself before it ever got here — the lit half is the deck and her shoulder,
   everything else is black.

   The frame sits the figure right of centre and the mask stops before the
   deck, which keeps the left third dark for the numerals. */

body[data-art="pop-nostalgia"] .art {
  background-image: url("../img/pop-nostalgia.jpg");
  inset: var(--art-top) -18px -54px -26px;
  /* Pixel offsets, not percentages: with a background wider than its box the
     percentage form resolves against (box - image) and stops meaning
     anything you can reason about. */
  background-position: right -88px top 22px;
  background-size: auto 130%;
  /* A wider, softer vignette than the Fairouz portrait uses. This one is a
     scene, not a head: the deck and the guitar are half the tribute, so the
     mask has to hold the whole lit cluster rather than crop to a face. */
  -webkit-mask-image: radial-gradient(66% 70% at 74% 40%, #000 0%, rgba(0, 0, 0, 0.88) 46%, transparent 84%);
  mask-image: radial-gradient(66% 70% at 74% 40%, #000 0%, rgba(0, 0, 0, 0.88) 46%, transparent 84%);
  opacity: 0.68;
}

/* --- The old school, currently 101.2 -------------------------------------
   A collage, generated to this page's brief: the left third of the print is
   already black and the tribute — harmonium, valve radio, the PTV microphone,
   the shellac — is stacked down the right. It arrived brighter than either
   of the others and went through the same crushed curve as the pop print
   before it got here, so what is lit is the faces and the dial.

   It is a tall strip, taller than the hero band is ever allowed to be, so it
   is sized past the band and pinned to the top: the harmonium pair and the
   singer at the microphone are what shows, the tabla and the tea are below
   the fold on purpose. */

body[data-art="old-school"] .art {
  background-image: url("../img/old-school.jpg");
  inset: var(--art-top) -18px -54px -26px;
  background-position: right -38px top -8px;
  background-size: auto 150%;
  -webkit-mask-image: radial-gradient(60% 72% at 78% 44%, #000 0%, rgba(0, 0, 0, 0.9) 46%, transparent 84%);
  mask-image: radial-gradient(60% 72% at 78% 44%, #000 0%, rgba(0, 0, 0, 0.9) 46%, transparent 84%);
  opacity: 0.66;
}

/* --- Jeff Buckley, currently 104.8 ---------------------------------------
   A film still, black and white, in profile at the microphone. It is
   landscape where the others are portrait, and already black everywhere
   but his face and the mic, so it needs no tone curve. The source was a
   16mm scan with bright sprocket holes down its right edge; those were
   cropped off the file itself rather than left for the mask to hide.

   His face sits in the right third facing into the page, so the frame is
   pinned right and the type at the foot of the band clears his jaw. The mic
   stays in; it is half of what the picture says. */

body[data-art="jeffbuckley"] .art {
  background-image: url("../img/jeffbuckley.jpg");
  inset: var(--art-top) -18px -54px -26px;
  background-position: right 0 top 0;
  background-size: auto 100%;
  -webkit-mask-image: radial-gradient(64% 70% at 70% 44%, #000 0%, rgba(0, 0, 0, 0.9) 46%, transparent 86%);
  mask-image: radial-gradient(64% 70% at 70% 44%, #000 0%, rgba(0, 0, 0, 0.9) 46%, transparent 86%);
  opacity: 0.72;
}

/* --- Zia Mohyeddin, Main Line between shows -------------------------------
   The one portrait here on a light ground: a sepia studio print, cream
   behind him. Under `screen` that cream would fog the whole band, so this
   frame draws the print normally instead and pulls it down to the page's
   level with the filter; a tighter mask then lets the cream go before it
   reaches any edge. His face sits right of centre, turned into the page,
   so the frame is pinned right like 104.8's. It only shows while nobody is
   on air: during a show the frequency belongs to the host. */
body[data-art="zia-mohyeddin"] .art {
  background-image: url("../img/zia-mohyeddin.jpg");
  inset: var(--art-top) -18px -54px -26px;
  background-position: right -30px top 0;
  /* cover, not a fixed height: on a short phone the band is wider than
     the print is at that height, and a height fit left a hard edge down
     his left side. Pinned top, so what cover crops is his collar. */
  background-size: cover;
  mix-blend-mode: normal;
  filter: grayscale(1) sepia(0.55) saturate(1.2) brightness(0.62) contrast(1.18);
  -webkit-mask-image: radial-gradient(66% 64% at 60% 46%, #000 0%, rgba(0, 0, 0, 0.9) 55%, transparent 92%);
  mask-image: radial-gradient(66% 64% at 60% 46%, #000 0%, rgba(0, 0, 0, 0.9) 55%, transparent 92%);
  opacity: 0.82;
}

/* --- Any set's own cover --------------------------------------------------
   A set that is not framed by hand above can carry cover.jpg in its own
   folder on the box, and the station state hands its URL over; listener.js
   sets it as this element's background. One frame has to suit pictures
   nobody has seen yet, so it is a safe one: pinned right, the band's full
   height, faded out well inside every edge. It is drawn the way Zia
   Mohyeddin's print is, normally and darkened, not screened, because an
   uploaded photo is as likely to have a light background as a dark one. */
body[data-art="cover"] .art {
  inset: var(--art-top) -18px -54px -26px;
  background-position: right 0 top 0;
  background-size: auto 100%;
  mix-blend-mode: normal;
  filter: grayscale(1) sepia(0.55) saturate(1.2) brightness(0.62) contrast(1.18);
  -webkit-mask-image: radial-gradient(60% 66% at 70% 42%, #000 0%, rgba(0, 0, 0, 0.9) 44%, transparent 84%);
  mask-image: radial-gradient(60% 66% at 70% 42%, #000 0%, rgba(0, 0, 0, 0.9) 44%, transparent 84%);
  opacity: 0.7;
}

/* Anything that lands on a face needs to win against it. These are the only
   two runs of type the art sits under, and both are small and deliberately
   quiet, so they get a lift and a shadow rather than the art getting weaker
   — the picture is the point. */
body[data-art] .hero-right .chan { color: var(--ink-1); }
body[data-art] .hero-right .slot .ur-line { color: var(--ink-2); }
body[data-art] .hero-right,
body[data-art] .now,
body[data-art] .makers,
body[data-art] .freq {
  text-shadow: 0 1px 3px oklch(0.10 0.010 40 / 0.92),
               0 2px 12px oklch(0.10 0.010 40 / 0.75);
}
body[data-art] .now { color: var(--ink-2); }
/* MHZ is the smallest thing on the screen and it lands on her cheek. */
body[data-art] .freq small { color: var(--ink-3); }


/* The lamp IS the presence reading. JS drives only its opacity. */
.lamp {
  position: absolute;
  inset: -10px -22px;
  background: radial-gradient(58% 62% at 42% 52%, var(--lamp) 0%, transparent 72%);
  opacity: 0.05;
  transition: opacity 1.4s ease;
  pointer-events: none;
}
.hero-row { position: relative; display: flex; gap: 10px; }
.hero-row:not(.second) { align-items: flex-end; justify-content: space-between; }
.hero-row.second { align-items: baseline; justify-content: space-between; flex-wrap: wrap; }

.freq {
  display: flex; align-items: baseline; gap: 6px;
  min-width: 0;
  font-size: var(--l-freq); font-weight: 700; font-stretch: 82%;
  letter-spacing: -0.02em; line-height: 0.88;
  color: var(--ink-1);
  font-variant-numeric: tabular-nums;
}
.freq small {
  font-family: var(--f-mono);
  font-size: var(--l-mhz); font-weight: 500;
  letter-spacing: 0.14em;
  color: var(--ink-4);
}
.hero-right {
  flex: none;
  display: flex; flex-direction: column; align-items: flex-end; gap: 5px;
}
.hero-right .chan {
  font-size: 9.5px; font-weight: 700; font-stretch: 74%;
  letter-spacing: 0.16em; text-transform: uppercase;
  color: var(--ink-3);
}
body.on .hero-right .chan { color: var(--lamp); }
.hero-right .slot { display: flex; align-items: center; min-height: 18px; }
/* Off air a channel puts nothing here; only a LIVE badge, or the note
   between stations, claims the line. */
.hero-right .slot:empty { display: none; }
.hero-right .slot .ur-line {
  font-family: var(--f-naskh); direction: rtl;
  font-size: 12px; color: var(--ink-4);
}

/* Small on purpose: the numerals are the headline, this is the caption. */
.tagline {
  position: relative;
  margin: 0;
  font-family: var(--f-serif);
  font-size: var(--l-now); font-style: italic; line-height: 1.3;
  color: var(--ink-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.tagline:empty { display: none; }
body[data-art] .tagline {
  color: var(--ink-2);
  text-shadow: 0 1px 3px oklch(0.10 0.010 40 / 0.92),
               0 2px 12px oklch(0.10 0.010 40 / 0.75);
}

.show {
  margin: 0;
  font-family: var(--f-serif);
  font-size: var(--l-show); font-weight: 400; line-height: 1.2;
  color: var(--ink-1);
  min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.show.ur-naskh { line-height: 1.8; }
.now {
  display: none;
  margin: 0;
  flex: none;
  font-family: var(--f-serif);
  font-size: var(--l-now); font-style: italic;
  color: var(--ink-3);
  max-width: 55%;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---------- 3. the dial ---------- */

.dial-frame {
  flex: none;
  padding: 8px;
  border: 1px solid var(--hair);
  border-radius: var(--r-card);
  background: linear-gradient(180deg, var(--ground-3), var(--ground-1));
  box-shadow: var(--lit-top), var(--shade-bot);
}
/* Screen-printed glass in a recess. The whole pane is the drag target, and
   68px is the floor at which the tick ladder, the MHz numerals, the station
   strips and the needle all still read. */
.dial {
  position: relative;
  height: var(--l-glass);
  overflow: hidden;
  border-radius: 3px;
  background: radial-gradient(60% 120% at 50% 100%, oklch(0.800 0.112 72 / .30) 0%, transparent 72%),
              var(--glass);
  box-shadow: var(--well);
  cursor: grab;
  touch-action: pan-y;
  transition: background 0.5s ease;
}
.dial:active { cursor: grabbing; }
/* Between stations the lamp goes out: the glass drops to a cold-lit ground,
   ticks lose contrast, strips go to outline, and the static comes up. */
body.between .dial {
  background: radial-gradient(60% 120% at 50% 100%, oklch(0.800 0.112 72 / .07) 0%, transparent 72%),
              oklch(0.185 0.014 58);
}
.dial .static {
  position: absolute; inset: 0;
  opacity: 0;
  mix-blend-mode: screen;
  filter: url(#sb-static);
  background: var(--ink-4);
  pointer-events: none;
  transition: opacity 0.4s ease;
}
body.between .dial .static { opacity: 0.30; animation: sb-hum 0.18s steps(2) infinite; }
.dial .sheen {
  position: absolute; top: 0; left: 0; right: 0; height: 22px;
  background: linear-gradient(180deg, oklch(0.98 0.02 80 / .06), transparent);
  pointer-events: none;
}
/* The serial belongs on the glass, where a real set carries it. */
.dial .needle {
  position: absolute; top: 0; bottom: 0; left: 50%;
  width: 2px; margin-left: -1px;
  z-index: 3;
  background: linear-gradient(180deg, var(--lamp), var(--lamp-deep));
  box-shadow: 0 0 12px oklch(0.800 0.112 72 / .55);
  pointer-events: none;
}
body.between .dial .needle {
  background: var(--ink-4);
  box-shadow: none;
  opacity: 0.8;
  animation: sb-hum 0.3s ease-in-out infinite;
}

#ribbon { position: absolute; top: 0; bottom: 0; left: 50%; will-change: transform; }
/* Nothing on a printed dial is perfectly even. */
#ribbon .tick { position: absolute; top: 6px; background: var(--ink-2); }
#ribbon .tick.h1 { height: 5px; width: 1px; opacity: 0.32; }
#ribbon .tick.h2 { height: 12px; width: 1.5px; opacity: 0.85; }
#ribbon .num {
  position: absolute; top: 22px;
  transform: translateX(-50%);
  font-family: var(--f-mono);
  font-size: 9px;
  color: var(--ink-4);
  font-variant-numeric: tabular-nums;
}
#ribbon .mark {
  position: absolute; bottom: 26px;
  width: 2px; height: 6px;
  transform: translateX(-50%);
  background: var(--lamp-deep);
}
#ribbon .stname {
  position: absolute; bottom: 7px;
  transform: translateX(-50%);
  padding: 3px 6px;
  border: 1px solid var(--hair);
  border-radius: 2px;
  background: oklch(0.26 0.03 66);
  font-size: 8.5px; font-weight: 700; font-stretch: 70%;
  letter-spacing: 0.1em;
  color: var(--ink-2);
  white-space: nowrap;
}
#ribbon .stname.current { background: var(--cream); border-color: oklch(0.80 0.03 88); color: var(--ink-rev); }
#ribbon .stname.empty   { background: transparent; border-color: var(--hair-soft); color: var(--ink-4); }
body.between #ribbon { opacity: 0.55; }
body.between #ribbon .tick { background: var(--ink-3); }
body.between #ribbon .stname { background: transparent; border-color: var(--hair); color: var(--ink-4); }

/* ---------- 4. channel row ---------- */

/* Five keys, one row, 44px minimum at every tier. The now-playing line the
   old cards carried is already in the hero; the state note is already on the
   station strip. What is left is the frequency, the name, and a lamp dot. */
.notches {
  flex: none;
  height: var(--l-key);
  display: grid;
  grid-template-columns: repeat(5, 1fr);
  gap: 6px;
}
.notches button {
  font: inherit;
  position: relative;
  min-width: 0;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 2px;
  cursor: pointer;
  border-radius: var(--r-key);
  background: var(--ground-2);
  border: 1px solid var(--hair);
  box-shadow: var(--lit-top), var(--shade-bot);
  transition: background 0.2s, border-color 0.2s;
}
.notches button .f {
  font-family: var(--f-mono);
  font-size: 12px; font-weight: 600;
  color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}
.notches button .n {
  font-size: 7.5px; font-weight: 700; font-stretch: 72%;
  letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--ink-4);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
  max-width: 100%;
}
.notches button .livedot {
  position: absolute; top: 4px; right: 4px;
  width: 4px; height: 4px;
  border-radius: 50%;
  background: transparent;
}
.notches button:hover { background: var(--ground-3); }

/* Live elsewhere: a lamp dot and a lamp outline. */
.notches button.islive { border-color: var(--lamp-deep); background: var(--ground-3); }
.notches button.islive .livedot { background: var(--lamp); }
/* On the cream keycap the lamp would disappear, so the dot goes dark there. */
.notches button.current.islive .livedot { background: oklch(0.45 0.14 30); }
.notches button.islive .f { color: var(--ink-1); }
.notches button.islive .n { color: var(--lamp); }

/* Deliberately empty: dashed, no dot. */
.notches button.empty {
  background: transparent;
  border: 1px dashed var(--hair-soft);
  box-shadow: none;
}
.notches button.empty .f, .notches button.empty .n { color: var(--ink-4); }

/* Current: cream keycap, sunk, needle-matched. */
.notches button.current {
  background: var(--cream);
  border-color: oklch(0.80 0.03 88);
  box-shadow: var(--well);
}
.notches button.current .f { color: var(--ink-rev); }
.notches button.current .n { color: oklch(0.40 0.05 45); }

/* ---------- 5. call-state rail ---------- */

/* Where you stand, and what you can do about it. Fixed height in all five
   states, so nothing above or below it ever moves. */
/* The one surface that had no material on it: flat fill, one stray rule down
   the left. It is built like its neighbours now — closed with a hairline,
   lit from the same lamp — while the left edge stays the thing that carries
   the state colour. States set background-color only, so the sheen survives
   them; the keys stay full height because 44px is the floor. */
.rail {
  flex: none;
  height: var(--l-rail);
  margin: 0;
  display: flex;
  align-items: stretch;
  gap: 10px;
  padding-left: 10px;
  background-color: var(--ground-1);
  background-image: linear-gradient(180deg, oklch(0.660 0.030 72 / .085), transparent 62%);
  border: 1px solid var(--hair-soft);
  border-left: 3px solid var(--hair);
  border-radius: 0 var(--r-strip) var(--r-strip) 0;
  box-shadow: var(--lit-top), var(--shade-bot);
}
.railtext {
  flex: 1;
  min-width: 0;
  display: flex; flex-direction: column; justify-content: center;
  gap: 3px;
}
.rail .tag {
  font-family: var(--f-mono);
  font-size: 8px; line-height: 1; letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--ink-4);
}
.rail .say {
  font-family: var(--f-serif);
  font-size: 11.5px; line-height: 1.25;
  color: var(--ink-3);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.rail .keys { flex: none; display: flex; align-items: center; gap: 6px; }

.railkey {
  font: inherit;
  min-width: 104px;
  height: 100%;
  padding: 0 10px;
  cursor: pointer;
  border-radius: var(--r-key);
  background: var(--ground-3);
  border: 1px solid var(--hair);
  font-size: 9.5px; font-weight: 700; font-stretch: 74%;
  letter-spacing: 0.13em; text-transform: uppercase;
  color: var(--ink-2);
  box-shadow: var(--lit-top), var(--shade-bot);
}
.railkey:hover { background: var(--ground-4); color: var(--ink-1); }
.railkey.hear { min-width: 78px; background: var(--cream); color: var(--ink-rev); border-color: oklch(0.80 0.03 88); }
.railkey.danger { background: oklch(0.24 0.055 25); color: oklch(0.86 0.06 30); border-color: var(--dump); }

/* A music-only channel has nothing to put here, so the row goes and the hero
   takes its height: the art grows and everything under it moves down. The
   cap on the hero grows by the same amount, or the space would pool under
   the volume bar instead. The dial only moves when a tune lands, never
   mid-drag: the rail follows the channel, which changes on release. */
.rail.vacant { display: none; }
.chassis:has(.rail.vacant) .hero { max-height: calc(320px + var(--l-rail) + var(--l-gap)); }
.rail.waiting { background-color: oklch(0.800 0.112 72 / .09); border-left-color: var(--lamp-deep); }
.rail.waiting .tag { color: var(--lamp); }
.rail.waiting .say { color: var(--ink-2); }
.rail.oncall { background-color: oklch(0.585 0.170 30 / .12); border-left-color: var(--onair); }
.rail.oncall .tag { color: var(--onair-lit); }
.rail.oncall .say { color: var(--ink-1); font-family: var(--f-sans); font-size: 11px; }
.rail.muted { background-color: oklch(0.585 0.170 30 / .07); border-left-color: var(--dump); }
.rail.muted .tag { color: var(--onair-lit); }
.rail.muted .say { color: var(--ink-2); font-family: var(--f-sans); font-size: 11px; }
.rail.unlock { background-color: var(--ground-2); border-left-color: var(--cream); }
.rail.unlock .tag { color: var(--cream); }
.rail.unlock .say { color: var(--ink-1); font-family: var(--f-sans); font-size: 11px; }

/* ---------- 6. bottom bar ---------- */

.bottombar {
  /* The key reads as centred on the slider track, not on the VOL block: the
     label row above the slider is what pushes the track down, so the key
     drops by half of it. Both boxes read these two, so it cannot drift. */
  --vol-head-h: 13px;
  --vol-gap: 2px;
  flex: none;
  height: var(--l-bar);
  display: flex;
  align-items: center;
  gap: 12px;
  padding-top: 6px;
  border-top: 1px solid var(--hair-soft);
}
/* A transport key off a tabletop set, stamped nickel. It latches: a rounded
   play triangle when the radio is off, two rounded bars when it is on, so the
   key carries the same state aria-pressed already reports. Pressing it while
   it shows bars stops the radio — it does not hold a position, and tuning
   back in rejoins the live edge (see retune() in listener.js).

   The button box is 44px of hit area pulled back by negative margins to the
   glyph's own footprint, so the bar lays out around the shape while thumbs
   still land past 44px. Both glyphs are pseudo-elements stacked on that box
   and cross-faded, because a clip-path on the button itself would take the
   hit area with it.

   The face and its shadows are custom properties so the two glyphs stay one
   key: hover, press and focus retune the variables once on the button and
   both shapes follow. */
.play-control {
  /* Aged nickel, not chrome: pulled off cool 250 onto the panel's own warm
     ramp and taken down from 0.90 so the key stops being the brightest thing
     in the room. The hard light/dark break at the middle is what still reads
     as stamped metal. */
  --key-face: linear-gradient(180deg,
              oklch(0.76 0.020 72) 0%,
              oklch(0.63 0.024 68) 46%,
              oklch(0.48 0.022 60) 54%,
              oklch(0.59 0.021 66) 100%);
  /* A clipped shape takes its border and inset shadows with it, so the dark
     edge and the plinth under it are drop-shadows: three 1px casts make the
     outline, the fourth is the 2px the key sits on. The no-op casts are how
     the pressed and unfocused states stay the same shape of value. */
  --key-edge: drop-shadow(0 1px 0 oklch(0.34 0.016 52 / .9))
              drop-shadow(0 -1px 0 oklch(0.34 0.016 52 / .9))
              drop-shadow(1px 0 0 oklch(0.34 0.016 52 / .9));
  --key-plinth: drop-shadow(0 2px 0 oklch(0.10 0.01 40 / .7));
  --key-glow: drop-shadow(0 0 0 transparent);
  position: relative;
  top: calc((var(--vol-head-h) + var(--vol-gap)) / 2);
  width: 44px; height: 44px;
  margin: 0 -11px;
  flex: none;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
  transition: transform 0.08s;
}
.play-control::before,
.play-control::after {
  content: "";
  position: absolute;
  left: 50%; top: 50%;
  width: 21px; height: 23px;
  background: var(--key-face);
  filter: var(--key-edge) var(--key-plinth) var(--key-glow);
  transition: opacity 0.18s ease, transform 0.18s ease,
              filter 0.15s, background 0.15s;
}
/* A rounded triangle: the corners are trimmed back along each edge and the
   vertex becomes the control point of a quad curve, so the shape reads as
   stamped-and-tumbled rather than cut. The tip is trimmed 4px against the
   base corners' 3px because the acute angle needs more to look equally
   soft. Coordinates are the 21x23 border box. */
.play-control::before {
  clip-path: path("M 2.63 1.44 L 17.49 9.58 Q 21 11.5 17.49 13.42 L 2.63 21.56 Q 0 23 0 20 L 0 3 Q 0 0 2.63 1.44 Z");
}
/* Two bars, one path with two subpaths so a single face gradient runs across
   both and they cannot drift apart. 6px wide on a 9px gap, and 20px rather
   than the triangle's full 23 because two solid bars read heavier than one
   triangle at the same height. */
.play-control::after {
  clip-path: path("M 2 1.5 L 4 1.5 Q 6 1.5 6 3.5 L 6 19.5 Q 6 21.5 4 21.5 L 2 21.5 Q 0 21.5 0 19.5 L 0 3.5 Q 0 1.5 2 1.5 Z M 17 1.5 L 19 1.5 Q 21 1.5 21 3.5 L 21 19.5 Q 21 21.5 19 21.5 L 17 21.5 Q 15 21.5 15 19.5 L 15 3.5 Q 15 1.5 17 1.5 Z");
}
/* The cross-fade. The outgoing glyph falls back a little and the incoming one
   comes up to size, which is what makes it read as one key changing rather
   than two icons swapping. The centring translate has to ride along in every
   transform, since there is only one of those to spend. */
.play-control::before { opacity: 1; transform: translate(-50%, -50%) scale(1); }
.play-control::after  { opacity: 0; transform: translate(-50%, -50%) scale(0.86); }
body.on .play-control::before { opacity: 0; transform: translate(-50%, -50%) scale(0.86); }
body.on .play-control::after  { opacity: 1; transform: translate(-50%, -50%) scale(1); }
@media (prefers-reduced-motion: reduce) {
  .play-control::before,
  .play-control::after {
    transition: opacity 0.01s, filter 0.15s, background 0.15s;
    transform: translate(-50%, -50%);
  }
  body.on .play-control::before,
  body.on .play-control::after { transform: translate(-50%, -50%); }
}
@media (hover: hover) {
  .play-control:hover {
    --key-face: linear-gradient(180deg,
                oklch(0.81 0.020 72) 0%,
                oklch(0.68 0.024 68) 46%,
                oklch(0.53 0.022 60) 54%,
                oklch(0.64 0.021 66) 100%);
  }
}
/* Pressed: the key goes down onto its plinth and the face darkens. */
.play-control:active {
  transform: translateY(2px);
  --key-plinth: drop-shadow(0 0 0 transparent);
  --key-face: linear-gradient(180deg,
              oklch(0.62 0.020 72) 0%,
              oklch(0.52 0.024 68) 46%,
              oklch(0.42 0.022 60) 54%,
              oklch(0.50 0.021 66) 100%);
}
/* The focus ring is a glow on the glyph, not an outline on the box: the box
   is 44px of pad and an outline round it would point at nothing. */
.play-control:focus-visible {
  outline: none;
  --key-glow: drop-shadow(0 0 4px var(--lamp));
}

.vol { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: var(--vol-gap); }
.vol-head {
  height: var(--vol-head-h);
  display: flex; justify-content: space-between;
  font-family: var(--f-mono);
  font-size: 8.5px; letter-spacing: 0.14em;
  color: var(--ink-4);
}
.vol-head span:last-child { color: var(--ink-3); font-variant-numeric: tabular-nums; }
/* A milled steel thumb in a trough. The visible track is 8px; the input is
   30px tall and the thumb 26px, so the hit area stays past 44px with the
   VOL row above it. */
.vol input {
  width: 100%; height: 30px;
  appearance: none; -webkit-appearance: none;
  background: transparent;
  cursor: pointer;
}
.vol input::-webkit-slider-runnable-track {
  height: 8px; border-radius: var(--r-pill);
  background: linear-gradient(90deg, var(--lamp-deep) var(--volpct, 85%), var(--trough) var(--volpct, 85%));
  box-shadow: var(--well);
}
.vol input::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 18px; height: 26px;
  margin-top: -9px;
  border-radius: 2px;
  background: linear-gradient(180deg, var(--ground-4), var(--ground-2));
  border: 1px solid var(--hair);
  box-shadow: var(--lift), var(--lit-top);
}
.vol input::-moz-range-track { height: 8px; border-radius: var(--r-pill); background: var(--trough); box-shadow: var(--well); }
.vol input::-moz-range-progress { height: 8px; border-radius: var(--r-pill); background: linear-gradient(90deg, var(--lamp-deep), var(--lamp)); }
.vol input::-moz-range-thumb {
  width: 18px; height: 26px;
  border-radius: 2px;
  background: linear-gradient(180deg, var(--ground-4), var(--ground-2));
  border: 1px solid var(--hair);
  box-shadow: var(--lift), var(--lit-top);
}

/* ---------- bottom sheet ---------- */

#backdrop {
  position: fixed; inset: 0;
  z-index: 50;
  background: oklch(0.132 0.014 52 / .68);
  opacity: 0; pointer-events: none;
  transition: opacity 0.28s ease;
}
body.sheet-open #backdrop { opacity: 1; pointer-events: auto; }

.sheet {
  position: fixed; left: 0; right: 0; bottom: 0;
  z-index: 60;
  margin: 0 auto;
  max-width: 420px;
  display: flex; flex-direction: column; gap: var(--s3);
  padding: var(--s5) var(--s4) calc(var(--s4) + env(safe-area-inset-bottom));
  background: linear-gradient(180deg, var(--ground-2), var(--ground-1));
  border: 1px solid var(--hair);
  border-bottom: 0;
  border-radius: 16px 16px var(--r-strip) var(--r-strip);
  box-shadow: 0 -18px 40px -20px oklch(0.05 0.01 40 / .9), var(--lit-top);
  transform: translateY(105%);
  transition: transform 0.34s cubic-bezier(0.32, 0.72, 0.24, 1);
}
.sheet.open { transform: translateY(0); }
.sheet .grip {
  position: absolute; top: 8px; left: 50%;
  transform: translateX(-50%);
  width: 44px; height: 4px;
  border-radius: var(--r-pill);
  background: var(--hair);
}
.sheet-head .ur-nastaliq { font-size: 24px; color: var(--ink-1); }
.sheet-head h3 {
  margin: 2px 0 0;
  font-size: var(--t-label); font-weight: 700; font-stretch: 76%;
  letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--ink-3);
}
.sheet .hint {
  margin: 0;
  font-family: var(--f-serif);
  font-size: var(--t-lede); font-weight: 300;
  line-height: 1.5;
  color: var(--ink-2);
}
.named-field { position: relative; display: block; }
.named-field .lbl {
  position: absolute; top: 50%; left: 14px;
  transform: translateY(-50%);
  font-family: var(--f-mono);
  font-size: var(--t-micro); letter-spacing: 0.14em;
  color: var(--ink-4);
  pointer-events: none;
}
.named-field .sb-field { padding-left: 62px; }
.named-field:focus-within .lbl { color: var(--lamp); }
.sheet .row { display: flex; gap: 10px; }
.sheet .row .sb-key { flex: 1; }
.sheet .row .sb-key.primary { flex: 1.4; }

@media (prefers-reduced-motion: reduce) {
  .art { transition: none; }
  body.on .makers .spool,
  body.between .dial .static,
  body.between .dial .needle { animation: none; }
}
