/* ==========================================================================
   RadioSite24 — the listener-facing design system.

   ONE stylesheet, ONE set of components, EIGHT themes. A theme supplies colour
   tokens (inlined into <head> per site by core/themes.py) plus a handful of
   treatment switches read off <html data-cards data-header data-hero
   data-player data-images data-texture data-mode>.

   That is the whole trick: nothing below branches on "which theme is this", it
   branches on "which treatment did the theme ask for". Adding a ninth theme is
   a dict entry, not a stylesheet.

   Structure:
     1  tokens & reset          6  cards & editorial
     2  typography              7  schedule
     3  masthead & nav          8  people
     4  layout                  9  rail: weather, on-air, social
     5  hero                   10  player bar
                               11  forms, sponsors, footer, utilities
   ========================================================================== */

/* ------------------------------------------------------- 1. tokens ------- */
/* Colour tokens arrive inlined per-site. Everything structural lives here so
   it is cached across every site on the platform. */
:root{
  --wrap:1240px;
  --gut:clamp(16px,3.4vw,32px);
  --gap:clamp(20px,2.6vw,26px);
  --block:clamp(34px,4.4vw,56px);
  --shadow-1:0 1px 2px rgb(0 0 0 / .05), 0 4px 16px -8px rgb(0 0 0 / .10);
  --shadow-2:0 2px 6px rgb(0 0 0 / .07), 0 18px 40px -20px rgb(0 0 0 / .22);
  --ease:cubic-bezier(.22,.61,.36,1);
  --player-h:76px;
}

*,*::before,*::after{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
html:focus-within{scroll-behavior:smooth}

/* Horizontal overflow is clipped HERE, on <html>, and with `clip` rather than
   `hidden`. Two reasons, both easy to get wrong:
     1. `overflow-x:hidden` forces overflow-y to compute to `auto`, turning the
        element into a scroll container. On <body> that gives the page two
        nested scrollers - confirmed here, the computed style really did read
        "hidden auto" - which is a well-known way to break position:sticky and
        scroll restoration.
     2. Any overflow other than `visible` on <body> also makes it a containing
        block for position:fixed descendants, so the player bar would stop
        resolving against the viewport.
   `clip` on <html> avoids both: it clips without establishing a scroll
   container, and fixed elements still resolve against the viewport. */
html{overflow-x:clip}

body{
  margin:0;
  /* The FOOTER's colour, not the page's. Anything that ends up below the
     footer then reads as the footer carrying on, instead of as a blank screen
     the site forgot to fill.

     Two things put stuff there. Overscroll — the rubber-band on a Mac or an
     iPhone — shows this colour, and dark-under-the-footer is the right answer
     for that anyway. And browser extensions: a screen recorder, an ad blocker
     or a coupon toolbar will happily append a viewport-tall `position:relative`
     div to <body>, which lands after the footer and adds its full height to the
     document. We cannot remove somebody else's element and should not try, but
     we can make sure it is not a visible hole.

     `main` paints the page colour instead — see below. */
  background:var(--deep);
  color:var(--ink);
  font-family:var(--sans);
  font-size:16px;
  line-height:1.6;
  -webkit-font-smoothing:antialiased;
  text-rendering:optimizeLegibility;
}

img,svg,video{max-width:100%;display:block}
img{height:auto;background:var(--line-2)}
a{color:inherit;text-decoration:none}
button,input,select,textarea{font:inherit;color:inherit}
h1,h2,h3,h4{margin:0;font-family:var(--display);font-weight:800;letter-spacing:-.021em;line-height:1.14}
p{margin:0 0 1em}
p:last-child{margin-bottom:0}
hr{border:0;border-top:1px solid var(--line);margin:var(--gap) 0}

/* One visible focus style for the whole product. Never removed, never subtle:
   a keyboard user has no other way of knowing where they are. */
:focus-visible{
  outline:3px solid var(--primary);
  outline-offset:2px;
  border-radius:3px;
}
:focus:not(:focus-visible){outline:none}

.skip{
  position:absolute;left:-9999px;top:0;z-index:300;
  background:var(--primary);color:var(--primary-on);
  padding:13px 22px;font-weight:700;border-radius:0 0 8px 0;
}
.skip:focus{left:0}

.wrap{max-width:var(--wrap);margin-inline:auto;padding-inline:var(--gut)}
.sr{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;
}

/* Anyone who has asked their system to stop animating things means it. */
@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{
    animation-duration:.001ms !important;animation-iteration-count:1 !important;
    transition-duration:.001ms !important;scroll-behavior:auto !important;
  }
}

/* Optional theme textures. Kept to pseudo-elements on the body so they cost
   nothing and can never intercept a click. */
body::before{content:"";position:fixed;inset:0;pointer-events:none;z-index:0;opacity:0}
[data-texture="mesh"] body::before{
  opacity:1;
  background:
    radial-gradient(60ch 40ch at 12% -8%, color-mix(in srgb, var(--primary) 22%, transparent), transparent 70%),
    radial-gradient(50ch 36ch at 92% 4%, color-mix(in srgb, var(--accent) 18%, transparent), transparent 70%);
}
[data-texture="grain"] body::before{
  opacity:.5;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)' opacity='.16'/%3E%3C/svg%3E");
}
[data-texture="scanline"] body::before{
  opacity:.28;
  background:repeating-linear-gradient(180deg,transparent 0 3px,rgb(255 255 255 / .035) 3px 4px);
}
body > *{position:relative;z-index:1}


/* Art placeholders must FILL the box they are given. Without an explicit size
   the span collapses to its text and leaves a coloured bar above a white gap -
   which is exactly what a station with no show artwork would see on their
   homepage, i.e. most of them on day one. */
.hero-img .art-ph{
  position:absolute;inset:0;width:100%;height:100%;
  font-size:clamp(56px,9vw,110px);
}
.ncard .im .art-ph,.hcard .im .art-ph,.track .art .art-ph{
  width:100%;height:100%;font-size:clamp(34px,5vw,52px);
}
.profile .pic .art-ph{width:100%;height:100%;font-size:82px}

/* Station name and strapline stack. As inline spans they sat side by side and
   read as one run-on line. */
.brand .bn,.brand .bs{display:block}

/* ------------------------------------------------- brand colour as text ---
   `--primary-text` and `--accent-text`, not `--primary` and `--accent`.

   A brand colour picked to look good behind white button text is very often too
   light to READ at 13px. Measured on a card: Contemporary Light's green accent
   was 3.6:1, Bold Pop's pink 3.6:1, Community's orange 2.6:1 - all under AA, all
   used for real text (every "Read more", every schedule time, every presenter's
   job title, the current menu item, every kicker).

   core/themes.py derives the text variants by moving the same hue only as far
   towards black - or white on a dark ground - as it has to. Borders and
   backgrounds keep the full-strength colour: non-text contrast needs 3:1, and
   muting those would flatten the design for nothing.
   ------------------------------------------------------------------------- */

/* -------------------------------------------------- 2. typography -------- */
.sec-title{
  display:flex;align-items:baseline;justify-content:space-between;
  gap:16px;margin:0 0 var(--gap);padding-bottom:12px;
  border-bottom:2px solid var(--line);position:relative;
}
.sec-title::after{
  content:"";position:absolute;left:0;bottom:-2px;width:64px;height:2px;
  background:var(--primary);
}
.sec-title h2{font-size:clamp(20px,2.4vw,25px)}
.sec-title .more{
  font-size:13.5px;font-weight:700;color:var(--primary-text);white-space:nowrap;
  display:inline-flex;align-items:center;gap:5px;
  /* WCAG 2.2 asks for 24px of target. A 13.5px line is 22px tall, so these
     "See all" links were a couple of pixels short on a phone - which is exactly
     the sort of link somebody stabs at three times on a bus. */
  min-height:24px;
}
.sec-title .more::after{content:"\203A";font-size:17px;line-height:1}
.sec-title .more:hover{text-decoration:underline}

.kicker{
  display:inline-flex;align-items:center;align-self:flex-start;gap:7px;
  background:var(--primary-lt);color:var(--primary-on-tint);
  font-size:10.5px;font-weight:800;letter-spacing:.13em;text-transform:uppercase;
  padding:6px 12px;border-radius:100px;margin-bottom:15px;
}
.kicker.accent{background:var(--accent-lt);color:var(--accent-on-tint)}
.kicker.plain{background:var(--line-2);color:var(--ink-2)}

.dot{
  width:6px;height:6px;border-radius:50%;background:currentColor;
  animation:blink 2s infinite;flex-shrink:0;
}
@keyframes blink{0%,100%{opacity:1}50%{opacity:.25}}

.lede{font-size:clamp(16.5px,1.9vw,19px);color:var(--ink-2);max-width:66ch}
.muted{color:var(--ink-3)}
.tnum{font-variant-numeric:tabular-nums}


/* These labels are wrapped in a plain <span> in the markup and are meant to
   stack. As inline spans they ran together on one line - "Evenings Priya Raval"
   instead of a title with the presenter under it - which quietly undid the
   type hierarchy everywhere the pattern is used. */
.live-card .lbl,.live-card .t1,.live-card .t2,
.oa .k,.oa .n,.oa .t,
.sched .tmz,.sched .nm,.sched .sh{display:block}
.live-card .lt{min-width:0}
.oa > span:last-child,.sched a > span:last-child{min-width:0;flex:1}

/* ------------------------------------------- 3. masthead & navigation ---- */
.masthead{background:var(--card);border-bottom:1px solid var(--line)}
.masthead .wrap{
  display:flex;align-items:center;justify-content:space-between;
  gap:24px;padding-block:15px;flex-wrap:wrap;
}
.brand{display:flex;align-items:center;gap:14px;min-width:0}
/* Height-driven, because a station logo is usually a WORDMARK: constrain the
   height and let the width follow, and a 10:1 wordmark and a square badge both
   sit properly on the same line.

   `--logo-scale` is the station's own adjustment (Design -> Logo size). A round
   badge at the same height as a wordmark always looks too small, and there is
   no automatic answer to that - only the person looking at it knows. Defaults
   to 1, so a site that never touches it is exactly as it was.

   max-width stops a very wide wordmark pushing the menu off a phone. */
.brand img{
  height:calc(clamp(46px,6vw,72px) * var(--logo-scale,1));
  width:auto;max-width:min(60vw,420px);object-fit:contain;background:none;
}
.brand .bn{font-family:var(--display);font-size:clamp(19px,2.4vw,26px);font-weight:900;letter-spacing:-.03em}
.brand .bs{
  font-size:10.5px;font-weight:700;letter-spacing:.15em;text-transform:uppercase;
  color:var(--ink-3);margin-top:3px;
}

/* The two live cards: what's on air, and what's playing. */
.live-pair{display:flex;gap:12px;flex-wrap:wrap}
.live-card{
  display:flex;align-items:center;gap:13px;
  background:var(--ground);border:1px solid var(--line);
  border-radius:var(--r);padding:10px 18px 10px 10px;min-width:250px;
  transition:border-color .18s var(--ease),box-shadow .18s var(--ease);
}
.live-card:hover{border-color:var(--primary);box-shadow:var(--shadow-1)}
.live-card .lart{width:52px;height:52px;border-radius:calc(var(--r) - 3px);object-fit:cover;flex-shrink:0}
.live-card .lbl{
  font-size:9.5px;font-weight:800;letter-spacing:.15em;text-transform:uppercase;
  color:var(--primary-text);display:flex;align-items:center;gap:6px;
}
.live-card.np .lbl{color:var(--accent-text)}
.live-card .t1{font-weight:700;font-size:14.5px;letter-spacing:-.01em;margin-top:2px}
.live-card .t2{font-size:12.5px;color:var(--ink-3)}
.live-card .t1,.live-card .t2{
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:22ch;
}
@media(max-width:640px){
  .live-pair{width:100%}
  .live-card{flex:1;min-width:0}
  .live-card:nth-child(2){display:none}   /* one card is plenty on a phone */
}

/* Art placeholder — a station with no artwork still gets something designed
   rather than a broken image. Initial on a brand gradient. */
.art-ph{
  display:grid;place-items:center;flex-shrink:0;
  background:linear-gradient(135deg,var(--primary),var(--primary-dk));
  color:var(--primary-on);font-family:var(--display);font-weight:900;
  letter-spacing:-.04em;
}
.live-card .art-ph{width:52px;height:52px;border-radius:calc(var(--r) - 3px);font-size:19px}

.navbar{
  background:var(--card);position:sticky;top:0;z-index:60;
  border-bottom:1px solid var(--line);
}
.navbar::before{
  content:"";position:absolute;left:0;right:0;top:0;height:3px;
  background:linear-gradient(90deg,var(--primary) 0%,var(--primary) 54%,var(--accent) 100%);
}
.navbar .wrap{display:flex;align-items:center;gap:6px}
.navbar nav{display:none;gap:clamp(2px,1.3vw,14px);flex:1;min-width:0}
@media(min-width:940px){.navbar nav{display:flex}}
.navbar nav a{
  position:relative;padding:19px 7px 16px;white-space:nowrap;
  font-size:12.5px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;
  color:var(--ink-2);transition:color .16s var(--ease);
}
[data-nav-case="normal"] .navbar nav a{
  text-transform:none;letter-spacing:0;font-size:14.5px;font-weight:600;
}
.navbar nav a::after{
  content:"";position:absolute;left:7px;right:7px;bottom:0;height:3px;
  background:var(--primary);transform:scaleX(0);transform-origin:center;
  transition:transform .22s var(--ease);
}
.navbar nav a:hover{color:var(--ink)}
.navbar nav a:hover::after,.navbar nav a.on::after{transform:scaleX(1)}
.navbar nav a.on{color:var(--primary-text)}

/* Header treatments. */
[data-header="dark"] .navbar,[data-header="dark"] .masthead{background:var(--deep);border-color:rgb(255 255 255 / .10)}
[data-header="dark"] .navbar nav a{color:rgb(255 255 255 / .74)}
[data-header="dark"] .navbar nav a:hover,[data-header="dark"] .navbar nav a.on{color:#fff}
[data-header="dark"] .brand .bn{color:#fff}
[data-header="dark"] .brand .bs{color:rgb(255 255 255 / .55)}
[data-header="dark"] .live-card{background:rgb(255 255 255 / .06);border-color:rgb(255 255 255 / .12)}
[data-header="dark"] .live-card .t1{color:#fff}
[data-header="dark"] .live-card .t2{color:rgb(255 255 255 / .6)}
[data-header="gradient"] .masthead{
  background:linear-gradient(120deg,var(--primary) 0%,var(--accent) 100%);
  border-color:transparent;
}
[data-header="gradient"] .brand .bn{color:var(--primary-on)}
[data-header="gradient"] .brand .bs{color:color-mix(in srgb,var(--primary-on) 72%,transparent)}
[data-header="gradient"] .live-card{
  background:rgb(255 255 255 / .16);border-color:rgb(255 255 255 / .26);backdrop-filter:blur(6px);
}
[data-header="gradient"] .live-card .t1,[data-header="gradient"] .live-card .lbl{color:#fff}
[data-header="gradient"] .live-card .t2{color:rgb(255 255 255 / .72)}

.listen-btn{
  margin-left:auto;display:inline-flex;align-items:center;gap:9px;
  background:var(--primary);color:var(--primary-on);border:0;border-radius:100px;
  cursor:pointer;font-family:var(--sans);font-size:12.5px;font-weight:800;
  letter-spacing:.11em;text-transform:uppercase;padding:12px 22px;
  box-shadow:0 4px 14px -6px color-mix(in srgb,var(--primary) 70%,transparent);
  transition:transform .16s var(--ease),box-shadow .16s var(--ease),filter .16s var(--ease);
}
.listen-btn:hover{filter:brightness(1.08);transform:translateY(-1px);
  box-shadow:0 8px 20px -6px color-mix(in srgb,var(--primary) 80%,transparent)}
.listen-btn:active{transform:translateY(0)}
@media(max-width:520px){.listen-btn span{display:none}.listen-btn{padding:12px 15px}}

.nav-toggle{
  background:none;border:0;color:inherit;padding:14px 10px;cursor:pointer;display:flex;
}
@media(min-width:940px){.nav-toggle{display:none}}
[data-header="dark"] .nav-toggle{color:#fff}

.mobile-nav{
  display:none;background:var(--card);border-bottom:1px solid var(--line);
  position:sticky;top:56px;z-index:59;box-shadow:var(--shadow-1);
  max-height:70vh;overflow-y:auto;
}
.mobile-nav.open{display:block}
@media(min-width:940px){.mobile-nav.open{display:none}}
.mobile-nav ul{list-style:none;margin:0;padding:4px 0 12px}
.mobile-nav a{
  display:block;padding:14px var(--gut);font-size:15px;font-weight:700;
  color:var(--ink-2);border-bottom:1px solid var(--line-2);
}
.mobile-nav a:hover,.mobile-nav a.on{color:var(--primary-on-tint);background:var(--primary-lt)}

/* --------------------------------------------------------- 4. layout ---- */
/* ------------------------------------------------------- grid, on a phone ---
   Every `1fr` below is `minmax(0,1fr)`, and that is not pedantry.

   A bare `1fr` is `minmax(auto,1fr)`, and `auto` as a MINIMUM is the track's
   min-content width - so a column holding a long headline, or a card with a grid
   of its own, grows past the viewport and drags the whole page sideways with it.
   On a 390px phone the news page measured 559px and the schedule page 726px:
   both scrolled horizontally, which is the most obvious mobile fault there is.

   It only showed up at phone width because .layout's DESKTOP rule already said
   minmax(0,1fr) and its base rule did not.
   ------------------------------------------------------------------------- */
.layout{
  display:grid;grid-template-columns:minmax(0,1fr);gap:34px;
  padding-block:clamp(24px,3.4vw,40px);align-items:start;
}
@media(min-width:1040px){.layout{grid-template-columns:minmax(0,1fr) 320px;gap:40px}}
.layout.solo{grid-template-columns:minmax(0,1fr)}
.block{margin-bottom:var(--block)}
.block:last-child{margin-bottom:0}

.pagehead{background:var(--card);border-bottom:1px solid var(--line)}
.pagehead .wrap{padding-block:clamp(26px,4vw,48px)}
.pagehead h1{font-size:clamp(29px,5vw,46px);max-width:22ch}
.pagehead .lede{margin-top:14px}
.crumbs{
  font-size:12.5px;color:var(--ink-3);margin-bottom:12px;
  display:flex;flex-wrap:wrap;gap:7px;align-items:center;
}
.crumbs a:hover{color:var(--primary-text)}
.crumbs .sep{color:var(--line)}

.grid{display:grid;gap:var(--gap)}
.grid.c2{grid-template-columns:minmax(0,1fr)}
.grid.c3{grid-template-columns:minmax(0,1fr)}
.grid.c4{grid-template-columns:repeat(2,minmax(0,1fr))}
@media(min-width:560px){.grid.c2,.grid.c3{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(min-width:900px){.grid.c3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid.c4{grid-template-columns:repeat(4,minmax(0,1fr))}}

/* ----------------------------------------------------------- 5. hero ---- */
.hero{
  background:var(--card);border:1px solid var(--line);border-radius:var(--r);
  overflow:hidden;display:grid;grid-template-columns:minmax(0,1fr);box-shadow:var(--shadow-1);
}
@media(min-width:720px){.hero{grid-template-columns:1.02fr .98fr}}
.hero-img{position:relative;background:var(--line-2);min-height:250px}
.hero-img img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.hero-img.contain{background:var(--card)}
.hero-img.contain img{object-fit:contain;padding:clamp(20px,4vw,46px)}
.hero-body{padding:clamp(24px,3.2vw,40px);display:flex;flex-direction:column;justify-content:center}
.hero-body h1{font-size:clamp(27px,4vw,42px);margin-bottom:13px}
.hero-body .sub{color:var(--ink-2);font-size:17px;margin:0 0 22px;max-width:52ch}
.hero-meta{
  display:flex;flex-wrap:wrap;gap:8px 22px;font-size:13.5px;color:var(--ink-3);
  margin-bottom:24px;padding-top:16px;border-top:1px solid var(--line-2);
}
.hero-meta b{color:var(--ink);font-weight:700}

/* Poster hero — art bleeds behind the text, for dark/music-led themes. */
[data-hero="poster"] .hero{
  grid-template-columns:minmax(0,1fr);position:relative;min-height:clamp(340px,46vw,470px);
  border:0;background:var(--deep);
}
[data-hero="poster"] .hero-img{position:absolute;inset:0;min-height:0}
[data-hero="poster"] .hero-img img{filter:saturate(1.05)}

/* NO PICTURE YET, which is where every new station starts.
   `.contain` normally paints the panel in --card so a logo sits on a clean
   ground. In a poster hero that panel is absolutely positioned across the whole
   block, so it covered the hero's own --deep ground - and the poster's headline
   is white by design. On Bold Pop, a light theme with a poster hero, that made
   the station name white on white: completely invisible on the homepage of a
   brand-new site. On the dark poster themes it was legible but the giant
   fallback initial was drawn straight through the headline and read as a
   rendering fault.
   Both go away by letting the hero's own dark ground through and not drawing the
   initial at all. White reads on --deep in every theme by 15:1 or better. */
[data-hero="poster"] .hero-img.contain{background:transparent}
[data-hero="poster"] .hero-img.contain .art-ph{display:none}
[data-hero="poster"] .hero-img::after{
  content:"";position:absolute;inset:0;
  background:linear-gradient(to top,rgb(0 0 0 / .92) 0%,rgb(0 0 0 / .62) 42%,rgb(0 0 0 / .18) 100%);
}
[data-hero="poster"] .hero-body{
  position:relative;justify-content:flex-end;color:#fff;
  padding:clamp(28px,4vw,52px);
}
[data-hero="poster"] .hero-body h1{color:#fff;font-size:clamp(30px,5.4vw,58px)}
[data-hero="poster"] .hero-body .sub{color:rgb(255 255 255 / .82)}
[data-hero="poster"] .hero-meta{color:rgb(255 255 255 / .68);border-top-color:rgb(255 255 255 / .18)}
[data-hero="poster"] .hero-meta b{color:#fff}

/* Split hero — a calmer, friendlier arrangement. */
[data-hero="split"] .hero{background:var(--primary-lt);border-color:transparent}
[data-hero="split"] .hero-body h1{color:var(--ink)}

[data-hero="minimal"] .hero{grid-template-columns:minmax(0,1fr);box-shadow:none;background:transparent;border:0}
[data-hero="minimal"] .hero-img{display:none}
[data-hero="minimal"] .hero-body{padding-inline:0}

.btn-row{display:flex;gap:11px;flex-wrap:wrap}
.btn{
  display:inline-flex;align-items:center;justify-content:center;gap:9px;
  padding:13px 24px;border-radius:calc(var(--r) - 2px);
  font-size:14.5px;font-weight:700;border:1.5px solid transparent;cursor:pointer;
  font-family:var(--sans);text-align:center;
  transition:filter .16s var(--ease),border-color .16s var(--ease),
             background .16s var(--ease),color .16s var(--ease),transform .16s var(--ease);
}
.btn:active{transform:translateY(1px)}
.btn-primary{background:var(--primary);color:var(--primary-on)}
.btn-primary:hover{filter:brightness(1.08)}
.btn-accent{background:var(--accent);color:var(--accent-on)}
.btn-outline{border-color:var(--line);color:var(--ink);background:var(--card)}
.btn-outline:hover{border-color:var(--ink)}
.btn-dark{background:var(--deep);color:#fff}
.btn-dark:hover{filter:brightness(1.35)}
.btn-block{width:100%}
[data-hero="poster"] .hero-body .btn-outline{
  border-color:rgb(255 255 255 / .42);color:#fff;background:rgb(255 255 255 / .08);
}
[data-hero="poster"] .hero-body .btn-outline:hover{border-color:#fff;background:rgb(255 255 255 / .16)}

/* ---------------------------------------------- 6. cards & editorial ---- */
/* One card body, four treatments. Everything else in the product reuses these
   three classes, which is why the whole site looks like one hand made it. */
.card{
  background:var(--card);border:1px solid var(--line);border-radius:var(--r);
  overflow:hidden;transition:transform .2s var(--ease),box-shadow .2s var(--ease),
                             border-color .2s var(--ease);
}
[data-cards="shadow"] .card{border-color:transparent;box-shadow:var(--shadow-1)}
[data-cards="flat"] .card{border-color:transparent;background:var(--card)}
[data-cards="glass"] .card{
  border-color:rgb(255 255 255 / .10);
  background:color-mix(in srgb,var(--card) 78%,transparent);
  backdrop-filter:blur(14px) saturate(1.3);
}
.card:hover{transform:translateY(-3px);box-shadow:var(--shadow-2)}
[data-cards="outline"] .card:hover{border-color:var(--primary)}

/* Editorial card: image over body. The workhorse for news, events, shows. */
.ncard{display:flex;flex-direction:column}
.ncard .im{aspect-ratio:16/10;overflow:hidden;background:var(--line-2)}
.ncard .im img{width:100%;height:100%;object-fit:cover;transition:transform .5s var(--ease)}
.ncard:hover .im img{transform:scale(1.05)}
.ncard .bd{padding:19px 19px 21px;display:flex;flex-direction:column;flex:1}
.ncard h3{font-size:17.5px;margin-bottom:8px;line-height:1.28}
.ncard:hover h3{color:var(--primary-text)}
.ncard p{color:var(--ink-2);font-size:14.5px;margin:0 0 15px;flex:1}
.ncard time,.ncard .src{
  font-size:12px;color:var(--ink-3);letter-spacing:.04em;display:block;margin-bottom:9px;
}
.ncard .rm{font-size:13px;font-weight:700;color:var(--primary-text);display:inline-flex;gap:6px}
.ncard .rm::after{content:"\203A";font-size:15px;line-height:1}
[data-images="rounded"] .ncard .im{margin:9px 9px 0;border-radius:calc(var(--r) - 3px)}

/* Horizontal card: image beside body. Index pages and What's On. */
.hcard{display:grid;grid-template-columns:minmax(0,1fr)}
@media(min-width:620px){.hcard{grid-template-columns:250px minmax(0,1fr)}}
.hcard .im{background:var(--line-2);aspect-ratio:16/10;overflow:hidden}
@media(min-width:620px){.hcard .im{aspect-ratio:auto;min-height:100%}}
.hcard .im img{width:100%;height:100%;object-fit:cover}
.hcard .bd{padding:20px 22px 22px}
.hcard h3{font-size:19px;margin-bottom:9px;line-height:1.26}
.hcard:hover h3{color:var(--primary-text)}
.hcard p{margin:0 0 13px;color:var(--ink-2);font-size:14.5px}
.hcard .rm{font-size:13px;font-weight:700;color:var(--primary-text)}
.hlist{display:grid;gap:16px}

/* Headline row: an RSS story. No image is normal and must look intentional. */
.stories{display:grid;gap:12px}
.story{
  display:block;background:var(--card);border:1px solid var(--line);
  border-radius:var(--r);padding:17px 20px;
  transition:border-color .18s var(--ease),box-shadow .18s var(--ease),transform .18s var(--ease);
}
.story:hover{border-color:var(--primary);box-shadow:var(--shadow-1);transform:translateY(-2px)}
.story h3{font-size:17px;line-height:1.32;margin:0 0 8px}
.story:hover h3{color:var(--primary-text)}
.story p{margin:0 0 9px;font-size:14.5px;color:var(--ink-2)}
.story .src{font-size:12px;color:var(--ink-3);display:flex;align-items:center;gap:7px}
.story .src::after{content:"\2197";font-size:12px;color:var(--primary-text);margin-left:auto}
.newscredit{
  margin:18px 0 0;font-size:12.5px;color:var(--ink-3);line-height:1.55;
  padding-top:14px;border-top:1px solid var(--line);
}

/* Track card — recently played. */
.played{display:grid;gap:14px;grid-template-columns:repeat(3,minmax(0,1fr))}
@media(min-width:560px){.played{grid-template-columns:repeat(4,minmax(0,1fr))}}
@media(min-width:1000px){.played{grid-template-columns:repeat(6,minmax(0,1fr))}}
.track .art{aspect-ratio:1;position:relative;overflow:hidden}
.track .art img{width:100%;height:100%;object-fit:cover}
.track .art .art-ph{width:100%;height:100%;font-size:36px}
.track .meta{padding:12px 13px 14px}
.track .ti{font-weight:700;font-size:14px;line-height:1.3;letter-spacing:-.01em}
.track .ar{font-size:12.5px;color:var(--ink-3);margin-top:2px}
.track .tm{font-size:11px;color:var(--ink-3);margin-top:7px;letter-spacing:.05em}
.track .svc{display:flex;gap:6px;margin-top:9px}
.track .svc a{
  width:26px;height:26px;border-radius:50%;display:grid;place-items:center;
  background:var(--line-2);color:var(--ink-2);transition:background .16s var(--ease),color .16s var(--ease);
}
.track .svc a:hover{background:var(--primary);color:var(--primary-on)}
.track .svc svg{width:14px;height:14px;fill:currentColor}

/* ------------------------------------------------------- 7. schedule ---- */
.daybar{
  display:flex;gap:6px;overflow-x:auto;padding-bottom:5px;margin-bottom:22px;
  scrollbar-width:thin;-webkit-overflow-scrolling:touch;
}
.daybar button{
  flex:0 0 auto;cursor:pointer;background:var(--card);
  border:1px solid var(--line);border-radius:100px;padding:11px 20px;
  font-size:13px;font-weight:700;letter-spacing:.05em;color:var(--ink-2);
  transition:background .16s var(--ease),color .16s var(--ease),border-color .16s var(--ease);
}
.daybar button:hover{border-color:var(--ink-3)}
.daybar button[aria-selected="true"]{
  background:var(--primary);border-color:var(--primary);color:var(--primary-on);
}
.dayplan[hidden]{display:none}

.slot{
  display:grid;grid-template-columns:minmax(0,1fr);background:var(--card);
  border:1px solid var(--line);border-radius:var(--r);
  margin-bottom:11px;overflow:hidden;transition:border-color .18s var(--ease);
}
.slot:hover{border-color:var(--primary)}
@media(min-width:620px){.slot{grid-template-columns:170px minmax(0,1fr);align-items:center}}
.slot .tm{
  padding:15px 20px;font-size:13.5px;font-weight:700;color:var(--primary-text);
  background:var(--ground);border-bottom:1px solid var(--line-2);
  font-variant-numeric:tabular-nums;
}
@media(min-width:620px){
  .slot .tm{border-bottom:0;border-right:1px solid var(--line-2);height:100%;display:flex;align-items:center}
}
.slot .bd{padding:14px 20px;display:flex;align-items:center;gap:14px}
.slot .av{width:46px;height:46px;border-radius:50%;object-fit:cover;flex-shrink:0;border:2px solid var(--line-2)}
.slot .nm{font-weight:700;font-size:16px;letter-spacing:-.012em}
.slot .ps{font-size:13.5px;color:var(--ink-3)}
.slot.live{border-color:var(--primary);box-shadow:var(--shadow-1)}
.slot.live .tm{background:var(--primary-lt);color:var(--primary-on-tint)}
.slot .badge{
  margin-left:auto;flex-shrink:0;font-size:9.5px;font-weight:800;letter-spacing:.14em;
  text-transform:uppercase;color:var(--primary-text);display:flex;align-items:center;gap:6px;
}

/* On-air strip: NOW / NEXT / LATER. The homepage's most-read component. */
.onair{display:grid;gap:12px;grid-template-columns:minmax(0,1fr)}
@media(min-width:700px){.onair{grid-template-columns:repeat(3,minmax(0,1fr))}}
.oa{
  display:flex;gap:13px;align-items:center;background:var(--card);
  border:1px solid var(--line);border-radius:var(--r);padding:14px;
  transition:border-color .18s var(--ease),transform .18s var(--ease);
}
.oa:hover{border-color:var(--primary);transform:translateY(-2px)}
.oa img,.oa .art-ph{width:58px;height:58px;border-radius:calc(var(--r) - 3px);object-fit:cover;flex-shrink:0;font-size:20px}
[data-images="circle-people"] .oa img,[data-images="circle-people"] .oa .art-ph{border-radius:50%}
.oa .k{font-size:9.5px;font-weight:800;letter-spacing:.15em;text-transform:uppercase;color:var(--ink-3);display:flex;align-items:center;gap:6px}
.oa .n{font-weight:700;font-size:15.5px;letter-spacing:-.012em;margin-top:2px}
.oa .t{font-size:12.5px;color:var(--ink-3);margin-top:1px}
.oa.now{border-color:var(--primary);background:var(--primary-lt)}
.oa.now .k{color:var(--primary-on-tint)}   /* on the tint, not the page */

/* --------------------------------------------------------- 8. people ---- */
.people{display:grid;gap:var(--gap);grid-template-columns:repeat(2,minmax(0,1fr))}
@media(min-width:720px){.people{grid-template-columns:repeat(3,minmax(0,1fr))}}
@media(min-width:1060px){.people{grid-template-columns:repeat(4,minmax(0,1fr))}}
.person{text-align:center;padding:26px 16px 22px;display:block}
.person .av,.person .avph{
  width:104px;height:104px;border-radius:50%;object-fit:cover;
  margin:0 auto 15px;border:3px solid var(--line-2);
}
.person .avph{display:grid;place-items:center;font-size:34px}
.person:hover .av{border-color:var(--primary)}
.person h3{font-size:17px;margin-bottom:4px}
.person .rt{font-size:12.5px;color:var(--primary-text);font-weight:700;letter-spacing:.03em}
.person .sh{font-size:13px;color:var(--ink-3);margin-top:6px}

/* Presenter page header. */
.profile{display:grid;gap:clamp(22px,3vw,40px);grid-template-columns:minmax(0,1fr);align-items:start}
@media(min-width:760px){.profile{grid-template-columns:280px minmax(0,1fr)}}
.profile .pic{border-radius:var(--r);overflow:hidden;background:var(--line-2);aspect-ratio:4/5}
.profile .pic img{width:100%;height:100%;object-fit:cover}

/* ----------------------------------- 9. rail: weather, on-air, social --- */
.rail{display:grid;gap:24px}
@media(min-width:1040px){.rail{position:sticky;top:76px}}
.panel{background:var(--card);border:1px solid var(--line);border-radius:var(--r);overflow:hidden}
[data-cards="shadow"] .panel{border-color:transparent;box-shadow:var(--shadow-1)}
.panel-hd{
  padding:15px 19px;border-bottom:1px solid var(--line-2);
  display:flex;align-items:center;justify-content:space-between;gap:10px;
}
/* h2 as well as h3: the sidebar panels are h2 now (they are top-level
   landmarks beside the main content, and as h3 they made every page
   without an h2 in <main> skip a heading level). The size is set here, so
   the tag changed and the look did not. */
.panel-hd h2,.panel-hd h3{font-size:15.5px;font-weight:700;letter-spacing:-.015em}
.panel-hd .sp{font-size:11px;color:var(--ink-3)}
.panel-bd{padding:19px}

.sched a{
  display:flex;gap:13px;align-items:center;padding:13px 19px;
  border-bottom:1px solid var(--line-2);transition:background .16s var(--ease);
}
.sched a:last-child{border-bottom:0}
.sched a:hover{background:var(--ground)}
.sched img,.sched .art-ph{
  width:46px;height:46px;border-radius:50%;object-fit:cover;flex-shrink:0;
  border:2px solid var(--line-2);font-size:16px;
}
.sched .nm{font-weight:700;font-size:14.5px;letter-spacing:-.01em}
.sched .tmz{font-size:12.5px;color:var(--primary-text);font-weight:600}
.sched .now{background:var(--primary-lt)}

.wx-now{display:flex;align-items:center;gap:16px;margin-bottom:16px}
.wx-now .tmp{font-size:42px;font-weight:800;letter-spacing:-.04em;line-height:1;font-family:var(--display)}
.wx-now .ic{font-size:34px;line-height:1}
.wx-now .ds{font-size:13.5px;color:var(--ink-3)}
.wx-days{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px;text-align:center}
.wx-days div{background:var(--ground);border-radius:calc(var(--r) - 3px);padding:11px 4px}
.wx-days .d{font-size:10.5px;letter-spacing:.09em;text-transform:uppercase;color:var(--ink-3)}
.wx-days .i{font-size:17px;margin:4px 0 2px}
.wx-days .t{font-size:14px;font-weight:700}
.wx-credit{margin:14px 0 0;font-size:11px;color:var(--ink-3);text-align:center}
.wx-credit a{text-decoration:underline;text-underline-offset:2px}
.wx-loading .wx-now,.wx-loading .wx-days{opacity:.45}

.soc{display:flex;gap:9px;flex-wrap:wrap}
.soc a{
  flex:1;min-width:42px;aspect-ratio:1;border-radius:calc(var(--r) - 3px);
  display:grid;place-items:center;background:var(--ground);
  border:1px solid var(--line);color:var(--ink-2);
  transition:background .16s var(--ease),color .16s var(--ease),border-color .16s var(--ease);
}
.soc a:hover{background:var(--primary);border-color:var(--primary);color:var(--primary-on)}
.soc svg{width:18px;height:18px;fill:currentColor}

/* Travel links: to the people who hold the data, not our own incident feed. */
.tgrid{display:grid;gap:10px}
@media(min-width:620px){.tgrid{grid-template-columns:repeat(2,minmax(0,1fr))}}
.tlink{
  display:flex;align-items:flex-start;gap:11px;background:var(--ground);
  border:1px solid var(--line-2);border-radius:calc(var(--r) - 2px);padding:12px 14px;
  transition:border-color .16s var(--ease),background .16s var(--ease);
}
.tlink:hover{border-color:var(--primary);background:var(--card)}
.tlink .ic{font-size:17px;line-height:1.3;flex-shrink:0}
.tlink .nm{display:block;font-weight:700;font-size:14px}
.tlink .nm::after{content:"\2197";font-size:11px;color:var(--primary-text);margin-left:6px}
.tlink .ds{display:block;font-size:12.5px;color:var(--ink-3);line-height:1.42;margin-top:2px}
.tnote{margin:0 0 15px;font-size:13px;color:var(--ink-3);line-height:1.5;max-width:64ch}

/* ---------------------------------------------------- 10. player bar ---- */
/* Fixed, persistent, and never re-mounted on navigation — see static/js/app.js.
   This bar is the product; everything else is a page around it. */
.player{
  position:fixed;left:0;right:0;bottom:0;z-index:90;
  background:var(--deep);color:#fff;
  border-top:1px solid rgb(255 255 255 / .10);
  padding-bottom:env(safe-area-inset-bottom,0px);
  transition:transform .3s var(--ease);
}
[data-player="light"] .player{
  background:var(--card);color:var(--ink);border-top-color:var(--line);
  box-shadow:0 -4px 20px -12px rgb(0 0 0 / .35);
}
[data-player="vivid"] .player{
  background:linear-gradient(100deg,var(--primary) 0%,var(--accent) 130%);
  color:var(--primary-on);border-top-color:transparent;
}
.player .wrap{display:flex;align-items:center;gap:14px;height:var(--player-h)}

.pbtn{
  width:48px;height:48px;flex-shrink:0;border-radius:50%;border:0;cursor:pointer;
  background:var(--primary);color:var(--primary-on);display:grid;place-items:center;
  transition:transform .16s var(--ease),filter .16s var(--ease);
}
[data-player="vivid"] .pbtn{background:#fff;color:#111}
.pbtn:hover{transform:scale(1.06);filter:brightness(1.08)}
.pbtn svg{width:19px;height:19px;fill:currentColor;margin-left:1px}
.pbtn[data-playing="true"] svg{margin-left:0}
/* Buffering: the ring turns while we wait, so a slow stream looks like it is
   working rather than broken. */
.pbtn.busy::after{
  content:"";position:absolute;width:60px;height:60px;border-radius:50%;
  border:2px solid transparent;border-top-color:currentColor;opacity:.55;
  animation:spin .8s linear infinite;
}
.pbtn{position:relative}
@keyframes spin{to{transform:rotate(360deg)}}

.pnow{display:flex;align-items:center;gap:12px;min-width:0;flex:1}
.pnow .pa{width:48px;height:48px;border-radius:8px;object-fit:cover;flex-shrink:0;background:rgb(255 255 255 / .1)}
.pnow .pa.art-ph{font-size:18px}
.pnow .pt{min-width:0}
.pnow .p1{
  font-weight:700;font-size:14.5px;letter-spacing:-.01em;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
.pnow .p2{
  font-size:12.5px;opacity:.7;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
}
[data-player="light"] .pnow .p2{opacity:1;color:var(--ink-3)}
.pstat{
  font-size:9.5px;font-weight:800;letter-spacing:.15em;text-transform:uppercase;
  display:flex;align-items:center;gap:6px;opacity:.85;
}
.pstat .dot{background:#ff4d4d}
.player.on .pstat .dot{background:#2ecc71}

/* ---- listen again ------------------------------------------------------
   The player in on-demand mode. Everything here is hidden by default and only
   the JS shows it, so a station with listen again switched off carries a few
   hundred bytes of CSS and no markup they can see. */
/* THE SCRUBBER. Full width of the window on purpose: the longer the track the
   finer the control, and these are three-hour programmes. A 3px line was
   technically draggable and nobody would ever have found it. */
.pseekwrap{position:absolute;top:0;left:0;right:0;height:0}
.pseekwrap[hidden]{display:none}
.pseek{
  position:relative;height:5px;cursor:pointer;touch-action:none;
  background:rgb(255 255 255 / .18);
}
[data-player="light"] .pseek{background:var(--line-2)}
/* The hit area is 18px tall while the line stays 5px: a track you can grab
   without having to aim. Fitts's law, not decoration. */
.pseek::after{content:"";position:absolute;left:0;right:0;top:-7px;height:19px}
.pseek .pbuf,
.pseek .pfill{position:absolute;left:0;top:0;height:100%;width:0}
.pseek .pbuf{background:rgb(255 255 255 / .22)}
[data-player="light"] .pseek .pbuf{background:var(--line)}
.pseek .pfill{background:var(--primary);transition:width .12s linear}
[data-player="vivid"] .pseek .pfill{background:#fff}
.pseek.dragging .pfill{transition:none}

/* The handle appears on hover, drag or focus. Visible at rest it reads as a
   control fighting the bar; absent entirely there is nothing to grab. */
.pseek .phandle{
  position:absolute;top:50%;left:0;width:13px;height:13px;border-radius:50%;
  background:var(--primary);transform:translate(-50%,-50%) scale(0);
  transition:transform .14s var(--ease);pointer-events:none;
  box-shadow:0 0 0 3px rgb(0 0 0 / .22);
}
[data-player="vivid"] .pseek .phandle{background:#fff}
.pseek:hover .phandle,
.pseek:focus-visible .phandle,
.pseek.dragging .phandle{transform:translate(-50%,-50%) scale(1)}
.pseek:focus-visible{outline:2px solid var(--primary);outline-offset:3px}

/* Where in the programme, and the time of day it went out - because nobody
   thinks "one hour thirty-two minutes in", they think "around half four". */
.pbubble{
  position:absolute;bottom:14px;transform:translateX(-50%);white-space:nowrap;
  background:var(--ink,#111);color:var(--card,#fff);border-radius:7px;
  padding:6px 10px;font-size:12px;font-weight:700;pointer-events:none;
  font-variant-numeric:tabular-nums;box-shadow:var(--shadow-1,0 4px 14px rgb(0 0 0/.3));
}
.pbubble[hidden]{display:none}

/* Wound back: the fill is the part of the hour already behind them, so the
   handle crawls rightwards towards live as they listen. */
.player.rewound .pfill{opacity:.85}
/* Live with a recording behind us: the whole bar is filled, because
   everything up to now has already happened. */
.player.scrubbing:not(.rewound):not(.ondemand) .pfill{opacity:.55}

/* Skip. Thirty seconds is the size of the thing somebody is hunting for. */
/* Label BESIDE the arrow, not inside it. Stacked, the 30 sat on top of the
   arrowhead and both became a smudge at 21px - the number is the whole point
   of the button, so it has to be readable. */
.pskip{
  display:inline-flex;align-items:center;gap:5px;flex-shrink:0;cursor:pointer;
  height:36px;padding:0 11px;border-radius:100px;border:0;background:none;
  color:inherit;transition:background .16s var(--ease);
}
.pskip[hidden]{display:none}
.pskip:hover{background:rgb(255 255 255 / .12)}
[data-player="light"] .pskip:hover{background:var(--ground)}
.pskip svg{width:18px;height:18px;fill:currentColor;flex-shrink:0}
.pskip span{font-size:11.5px;font-weight:800;letter-spacing:-.01em;opacity:.9}
.pskip:focus-visible{outline:2px solid var(--primary);outline-offset:2px}
/* On a phone the bar is tight: the play button and the scrubber matter more
   than the skips, which the drag already covers. */
@media(max-width:520px){.pskip{display:none}}

.ptime{
  font-size:12px;font-variant-numeric:tabular-nums;opacity:.75;flex-shrink:0;
  letter-spacing:.01em;
}
.ptime[hidden]{display:none}

.plive{
  display:flex;align-items:center;gap:7px;flex-shrink:0;cursor:pointer;
  background:rgb(255 255 255 / .12);border:1px solid rgb(255 255 255 / .2);
  color:inherit;border-radius:100px;padding:7px 14px;font-size:12px;font-weight:700;
  transition:background .16s var(--ease);
}
.plive[hidden]{display:none}
.plive:hover{background:rgb(255 255 255 / .2)}
[data-player="light"] .plive{background:var(--ground);border-color:var(--line)}
.plive .dot{width:7px;height:7px;border-radius:50%;background:#ff4d4d;flex-shrink:0}
/* On demand, the live indicator is not the truth, so it goes. */
.player.ondemand .pstat{display:none}

/* The listen again page: the same row shape as the schedule, so the two pages
   read as one product rather than as two features. */
.agday{margin:26px 0 12px;font-size:19px}
.agrow{
  display:flex;align-items:center;gap:14px;background:var(--card);
  border:1px solid var(--line);border-radius:var(--r);padding:14px 18px;
  margin-bottom:10px;transition:border-color .18s var(--ease);flex-wrap:wrap;
}
.agrow:hover{border-color:var(--primary)}
.agrow .tm{
  font-size:13.5px;font-weight:700;color:var(--primary-text);
  font-variant-numeric:tabular-nums;flex-shrink:0;min-width:52px;
}
.agrow .bd{display:flex;flex-direction:column;gap:2px;flex:1;min-width:150px}
.agrow .nm{font-weight:700;font-size:15.5px;letter-spacing:-.012em}
.agrow .ps{font-size:13px;color:var(--ink-3)}
.agrow .again{position:static;transform:none;margin:0;width:auto}

/* NOT flex. Flex makes every text node its own item, so `gap` pushed the full
   stop away from the link it belongs to and the line read "...last 7 days .".
   This is a sentence, so it is laid out as one. */
/* A station's own words inside a page that is otherwise ours. Styled as body
   copy rather than as a call-out: it IS the page's opening paragraph, not an
   aside, and a box round it would say otherwise. */
.ownwords{margin:0 0 22px;color:var(--ink-2,var(--ink));line-height:1.65}
.ownwords > :first-child{margin-top:0}
.ownwords > :last-child{margin-bottom:0}
.ownwords p{margin:0 0 12px}
.ownwords ul,.ownwords ol{margin:0 0 12px;padding-left:22px}
.ownwords li{margin-bottom:5px}
.ownwords h2,.ownwords h3{margin:20px 0 8px;font-size:18px;letter-spacing:-.015em}
.ownwords a{color:var(--primary-text);font-weight:600}

/* The noticeboard's feature. This one IS a call-out - it is the station
   asking for something, not part of the page's own prose. */
.focus{
  background:var(--primary-lt,var(--ground));border:1px solid var(--line-2);
  border-left:4px solid var(--primary);border-radius:var(--r);
  padding:clamp(20px,3vw,28px);margin:0 0 26px;
}
.focus h2{margin:0 0 10px;font-size:20px;letter-spacing:-.02em}
.focus .ownwords{margin-bottom:0}
.focus .btn{margin-top:16px}

.againnote{margin:0 0 16px;font-size:14px;color:var(--ink-3);line-height:1.5}
.againnote .ic{margin-right:7px;opacity:.6}
.againnote a{color:var(--primary-text);font-weight:700}

/* The button on a schedule row. */
.slotwrap{position:relative}
.again{
  position:absolute;right:14px;top:50%;transform:translateY(-50%);
  display:inline-flex;align-items:center;gap:7px;cursor:pointer;
  background:var(--ground);border:1px solid var(--line);color:var(--primary-text);
  border-radius:100px;padding:8px 14px;font-size:12.5px;font-weight:700;
  transition:border-color .16s var(--ease),background .16s var(--ease);
}
.again:hover{border-color:var(--primary);background:var(--primary-lt)}
.again:focus-visible{outline:2px solid var(--primary);outline-offset:2px}
.again svg{width:14px;height:14px;fill:currentColor;flex-shrink:0}
/* Room for it, so a long programme title never runs underneath the button. */
.slot.has-again .bd{padding-right:150px}
/* On a phone the row is stacked and there is no room beside it, so the button
   goes back into the flow under the programme rather than sitting on top of
   the title. */
@media(max-width:619px){
  .again{position:static;transform:none;margin:0 20px 14px;width:calc(100% - 40px);justify-content:center}
  .slot.has-again .bd{padding-right:20px;padding-bottom:10px}
}

.pvol{display:none;align-items:center;gap:9px;flex-shrink:0}
@media(min-width:820px){.pvol{display:flex}}
.pvol input[type=range]{
  width:98px;accent-color:var(--primary);cursor:pointer;background:transparent;
}
[data-player="vivid"] .pvol input[type=range]{accent-color:#fff}
.pvol button{background:none;border:0;color:inherit;cursor:pointer;padding:6px;display:grid;place-items:center}
.pvol svg{width:18px;height:18px;fill:currentColor}

.pchan{
  display:none;flex-shrink:0;background:rgb(255 255 255 / .12);
  border:1px solid rgb(255 255 255 / .18);border-radius:100px;padding:3px;gap:2px;
}
@media(min-width:1000px){.pchan{display:flex}}
.pchan button{
  background:none;border:0;color:inherit;cursor:pointer;border-radius:100px;
  padding:7px 14px;font-size:12px;font-weight:700;opacity:.7;
}
.pchan button[aria-pressed="true"]{background:#fff;color:#111;opacity:1}
[data-player="light"] .pchan{background:var(--ground);border-color:var(--line)}
[data-player="light"] .pchan button[aria-pressed="true"]{background:var(--primary);color:var(--primary-on)}

@media(max-width:520px){
  .player .wrap{gap:11px}
  .pnow .pa{width:42px;height:42px}
  .pbtn{width:44px;height:44px}
}

/* ------------------------------- 11. forms, sponsors, footer, utils ----- */
.form{display:grid;gap:16px}
.field{display:grid;gap:6px}
.field label{font-size:13.5px;font-weight:700}
.field .hint{font-size:12.5px;color:var(--ink-3)}
.field input,.field select,.field textarea{
  width:100%;padding:12px 14px;border:1.5px solid var(--line);
  border-radius:calc(var(--r) - 2px);background:var(--card);color:var(--ink);
  transition:border-color .16s var(--ease),box-shadow .16s var(--ease);
}
.field input:focus,.field select:focus,.field textarea:focus{
  border-color:var(--primary);
  box-shadow:0 0 0 3px color-mix(in srgb,var(--primary) 18%,transparent);
  outline:none;
}
.field textarea{min-height:132px;resize:vertical}
.field.err input,.field.err textarea,.field.err select{border-color:#DC2626}
.field .msg{font-size:12.5px;color:#DC2626;font-weight:600}
.check{display:flex;gap:10px;align-items:flex-start;font-size:14px;line-height:1.45}
.check input{width:18px;height:18px;margin-top:2px;flex-shrink:0;accent-color:var(--primary)}
.form-row{display:grid;gap:16px;grid-template-columns:minmax(0,1fr)}
@media(min-width:620px){.form-row{grid-template-columns:repeat(2,minmax(0,1fr))}}
.formnote{font-size:14px;padding:13px 16px;border-radius:calc(var(--r) - 2px);margin:0}
.formnote.ok{background:var(--accent-lt);color:var(--accent-on-tint)}
.formnote.bad{background:#FEF2F2;color:#991B1B}

.sponsors{background:var(--card);border-top:1px solid var(--line);padding-block:26px}
.sponsors .lbl{
  text-align:center;font-size:10px;font-weight:800;letter-spacing:.2em;
  text-transform:uppercase;color:var(--ink-3);margin-bottom:18px;
}
.sponsor-wall{
  display:flex;flex-wrap:wrap;justify-content:center;align-items:center;
  gap:clamp(20px,4vw,52px);
}
.sponsor-wall a{display:block;opacity:.72;transition:opacity .2s var(--ease),transform .2s var(--ease)}
.sponsor-wall a:hover{opacity:1;transform:translateY(-2px)}
.sponsor-wall img{max-height:46px;width:auto;background:none}
.sponsored-by{font-size:11px;color:var(--ink-3);letter-spacing:.03em}
.sponsored-by b{color:var(--ink-2);font-weight:700}

/* --------------------------------------------------- injected elements ---
   Browser extensions append their own hosts to the end of <body>: screen
   recorders, ad blockers, coupon toolbars, password managers. They are usually
   an EMPTY element (their UI lives in a shadow root) and they are usually in
   normal flow, so a host that happens to be sized adds its full height to the
   document — below the footer, where it reads as a screen of blank space the
   site forgot to fill. One real example, measured: a recorder adding exactly
   one viewport height, 889px, under the footer of every page.

   We cannot remove somebody else's element and should not try. Taking it out
   of flow is enough: it stops contributing height, and its own UI is
   positioned from inside its shadow root either way.

   Deliberately narrow — only an element AFTER the footer, only a `div`, and
   only one that is `:empty`, which a shadow host with no light-DOM children
   is. Our own trailing elements (the player, the cookie note, studio mode's
   drawer) all have content, so none of them match. */
body > .footer ~ div:empty{
  position:absolute;
}

/* The footer reserves the room for the fixed player, rather than <body>
   doing it. Body padding left a strip of PAGE colour under the dark footer -
   which on a short page read as the site simply stopping halfway down. Put
   the space inside the footer and the colour runs to the player. */
.footer{
  background:var(--deep);color:rgb(255 255 255 / .72);margin-top:var(--block);
  padding-block:clamp(34px,5vw,56px)
    calc(26px + var(--player-h) + env(safe-area-inset-bottom,0px));
}
/* No player configured: no room to reserve. */
body:not(:has(.player)) .footer{padding-block-end:26px}
.footer .cols{display:grid;gap:32px;grid-template-columns:minmax(0,1fr)}
@media(min-width:700px){.footer .cols{grid-template-columns:1.4fr minmax(0,1fr) minmax(0,1fr)}}
@media(min-width:1000px){.footer .cols{grid-template-columns:1.6fr minmax(0,1fr) minmax(0,1fr) minmax(0,1fr)}}
.footer h4{color:#fff;font-size:12px;letter-spacing:.15em;text-transform:uppercase;margin-bottom:14px}
.footer a:hover{color:#fff;text-decoration:underline}
.footer ul{list-style:none;margin:0;padding:0;display:grid;gap:9px;font-size:14px}
.footer .fbrand img{height:56px;margin-bottom:14px}
.footer .fbrand p{font-size:14px;max-width:38ch}
.footer .legal{
  margin-top:34px;padding-top:20px;border-top:1px solid rgb(255 255 255 / .12);
  display:flex;flex-wrap:wrap;gap:10px 22px;justify-content:space-between;
  font-size:12.5px;color:rgb(255 255 255 / .5);}
.footer .legal .made{margin-inline-start:auto}
.footer .legal .made a{color:rgb(255 255 255 / .8);text-decoration:underline;
  text-underline-offset:2px}
.footer .legal .made a:hover{color:#fff}
@media(max-width:700px){.footer .legal .made{margin-inline-start:0}}

.empty{
  text-align:center;padding:clamp(34px,6vw,64px) var(--gut);
  border:1.5px dashed var(--line);border-radius:var(--r);color:var(--ink-3);
}
.empty h3{color:var(--ink);font-size:19px;margin-bottom:8px}

.pager{display:flex;gap:8px;justify-content:center;margin-top:var(--gap);flex-wrap:wrap}
.pager a,.pager span{
  padding:10px 15px;border:1px solid var(--line);border-radius:calc(var(--r) - 2px);
  font-size:14px;font-weight:600;background:var(--card);
}
.pager .on{background:var(--primary);border-color:var(--primary);color:var(--primary-on)}
.pager .off{opacity:.45}

.prose{max-width:72ch;font-size:16.5px;color:var(--ink-2)}
.prose h2{font-size:26px;margin:1.6em 0 .5em;color:var(--ink)}
.prose h3{font-size:20px;margin:1.5em 0 .45em;color:var(--ink)}
.prose a{color:var(--primary-text);text-decoration:underline;text-underline-offset:2px}
.prose ul,.prose ol{padding-left:1.3em;margin:0 0 1em}
.prose li{margin-bottom:.4em}
.prose img{border-radius:var(--r);margin:1.4em 0}
.prose blockquote{
  margin:1.5em 0;padding:4px 0 4px 22px;border-left:3px solid var(--primary);
  font-size:19px;color:var(--ink);font-style:italic;
}
.prose iframe{width:100%;aspect-ratio:16/9;height:auto;border:0;border-radius:var(--r);margin:1.4em 0}

.tags{display:flex;flex-wrap:wrap;gap:7px}
.tag{
  font-size:12px;font-weight:600;padding:5px 11px;border-radius:100px;
  background:var(--line-2);color:var(--ink-2);
}
.tag:hover{background:var(--primary);color:var(--primary-on)}

.cookie{
  position:fixed;left:var(--gut);right:var(--gut);z-index:95;
  bottom:calc(var(--player-h) + 14px + env(safe-area-inset-bottom,0px));
  max-width:520px;background:var(--card);border:1px solid var(--line);
  border-radius:var(--r);box-shadow:var(--shadow-2);padding:18px 20px;
  display:flex;flex-direction:column;gap:12px;font-size:14px;
}
.cookie[hidden]{display:none}

/* The page-swap transition. Deliberately tiny: 120ms is enough to feel
   deliberate and short enough that nobody waiting for content notices it. */
/* The page's own background lives here now, because <body> is painted in the
   footer's colour. `min-height` keeps a short page — a 404, an empty search —
   from showing the dark through the middle. */
main{
  animation:fade .16s var(--ease);
  background:var(--ground);
  min-height:40vh;
}
@keyframes fade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
body.swapping main{opacity:.4;transition:opacity .1s linear}

@media print{
  .player,.navbar,.mobile-nav,.listen-btn,.cookie,.sponsors{display:none !important}
  body{padding-bottom:0;background:#fff;color:#000}
  .card,.panel{border-color:#ccc;box-shadow:none}
}

/* A field the server said was wrong. Marked as well as focused, because the
   focus ring alone is gone the moment the person clicks somewhere else. */
.field.bad input,.field.bad textarea,.field.bad select{
  border-color:var(--bad, #B42318);
  box-shadow:0 0 0 3px color-mix(in srgb, var(--bad, #B42318) 16%, transparent);
}
.field.bad label{color:var(--bad, #B42318)}

/* ---------------------------------------------------------- tap targets ---
   WCAG 2.2 (2.5.8, AA) asks for 24x24 of target. These were all a few pixels
   short, and every one of them is in a place you reach for on a phone: the
   footer link lists at 18px, the legal strip at 16px, the weather credit at
   14px, a podcast episode heading at 23px.

   Padding rather than font size, so nothing about the design changes - the
   links simply become possible to hit. Measured by the responsive tests. */
.footer ul li a,
.footer .fbrand a,
.footer p a,
.legal a,
.wx-credit a,
.newscredit a{
  display:inline-flex;align-items:center;min-height:24px;
}
/* A list of links is also easier to hit with a little air between the rows. */
.footer ul li{margin-bottom:2px}
.legal a{padding-inline:2px}

/* A heading that is a link needs the same. One pixel short is still short. */
.ncard h3 a,.hcard h3 a,.story h3 a,.card h3 a{
  display:inline-flex;align-items:center;min-height:24px;
}

/* ------------------------------------------------------- the station app --
   The install card. Bottom of the screen, out of the way, and gone for good
   once dismissed - an install prompt that keeps coming back is an advert. */
.install-card{
  position:fixed;left:12px;right:12px;bottom:calc(var(--player-h,74px) + 12px);
  z-index:60;display:flex;gap:14px;align-items:center;flex-wrap:wrap;
  background:var(--card);border:1px solid var(--line);border-radius:var(--r);
  box-shadow:0 12px 40px rgb(0 0 0 / .18);padding:14px 16px;
  max-width:560px;margin-inline:auto;
}
.install-card[hidden]{display:none !important}
.install-card .ic-b{display:flex;gap:13px;align-items:center;flex:1;min-width:220px}
.install-card img{border-radius:10px;flex:none}
.install-card b{display:block;font-size:14.5px}
.install-card span{display:block;font-size:13px;color:var(--ink-3);line-height:1.45}
.install-card .ios{margin-top:6px;color:var(--ink-2)}
.install-card .ic-a{display:flex;gap:8px;margin-left:auto}

.notify{margin-top:14px;padding-top:14px;border-top:1px solid var(--line)}

/* A presenter's photograph standing in for missing show artwork.
   Circular, and cropped from the TOP rather than the centre: a portrait
   centre-cropped into a square takes the chin and loses the eyes, which is
   the one thing a listener recognises. */
/* Only the crop, never the size. Setting width/aspect-ratio here fought the
   sizes the cards already give their pictures and pushed them off to one side
   at odd heights - and on the schedule it was pointless anyway, because
   .slot .av is a circle already. A class that changes layout has to be opted
   into by the layout, not imposed on it. */
.art-face{
  border-radius:50%;
  object-fit:cover;
  object-position:50% 22%;
}

/* What replaces a form once it has sent. The form used to stay on screen with
   a line underneath, which reads as "nothing happened" - people sent the same
   message two or three times and a presenter got it three times. */
.formdone{
  display:grid;gap:14px;justify-items:start;
  padding:clamp(20px,3vw,30px);
  border:1px solid var(--line-2);border-radius:var(--r);
  background:var(--primary-lt,var(--panel));
}
.formdone:focus{outline:2px solid var(--primary);outline-offset:3px}
.formdone-hd{margin:0;font-weight:700;font-size:17px;color:var(--ink)}
