/* ==========================================================================
   Fort Collins Public Media
   One stylesheet. No preprocessor, no framework, no build step.

   Design intent: this is a broadcast facility, not a startup. The look is
   editorial and high-contrast — heavy type, generous space, one signal color
   used sparingly for anything live or actionable. No gradients, no drop
   shadows, no rounded-everything. Rules and blocks do the structural work.

   To rebrand: change the tokens in :root. Nothing below hardcodes a color.
   ========================================================================== */

/* --- Tokens --------------------------------------------------------------- */

:root {
  /* Palette — the brand sheet.
     ------------------------------------------------------------------
     THE ONE RULE: yellow is a SURFACE, never a text colour on paper.
     Yellow on paper is 1.4:1, which is not a near miss — it is invisible.
     On ink it is 11.7:1, which is why the masthead is a dark band and why
     the plate always carries ink on top of it rather than the reverse.

     There is a test for this. script/test_tokens.py refuses `color:
     var(--signal)` anywhere in this file, because the failure mode is a
     rule that looks fine to whoever wrote it and is unreadable to
     everybody else. */
  --ink:        #121417;  /* body text, and the dark surfaces */
  --slate:      #232830;  /* the second dark surface, one step up from ink */
  --ink-soft:   #545c66;  /* secondary text */
  --paper:      #f4f1ea;  /* page background — warm, not white */
  --paper-alt:  #e8e3d8;  /* banded sections */
  --rule:       #d5cfc2;  /* hairlines */

  /* The plate. Fills, marks, and the current-page underline on dark.
     Never `color:` on paper. */
  --signal:     #ffc61a;
  --signal-ink: #121417;  /* what sits on top of yellow — 11.7:1 */

  /* On-air red. Two of them on purpose: the brand red is right for fills
     and large marks and is only 4.1:1 on paper, which fails AA for the
     small bold labels that need it most — an on-air badge is exactly the
     text you cannot afford to make people squint at. The -ink variant is
     the same hue walked dark enough to read at 6.3:1. */
  --record:     #d93a26;
  --record-ink: #a52c1d;

  /* The masthead band. Fixed dark in BOTH schemes, unlike --ink, which
     inverts. The lockup is drawn on near-black in the brand sheet and the
     plate only reaches 11.7:1 on a dark ground, so this is the one surface
     that does not get to flip. */
  --masthead:      #121417;
  --masthead-ink:  #f4f1ea;
  --masthead-soft: #9ba3ad;  /* secondary text on the band — 7.2:1 */

  /* Links. Ink with an underline, picking up red on hover — the palette
     has no fifth colour and does not need one. */
  --accent:     #121417;

  /* Type. Two faces, self-hosted — see the @font-face block below.
     Georgia and the system sans lead the fallbacks because they are the
     closest metric match to what is loading; with font-display: swap the
     page renders in them first, and a poor match is what makes headlines
     visibly jump when the real face arrives. */
  --font-display: "Source Serif 4", Georgia, "Times New Roman", serif;
  --font-body:    Archivo, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono:    ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;

  /* Fluid type scale */
  --step--1: clamp(0.86rem, 0.83rem + 0.15vw, 0.94rem);
  --step-0:  clamp(1.02rem, 0.98rem + 0.20vw, 1.13rem);
  --step-1:  clamp(1.28rem, 1.18rem + 0.45vw, 1.55rem);
  --step-2:  clamp(1.60rem, 1.40rem + 0.95vw, 2.15rem);
  --step-3:  clamp(2.00rem, 1.60rem + 1.90vw, 3.10rem);
  --step-4:  clamp(2.50rem, 1.75rem + 3.40vw, 4.60rem);

  /* Space and measure */
  --wrap: 68rem;
  --measure: 38rem;
  --gap: clamp(1rem, 0.7rem + 1.4vw, 2rem);
}

@media (prefers-color-scheme: dark) {
  :root {
    --ink:       #ece8e1;
    --slate:     #2b313a;
    --ink-soft:  #a1a8b2;
    --paper:     #121417;
    --paper-alt: #1c2126;
    --rule:      #2f363d;

    /* Yellow survives the inversion unchanged, which is the whole appeal of
       it — the plate is the same plate in both schemes. What changes is that
       here it is finally safe as a text colour too. */
    --signal:     #ffc61a;
    --signal-ink: #121417;

    /* Walked light instead of dark, for the same reason and by the same
       amount: 4.0:1 against this background is not enough for a badge. */
    --record:     #d93a26;
    --record-ink: #e16151;

    --accent:     #ece8e1;
  }
}

/* --- Fonts ---------------------------------------------------------------
   Self-hosted rather than fetched from a CDN. The site has no build step and
   no npm, and it should have no third party in the render path either — a
   font host is one more thing that can be slow, blocked, or watching.

   Both are variable, so one file covers every weight: 34 KB for Archivo and
   119 KB for Source Serif. `swap` means text is readable immediately in the
   fallback and never invisible while they load.
   -------------------------------------------------------------------------- */

@font-face {
  font-family: "Archivo";
  src: url("/assets/fonts/archivo.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "Source Serif 4";
  src: url("/assets/fonts/source-serif-4.woff2") format("woff2");
  font-weight: 200 900;
  font-style: normal;
  font-display: swap;
}

/* --- Base ----------------------------------------------------------------- */

*, *::before, *::after { box-sizing: border-box; }

/* The browser hides [hidden] with `display: none` at the lowest priority, so
   ANY rule that sets display — .btn, .filter, a flex container — silently
   un-hides it. That has bitten this file eight times already, which is why
   there are eight `.something[hidden] { display: none }` rules further down.
   This is the one that stops the ninth. */
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--step-0);
  line-height: 1.6;
  text-underline-offset: 0.18em;
}

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 750;
  line-height: 1.08;
  letter-spacing: -0.022em;
  margin: 0 0 0.5em;
  text-wrap: balance;
}

h1 { font-size: var(--step-3); }
h2 { font-size: var(--step-2); }
h3 { font-size: var(--step-1); }

p, ul, ol, dl, table { margin: 0 0 1.1em; }

a { color: var(--accent); text-decoration-thickness: 1px; }
a:hover { color: var(--record-ink); }

img { max-width: 100%; height: auto; display: block; }

/* URLs and paths in code spans are long unbreakable tokens; without this they
   push the page sideways on a phone. */
code { font-family: var(--font-mono); font-size: 0.9em; overflow-wrap: anywhere; }

/* A block of text meant to be copied rather than read — a device record, a
   key. Scrolls inside itself: a long unbroken token must not be what makes
   the whole page pan sideways on a phone. */
.code-block {
  margin: 0 0 1.5em;
  padding: 1rem;
  background: var(--paper-sunk, rgba(0, 0, 0, 0.04));
  border: 1px solid var(--rule);
  border-radius: 4px;
  overflow-x: auto;
  max-width: 100%;
}
.code-block code {
  white-space: pre;
  overflow-wrap: normal;
  font-size: var(--step--1);
}

hr { border: 0; border-top: 1px solid var(--rule); margin: 2.5rem 0; }

:focus-visible { outline: 3px solid var(--signal); outline-offset: 2px; }

.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--signal); color: var(--signal-ink);
  padding: 0.75rem 1.25rem; font-weight: 600;
}
.skip-link:focus { left: 0; }

/* --- Layout --------------------------------------------------------------- */

.wrap {
  width: min(100% - 2.5rem, var(--wrap));
  margin-inline: auto;
}

/* Prose holds text to a comfortable reading measure, but grids and data
   tables are allowed the full column width. Hence the measure lives on the
   text elements rather than on the container. */
.prose { max-width: 100%; }
.prose > p,
.prose > h2,
.prose > h3,
.prose > blockquote,
.prose > .transaction,
.prose > ul:not(.grid):not(.rows),
.prose > ol { max-width: var(--measure); }

.prose > :last-child { margin-bottom: 0; }
.prose h2 { margin-top: 2em; }
.prose h3 { margin-top: 1.6em; }
.prose li { margin-bottom: 0.35em; }

.muted { color: var(--ink-soft); }
.lede  { font-size: var(--step-1); color: var(--ink-soft); max-width: var(--measure); line-height: 1.4; }

.eyebrow {
  font-size: var(--step--1);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  font-weight: 650;
  color: var(--ink-soft);
  margin-bottom: 0.6rem;
}

.band { background: var(--paper-alt); padding: clamp(2.5rem, 6vw, 5rem) 0; margin-top: 4rem; }
/* On a dark band the secondary tokens have to flip too, or every muted
   element renders dark-on-dark. Redefining them here means components inside
   need no special cases. */
.band-ink {
  background: var(--ink);
  color: var(--paper);
  --ink-soft: color-mix(in srgb, var(--paper) 66%, transparent);
  --rule: color-mix(in srgb, var(--paper) 24%, transparent);
}
.band-ink h2 { color: var(--paper); }
.band-ink a { color: var(--paper); }

/* --- Header --------------------------------------------------------------- */

/* A dark band, in both schemes.
   The lockup is drawn on near-black in the brand sheet, and that is not a
   stylistic preference — the plate is 1.4:1 on paper and 11.7:1 on ink. A
   yellow mark on a light masthead would be a yellow mark nobody can see.
   Revert by deleting the two colour lines; nothing else depends on it. */
.site-head {
  border-bottom: 1px solid var(--slate);
  position: sticky; top: 0; z-index: 20;
  background: color-mix(in srgb, var(--masthead) 94%, transparent);
  color: var(--masthead-ink);
  backdrop-filter: blur(8px);
}
/* :not(.btn) matters. A button carries its own colour pair — ink on yellow
   for the CTA — and `.site-head a` outranks `.btn-cta`, which is a class on
   its own. Without this the Donate button rendered paper-on-yellow at 1.4:1.
   Same cascade mistake as the navigation, found by the same test. */
.site-head a:not(.btn) { color: inherit; }

.site-head-inner {
  display: flex; align-items: center; gap: var(--gap);
  min-height: 4.25rem;
}

.wordmark {
  display: flex; align-items: center; gap: 0.6rem;
  text-decoration: none; color: inherit; margin-right: auto;
}

/* The mark: a solid block, like a tally light. Deliberately not a logo — it
   is a placeholder shape that reads as intentional until a real one exists. */
/* The plate. Rotated, because a square set straight reads as a bullet point
   and a square set askew reads as a mark somebody chose. */
.wordmark-mark {
  width: 1.15rem; height: 1.15rem;
  background: var(--signal);
  flex: none;
  rotate: -8deg;
}

.wordmark-text { display: flex; flex-direction: column; line-height: 1.05; }
.wordmark-text b {
  font-family: var(--font-display);
  font-size: 1.02rem; font-weight: 780; letter-spacing: -0.02em;
}
.wordmark-text span {
  font-size: 0.7rem; text-transform: uppercase;
  letter-spacing: 0.14em;
  /* --ink-soft is tuned for paper and lands at 2.7:1 here. */
  color: var(--masthead-soft);
}

.site-nav { display: flex; align-items: center; gap: 1.4rem; }
.site-nav ul {
  display: flex; gap: 1.35rem;
  list-style: none; margin: 0; padding: 0;
}
.site-nav a:not(.btn) {
  /* `inherit`, NOT var(--ink). This said --ink and shipped invisible: the
     masthead went dark, --ink is the dark colour, and this rule has the same
     specificity as `.site-head a { color: inherit }` while coming later, so
     it quietly won. Nothing caught it — the smoke tests look for links with
     no accessible label, not for links the same colour as what is behind
     them. tests/contrast.spec.js was written for this. */
  color: inherit; text-decoration: none;
  font-size: var(--step--1); font-weight: 550;
  padding: 0.35rem 0;
  border-bottom: 2px solid transparent;
}
.site-nav a:not(.btn):hover { border-bottom-color: var(--rule); }
.site-nav a:not(.btn)[aria-current="page"] { border-bottom-color: var(--signal); }

/* A group of links under one grayed verb: "Reserve  Equipment  Studios".
   The label is not a link and does not pretend to be one — it names what the
   two beside it are for, which is the whole reason the menu could lose two
   items without losing any meaning. */
.nav-group {
  display: flex; align-items: baseline; gap: 0.7rem;
}
.nav-group-label {
  font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.12em;
  font-weight: 650;
  color: var(--masthead-soft);
  /* Smaller, quieter, and set in caps beside the two links rather than among
     them — the same treatment a column heading gets, so the eye reads it as a
     label and not as a third destination. */
  align-self: center;
}
.nav-group ul { gap: 1.1rem; }

.nav-toggle { display: none; }

@media (max-width: 60rem) {
  .nav-toggle {
    display: grid; place-items: center;
    width: 2.75rem; height: 2.75rem;
    background: none; border: 1px solid var(--masthead-soft); cursor: pointer;
  }
  .nav-toggle-bars,
  .nav-toggle-bars::before,
  .nav-toggle-bars::after {
    /* --ink is the dark colour and the band is the dark surface, so this was
       three invisible bars on a black square. */
    display: block; width: 1.15rem; height: 2px; background: var(--masthead-ink);
  }
  .nav-toggle-bars { position: relative; }
  .nav-toggle-bars::before,
  .nav-toggle-bars::after { content: ""; position: absolute; left: 0; }
  .nav-toggle-bars::before { top: -6px; }
  .nav-toggle-bars::after  { top:  6px; }

  /* The drawer is the masthead continuing downwards, not a light panel
     hanging off it. It was --paper, and the links inherit the band's light
     text, so the whole menu rendered as f4f1ea on f4f1ea — 1.00:1, every
     item present and none of them visible. tests/contrast.spec.js found it;
     it is the third place one `color: inherit` broke something, and by far
     the worst, because it was the entire navigation on a phone. */
  .site-nav {
    display: none;
    position: absolute; left: 0; right: 0; top: 100%;
    flex-direction: column; align-items: stretch; gap: 0;
    background: var(--masthead);
    border-bottom: 1px solid var(--slate);
    padding: 0.5rem 1.25rem 1.25rem;
  }
  .site-nav[data-open="true"] { display: flex; }
  .site-nav ul { flex-direction: column; gap: 0; }
  .site-nav ul a {
    display: block; padding: 0.8rem 0;
    border-bottom: 1px solid var(--slate);
    font-size: var(--step-0);
  }

  /* Stacked rather than side by side once there is a column to work with,
     so "Reserve" reads as the heading it is. */
  .nav-group { display: block; }
  .nav-group-label { display: block; padding: 0.9rem 0 0.35rem; }
  .site-nav .btn-cta { margin-top: 1rem; text-align: center; }
}

/* --- Buttons -------------------------------------------------------------- */

.btn {
  display: inline-block;
  padding: 0.65rem 1.15rem;
  font-size: var(--step--1);
  font-weight: 650;
  text-decoration: none;
  border: 2px solid var(--ink);
  color: var(--ink);
  background: transparent;
}
.btn:hover { background: var(--ink); color: var(--paper); }

.btn-primary,
.btn-cta {
  background: var(--signal);
  border-color: var(--signal);
  color: var(--signal-ink);
}
.btn-primary:hover,
.btn-cta:hover {
  background: transparent;
  color: var(--record-ink);
  border-color: var(--record-ink);
}

/* --- Hero ----------------------------------------------------------------- */

.hero { padding: clamp(3rem, 8vw, 6.5rem) 0 clamp(2.5rem, 5vw, 4rem); }

/* With a photo, the type sits on an opaque panel rather than on the image.
   The studio shot can be as busy and colorful as it likes; the words stay
   readable either way. This is the fix for the old full-bleed hero. */
.hero-photo {
  background-image: var(--hero-image);
  background-size: cover;
  background-position: center;
  padding-block: clamp(4rem, 9vw, 8rem);
}
.hero-photo .hero-panel {
  background: var(--paper);
  padding: clamp(1.75rem, 4vw, 3rem);
  max-width: 46rem;
}
.hero-photo .hero-panel .lede { max-width: 40ch; }
.hero h1 {
  font-size: var(--step-4);
  max-width: 16ch;
  margin-bottom: 0.4em;
}
.hero .lede { font-size: var(--step-1); max-width: 46ch; }
.hero-actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 2rem; }

.page-head {
  padding: clamp(2.5rem, 5vw, 4rem) 0 clamp(1.5rem, 3vw, 2.5rem);
  border-bottom: 1px solid var(--rule);
  margin-bottom: clamp(2rem, 4vw, 3rem);
}

/* --- Cards and grids ------------------------------------------------------ */

.grid {
  display: grid;
  gap: var(--gap);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr));
  list-style: none; padding: 0; margin: 0;
}

/* Narrower minimum, so four short cards sit on one row instead of 3 + 1. */
.grid-4 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr)); }

.card {
  border: 1px solid var(--rule);
  padding: 1.4rem;
  background: var(--paper);
  display: flex; flex-direction: column; gap: 0.5rem;
}
.card h3 { margin: 0; font-size: var(--step-1); }
.card p { margin: 0; color: var(--ink-soft); font-size: var(--step--1); }
.card a { text-decoration: none; }
.card a:hover { text-decoration: underline; }
.card-foot { margin-top: auto; padding-top: 0.9rem; }

.card .price,
.price {
  font-family: var(--font-display);
  font-size: var(--step-2);
  font-weight: 780;
  letter-spacing: -0.03em;
  color: var(--ink);
}

/* Definition-style rows — used for facilities, carriage, equipment. */
.rows { list-style: none; margin: 0 0 1.5em; padding: 0; }
.rows li {
  display: flex; flex-wrap: wrap; gap: 0.25rem 1rem;
  justify-content: space-between; align-items: baseline;
  padding: 0.85rem 0;
  border-bottom: 1px solid var(--rule);
}
.rows li b { font-weight: 650; }
.rows li span { color: var(--ink-soft); font-size: var(--step--1); }

/* --- Tags ----------------------------------------------------------------- */

.tag {
  display: inline-block;
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em;
  font-weight: 700; padding: 0.2rem 0.5rem;
  border: 1px solid var(--rule); color: var(--ink-soft);
}
.tag-warn { border-color: var(--record-ink); color: var(--record-ink); }

/* --- Transactions --------------------------------------------------------- */

.transaction { margin: 1.75rem 0; }
.transaction-provider {
  display: block; margin-top: 0.5rem;
  font-size: var(--step--1); color: var(--ink-soft);
}

/* Unconfigured transaction. Loud on purpose — it should never survive to
   production without someone noticing. */
.transaction-todo {
  border-left: 4px solid var(--signal);
  background: var(--paper-alt);
  padding: 1rem 1.15rem;
  font-size: var(--step--1);
}
.transaction-todo b { display: block; margin-bottom: 0.25rem; font-size: var(--step-0); }
.transaction-todo .muted { display: block; margin-top: 0.4rem; }

/* --- Embeds --------------------------------------------------------------- */

.embed { margin: 2rem 0; }
.embed iframe { width: 100%; border: 0; aspect-ratio: 16 / 9; }
.embed-audio iframe { aspect-ratio: auto; height: 180px; }

/* The live player gets a frame so it reads as a piece of equipment rather
   than a hole in the page. */
.embed-live {
  border: 1px solid var(--rule);
  background: #000;
  margin-block: 1.5rem;
  /* Capped so the player reads as one element on the page rather than
     swallowing the whole viewport at desktop widths. */
  max-width: 56rem;
}
.embed-live iframe { display: block; }

/* --- Sections ------------------------------------------------------------- */

.section { margin-top: clamp(3rem, 6vw, 5rem); }

.section-head {
  display: flex; flex-wrap: wrap; gap: 1rem 2rem;
  align-items: end; justify-content: space-between;
  margin-bottom: 1.75rem;
}
.section-head h2 { margin: 0; }
.section-head .eyebrow { margin-bottom: 0.35rem; }

/* --- On-air bar ----------------------------------------------------------- */

/* Sits directly under the hero. Reads as a ticker without moving, because a
   thing that scrolls by itself is harder to read, not easier. */
.onair-bar {
  background: var(--ink);
  color: var(--paper);
  font-size: var(--step--1);
}
.onair-bar[hidden] { display: none; }
.onair-bar > * { display: inline; }

.onair-bar {
  padding: 0.85rem clamp(1.25rem, 4vw, 3rem);
  display: flex; flex-wrap: wrap; gap: 0.4rem 1.25rem; align-items: baseline;
}

.onair-label {
  text-transform: uppercase; letter-spacing: 0.1em;
  font-weight: 700; font-size: 0.72rem;
  color: var(--record-ink);
  margin-right: 0.35rem;
}
.onair-next { color: var(--ink-soft); }

.onair-inline {
  border: 1px solid var(--rule);
  background: var(--paper-alt);
  color: var(--ink);
  margin-bottom: 2rem;
}

/* --- Featured ------------------------------------------------------------- */

.featured { margin-top: clamp(2.5rem, 5vw, 4rem); }

.grid-feature { grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr)); }

.card-feature { border-top: 4px solid var(--signal); }
.card-feature .tag { align-self: start; }
.feature-when { font-weight: 650; color: var(--ink) !important; }

/* --- Show cards ----------------------------------------------------------- */

.grid-show { grid-template-columns: repeat(auto-fill, minmax(min(100%, 15rem), 1fr)); }

.card-show { padding: 0; border: 1px solid var(--rule); gap: 0; }
.card-show h3 {
  font-size: var(--step-0); font-weight: 650; line-height: 1.25;
  padding: 0.9rem 1rem 0.35rem;
}
.card-show h3 a { color: var(--ink); }
.card-show .show-meta {
  padding: 0 1rem 0.25rem;
  font-size: 0.8rem;
}
.card-show .show-meta:last-child { padding-bottom: 1rem; }

.show-thumb { display: block; background: var(--paper-alt); }
.show-thumb img {
  width: 100%; aspect-ratio: 16 / 9; object-fit: cover;
}

/* --- Archive -------------------------------------------------------------- */

.rows-inline { display: flex; flex-wrap: wrap; gap: 0 2rem; }
.rows-inline li { border: 0; padding: 0.4rem 0; }

.rows-tight li { padding: 0.5rem 0; }

.rows-connect li { border-bottom-color: color-mix(in srgb, currentColor 25%, transparent); }

.rows-archive li[hidden] { display: none; }

/* Airing counts read as data, not as prose — they are the thing someone is
   scanning for when they sort by them. "Not aired" is the signal, so it gets
   the emphasis rather than the counts do. */
.airings { font-variant-numeric: tabular-nums; font-weight: 600; }
.airings-none { color: var(--record-ink); }

/* A date that has not happened yet. Same weight as an airing count — it is
   the thing being scanned for, not decoration. */
.coming { color: var(--record-ink); font-variant-numeric: tabular-nums; }

/* Several short fields on one line where they belong together — a date, a
   time and a runtime read as one answer, not three questions. */
.field-row { display: flex; flex-wrap: wrap; gap: 0 1rem; }
.field-row .field { flex: 1 1 8rem; }

.field textarea {
  width: 100%; font: inherit; padding: 0.6rem 0.7rem;
  background: var(--paper); color: var(--ink);
  border: 1px solid var(--rule); resize: vertical;
}

/* Editing a settings file: monospace, no wrapping, scrolls inside itself.
   Indentation carries meaning in YAML, so a proportional font that hides
   alignment is actively misleading, and soft-wrapping a long line makes it
   look indented when it is not. */
#settings-text {
  font-family: var(--font-mono);
  font-size: var(--step--1);
  line-height: 1.5;
  tab-size: 2;
  white-space: pre;
  overflow-x: auto;
}

/* A button that reads as a link, for a secondary action sitting inside a
   sentence. */
.btn-link {
  background: none; border: 0; padding: 0;
  font: inherit; color: var(--accent); cursor: pointer;
  text-decoration: underline; text-underline-offset: 0.18em;
}
.btn-link:hover { color: var(--record-ink); }

/* Cablecast titles are frequently one long underscore-joined token —
   Midnight_Limited_1940_2026-07-14_11_59_43 — with no place to break. On a
   phone that pushes the whole page sideways. */
.rows-archive li b,
.rows-archive li span,
.card-show h3 { overflow-wrap: anywhere; }

.filter {
  position: sticky; top: 4.25rem; z-index: 5;
  background: var(--paper);
  border-bottom: 1px solid var(--rule);
  padding: 1rem 0;
  margin-bottom: 1rem;
  /* Two controls now, wrapping to stacked on a narrow screen rather than
     squeezing a select down to nothing. */
  display: grid; gap: 0.4rem 1rem;
  grid-template-columns: minmax(12rem, 1fr) auto;
  align-items: end;
}
.filter label { grid-row: 1; }
.filter input, .filter select { grid-row: 2; }
.filter-count { grid-column: 1 / -1; grid-row: 3; }
.filter select {
  font: inherit; font-size: var(--step--1);
  padding: 0.6rem 0.7rem;
  background: var(--paper); color: var(--ink);
  border: 1px solid var(--rule);
}
@media (max-width: 32rem) {
  .filter { grid-template-columns: 1fr; }
  .filter label, .filter input, .filter select, .filter-count { grid-row: auto; }
}
.filter[hidden] { display: none; }
.filter label {
  display: block; font-size: 0.72rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--ink-soft); margin-bottom: 0.4rem;
}
.filter input {
  width: min(100%, 26rem);
  font: inherit; font-size: var(--step-0);
  padding: 0.6rem 0.8rem;
  border: 1px solid var(--rule);
  background: var(--paper); color: var(--ink);
}
.filter-count { margin: 0.5rem 0 0; font-size: var(--step--1); color: var(--ink-soft); }

/* --- Footer --------------------------------------------------------------- */

.site-foot {
  margin-top: 5rem;
  border-top: 3px solid var(--ink);
  padding-top: 3rem;
  font-size: var(--step--1);
}

.site-foot-inner {
  display: grid; gap: var(--gap);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 15rem), 1fr));
}

.foot-title {
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.12em;
  color: var(--ink-soft); margin-bottom: 0.9rem; font-weight: 700;
}
.site-foot address { font-style: normal; margin-bottom: 1em; }
.foot-links { list-style: none; margin: 0; padding: 0; }
.foot-links li { margin-bottom: 0.45rem; }

.foot-legal {
  display: flex; flex-wrap: wrap; gap: 0.5rem 2rem;
  justify-content: space-between;
  margin-top: 3rem; padding: 1.25rem 0 2.5rem;
  border-top: 1px solid var(--rule);
  color: var(--ink-soft);
}
.foot-legal p { margin: 0; }

/* --- Check-in ------------------------------------------------------------- */

.rows-checkin li b { font-variant-numeric: tabular-nums; }

/* Member programs pulled from feeds. A thumbnail beside the title, because
   these are videos and podcasts and a wall of text does not read as either.
   The image is fixed-ratio so a feed serving an unexpected size cannot shove
   the layout around. */
.feed { list-style: none; margin: 0 0 1.5em; padding: 0; }
.feed li {
  display: flex; gap: 1rem; align-items: flex-start;
  padding: 1rem 0;
  border-bottom: 1px solid var(--rule);
}
.feed-thumb {
  flex: none;
  width: clamp(6.5rem, 22vw, 10rem);
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--paper-alt);
}
.feed-thumb img { width: 100%; height: 100%; object-fit: cover; }
.feed-body { min-width: 0; }
.feed-body b { font-weight: 650; display: block; margin-bottom: 0.2rem; }
.feed-meta { margin: 0; font-size: var(--step--1); }
.feed-summary {
  margin: 0.35rem 0 0;
  font-size: var(--step--1);
  /* Two lines. A feed description can be a wall of text and this is a
     sampler — someone who wants the rest follows the link. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Merged community calendar. Dates line up as a column, so a list of six
   scans as a schedule rather than as six sentences that happen to start with
   a date. The time sits under the date and does not compete with it. */
.rows-events li b,
.rows-feed li b {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  display: flex; flex-direction: column; gap: 0.1rem;
}
.rows-events li b .muted,
.rows-feed li b .muted { font-weight: 400; }

/* Class context on the check-in page. Same information the homepage shows,
   from the same data, worked out by the same function. */
.class-banner {
  border-left: 5px solid var(--signal);
  background: var(--paper-alt);
  padding: 1.25rem 1.5rem;
  margin-bottom: 2.5rem;
}
.class-banner[hidden] { display: none; }
.class-banner h2 { margin: 0 0 0.3em; }
.class-banner .lede { font-size: var(--step-0); margin-bottom: 0.5em; }
.class-banner [data-class-room]::before { content: " · "; }
.class-banner p:last-child { margin-bottom: 0; }

.rsvp-noted { font-weight: 600; }
.rsvp-noted[hidden],
[data-rsvp-offer][hidden] { display: none; }

/* Only one state panel is ever visible; the rest carry `hidden`. */
.state[hidden] { display: none; }
.state { margin-bottom: 2.5rem; }
.state h2 { margin-top: 0; }

/* The primary action is meant to be hit with a thumb, at a glance, possibly
   while walking. */
.btn-big {
  font-size: var(--step-1);
  padding: 0.9rem 1.8rem;
}

.field { margin-bottom: 1.25rem; max-width: var(--measure); }
.field label {
  display: block;
  font-size: 0.75rem; font-weight: 700;
  text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--ink-soft); margin-bottom: 0.4rem;
}
.field input,
.field select {
  font: inherit; font-size: var(--step-0);
  width: 100%;
  padding: 0.6rem 0.75rem;
  border: 1px solid var(--rule);
  background: var(--paper); color: var(--ink);
}

/* Class mode. When a class is running the band gets the signal colour on its
   leading edge, so the page reads differently at a glance without the layout
   jumping around. */
[data-class-mode="now"],
[data-class-mode="late"],
[data-class-mode="soon"] { border-left: 5px solid var(--signal); }

.class-now[hidden] { display: none; }
.class-now h2 { margin-bottom: 0.3em; }
.class-now [data-class-room]::before { content: " · "; }
.class-now hr { margin: 2.5rem 0 2rem; }

.class-late {
  border-left: 3px solid var(--signal);
  padding-left: 0.9rem;
  font-weight: 600;
}
.class-late[hidden] { display: none; }

/* Deliberately not a paywall: one line, plain numbers, membership beside it
   rather than behind it. */
.class-price {
  font-size: var(--step--1);
  background: var(--paper);
  border: 1px solid var(--rule);
  padding: 0.8rem 1rem;
  max-width: var(--measure);
}
.class-price[hidden] { display: none; }

.checkin-card {
  display: flex; flex-wrap: wrap; gap: var(--gap);
  align-items: center; justify-content: space-between;
}
.checkin-card-text { flex: 1 1 22rem; }
.checkin-card-text h2 { margin-bottom: 0.3em; }

.checkin-card-qr {
  flex: 0 0 auto;
  display: flex; flex-direction: column; align-items: center; gap: 0.5rem;
  text-decoration: none;
}
.checkin-card-qr img {
  width: clamp(8rem, 22vw, 11rem); height: auto;
  background: #fff; padding: 0.6rem;
  border: 1px solid var(--rule);
}
.checkin-card-qr span {
  font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.12em;
  font-weight: 700; color: var(--ink-soft);
}

.rows input[type="text"] {
  font: inherit;
  padding: 0.35rem 0.6rem;
  border: 1px solid var(--rule);
  background: var(--paper);
  color: var(--ink);
  max-width: 14rem;
}

/* A label styled as a button, so "Restore from a file" matches the buttons
   either side of it while still driving the hidden file input. */
label.btn { cursor: pointer; }

/* --- Poster --------------------------------------------------------------- */

.poster {
  border: 3px solid var(--ink);
  padding: clamp(1.5rem, 5vw, 3rem);
  text-align: center;
  max-width: 34rem;
  margin: 2rem 0;
}
.poster-eyebrow {
  font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.14em;
  font-weight: 700; color: var(--ink-soft); margin-bottom: 0.5rem;
}
.poster-title { font-size: var(--step-3); margin-bottom: 1.5rem; }
.poster-qr {
  width: min(100%, 20rem); height: auto; margin: 0 auto 1.25rem;
  /* The SVG paths are black; invert in dark mode so the code stays scannable
     on screen as well as on paper. */
  background: #fff; padding: 0.75rem;
}
.poster-url { font-weight: 650; font-size: var(--step-1); margin-bottom: 0.75rem; overflow-wrap: anywhere; }
.poster-note { color: var(--ink-soft); font-size: var(--step--1); margin: 0; }

@media print {
  .site-head, .site-foot, .no-print, .page-head .lede { display: none !important; }
  .poster { border-width: 2px; page-break-inside: avoid; }
  body { background: #fff; color: #000; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

/* --- Hosted forms ---------------------------------------------------------- */

/* A framed third-party form. Given its own frame and ground so it reads as a
   panel we put there deliberately, rather than a hole in the page. Microsoft
   Forms does not report its height to the parent, so this is a tall fixed box
   that scrolls internally — there is no message-passing to hook into. */
.hosted-form {
  border: 1px solid var(--rule);
  background: #fff;
  margin: 1.75rem 0 1rem;
}
.hosted-form iframe {
  display: block;
  width: 100%;
  height: min(80vh, 46rem);
  border: 0;
}

.hosted-form-escape {
  font-size: var(--step--1);
  color: var(--ink-soft);
  max-width: var(--measure);
  margin-bottom: 2rem;
}


/* --- The countdown -------------------------------------------------------
   Shown while media is being fetched, before it can start.

   NOT A SPINNER, quite. A spinner says "something is happening"; this says
   "it is nearly your turn", which is the honest thing to say in the seconds
   before a video begins. So the plate depletes around itself like a studio
   countdown and swings rather than revolving — the easing lands it at the top
   of each cycle so the motion reads as a beat rather than a wheel.

   No numbers, deliberately. We do not know how long it will take, and a
   number that is wrong is worse than no number.
   -------------------------------------------------------------------------- */

/* Registering the custom property is what makes the sweep animatable at all —
   to CSS it is otherwise an opaque string, and the whole thing would jump from
   full to empty. Where @property is unsupported the plate simply sits still,
   which is a fine thing for it to do. */
@property --sweep {
  syntax: "<angle>";
  inherits: false;
  initial-value: 360deg;
}

.countdown {
  --size: 2.75rem;
  --sweep: 360deg;

  width: var(--size);
  height: var(--size);
  flex: none;
  background: var(--signal);

  /* The depleting wedge. Masked rather than drawn, so the shape stays a
     square plate and only its coverage changes. */
  -webkit-mask-image: conic-gradient(#000 var(--sweep), transparent 0);
  mask-image: conic-gradient(#000 var(--sweep), transparent 0);

  animation:
    countdown-sweep 1.5s linear infinite,
    countdown-swing 1.5s cubic-bezier(0.16, 0.84, 0.28, 1) infinite;
}

@keyframes countdown-sweep {
  from { --sweep: 360deg; }
  to   { --sweep: 0deg; }
}

/* Counter-clockwise, and eased so it arrives rather than arrives at speed.
   The scale is what puts it halfway to a pulse: it settles slightly small at
   the end of a cycle and snaps back as the next one starts. */
@keyframes countdown-swing {
  0%   { rotate: 0turn;      scale: 1; }
  70%  { rotate: -0.92turn;  scale: 0.94; }
  100% { rotate: -1turn;     scale: 1; }
}

.countdown-wrap {
  display: flex; align-items: center; gap: 0.9rem;
  color: var(--ink-soft); font-size: var(--step--1);
}

/* Motion that rotates is a vestibular trigger, and this rotates a full turn
   every second and a half. Reduced motion gets the same plate holding still
   and breathing — a change of state without a change of place. */
@media (prefers-reduced-motion: reduce) {
  .countdown {
    animation: countdown-breathe 2.4s ease-in-out infinite;
    -webkit-mask-image: none;
    mask-image: none;
    rotate: -8deg;
  }
  @keyframes countdown-breathe {
    0%, 100% { opacity: 1; }
    50%      { opacity: 0.45; }
  }
}

/* --- The Booqable store on /equipment/ ------------------------------------ */
/*
   Booqable renders its store into our page — light DOM, no shadow root, no
   custom elements, and a global stylesheet of its own. That is the whole
   reason this section can exist: on Wix it is an iframe, and an iframe is a
   sealed box you can only make taller.

   Their layout is a grid of cards. This turns it into a list of rows: one
   item per line, thumbnail, name, price, and the button to add it. Gear is
   picked from by scanning names, and names read faster in a column than in a
   grid — a card grid makes you read in a boustrophedon, and it spends most of
   a phone screen on photographs of black rectangles that all look alike.

   Everything below overrides a rule read out of their stylesheet rather than
   guessed at, so each one names what it is fighting. Their selectors all
   begin `.booqable-component`, so ours have to be at least as specific to
   win — that is why these are not one-word rules.

   The risk of overriding somebody else's markup is that they change it and
   this quietly stops applying. Kept as small as possible for that reason, and
   the failure mode is their card grid coming back rather than a broken page.
   Their script does not load on a CI runner, so nothing here is covered by a
   test — it is checked by looking. */

.booqable-store {
  /* The cart launcher floats bottom-right, over whatever is there. Without
     this it lands on the paragraph below the store. */
  padding-bottom: 4.5rem;
}

/*
   THE GRID BECOMES A COLUMN.

   Theirs: `display:flex; flex-wrap:wrap; gap:16px; justify-content:center`
   with `.booqable-product { min-width:280px; max-width:320px }`. The minimum
   width is a floor rather than a preference, and two of them plus the gap
   need 576px — which is why a 390px phone showed exactly one per row and why
   the seventh item was a scroll away.

   One column, and each item a row. No breakpoints: a list is a list at every
   width, and the rows get longer rather than more numerous. */
.booqable-component .booqable-product-list-grid {
  display: block;
  padding: 0;
}

.booqable-component .booqable-product-list-grid .booqable-product,
.booqable-component .booqable-product-wrapper {
  min-width: 0;
  max-width: none;
  width: auto;
  margin: 0;
}

/* Hairlines between rows rather than a shadow around each one. A card carries
   its own edges because it floats; a row in a list does not need them, and
   twelve drop shadows down a page is noise. */
.booqable-component .booqable-product-wrapper .booqable-product-inner {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.9rem;
  border-radius: 0;
  box-shadow: none !important;
  background: transparent;
  border-bottom: 1px solid var(--rule);
  padding: 0.7rem 0.2rem;
}

.booqable-component .booqable-product-list-grid .booqable-product:first-child .booqable-product-inner {
  border-top: 1px solid var(--rule);
}

/*
   THE THUMBNAIL.

   Theirs: the image is `width:100%` and the no-photo placeholder is
   `height:250px` AND `padding-bottom:100%` — both, so an item with no
   photograph got 250px plus a square of nothing. That was most of a phone
   screen for the 64gb sd card. In a row it is a fixed square instead, and
   `flex: none` stops it being squeezed by a long name. */
.booqable-component .booqable-product-wrapper .booqable-product-inner .bq-image-wrapper {
  flex: none;
  width: 3.4rem;
  height: 3.4rem;
  border-bottom: 0;
  border: 1px solid var(--rule);
  border-radius: 3px;
  overflow: hidden;
  display: grid;
  place-items: center;
}

.booqable-component .booqable-product-wrapper .booqable-product-inner img.bq-product-image {
  width: 100%;
  height: 100%;
  object-fit: contain;
}

.booqable-component .booqable-product-wrapper .booqable-product-inner span.bq-no-photo {
  width: 100%;
  height: 100%;
  padding-bottom: 0;
  border-bottom: 0;
}

.booqable-component .booqable-product-wrapper .booqable-product-inner span.bq-no-photo i.bq-no-photo {
  font-size: 18px;
  margin: 0;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

/*
   THE MIDDLE OF THE ROW.

   Theirs: `.bq-details { height:70px; padding:15px }` with the button floated
   right inside it, sized for the bottom of a card. Here it is the flexible
   middle column, and the button comes out of the float and sits at the end. */
.booqable-component .booqable-product-wrapper .booqable-product-inner .bq-details {
  flex: 1 1 auto;
  min-width: 0;
  height: auto;
  padding: 0;
  display: flex;
  align-items: center;
  gap: 0.9rem;
}

/*
   THE NAME.

   Theirs: nowrap with an ellipsis in a wrapper fixed at 17px — fine in a
   320px card, and it is the END of our names that tells them apart, so
   "Rokinon 50mm Lens for Sony E" and "…for Canon" clip to the same string.
   A row is the full width of the page, so most names now fit on one line;
   two is the ceiling for the ones that do not. */
.booqable-component .booqable-product-wrapper .booqable-product-inner .bq-product-title-wrapper,
.booqable-component .booqable-product-wrapper .booqable-product-inner .bq-product-title-wrapper.bq-has-prices {
  flex: 1 1 auto;
  min-width: 0;
  height: auto;
  padding-right: 0;
}

.booqable-component .booqable-product-wrapper .booqable-product-inner .bq-product-name {
  white-space: normal;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
  line-height: 1.3;
  font-size: var(--step-0);
  color: var(--ink);
}

/* The price column. Right-aligned and not allowed to wrap, so the amounts
   line up down the page and can be compared without reading each row. */
.booqable-component .booqable-product-wrapper .booqable-product-inner .bq-details .bq-price-details {
  flex: none;
  margin-top: 0;
  text-align: right;
  white-space: nowrap;
  font-size: var(--step--1);
  color: var(--ink-soft);
}

.booqable-component .booqable-product-wrapper .booqable-product-inner .bq-details .bq-price-details span.bq-price {
  color: var(--ink);
}

/* The add button, out of the float and onto the end of the row. */
.booqable-component .booqable-product-wrapper .booqable-product-inner .bq-details .bq-button {
  float: none;
  flex: none;
  padding: 0.5rem 0.7rem;
  font-size: 1rem;
}

/* Their status pill is positioned with a negative margin to hang off the top
   of a card image. There is no card any more, so it would hang off nothing. */
.booqable-component .booqable-product-wrapper .booqable-product-inner .bq-details .bq-status {
  float: none;
  margin: 0;
}

/*
   NARROW ROWS.

   Below about a 30rem screen there is not room for name and price side by
   side without the name losing most of its width, so the middle column
   stacks: name on top, price under it, button still at the end. */
@media (max-width: 30rem) {
  .booqable-component .booqable-product-wrapper .booqable-product-inner .bq-details {
    flex-wrap: wrap;
    gap: 0.15rem 0.9rem;
  }

  .booqable-component .booqable-product-wrapper .booqable-product-inner .bq-product-title-wrapper,
  .booqable-component .booqable-product-wrapper .booqable-product-inner .bq-product-title-wrapper.bq-has-prices {
    flex: 1 1 100%;
    order: 1;
  }

  .booqable-component .booqable-product-wrapper .booqable-product-inner .bq-details .bq-price-details {
    order: 2;
    text-align: left;
  }

  .booqable-component .booqable-product-wrapper .booqable-product-inner .bq-details .bq-button {
    order: 3;
    margin-left: auto;
  }
}

/*
   DARK-ON-DARK.

   Theirs: `.booqable-product-list-notice { color:#213b47 }` with no
   background of its own. On paper that is near-black on white. On our dark
   scheme it is near-black on near-black — the "Select your rental period for
   prices and availability" box, present and unreadable, which is the same
   class of bug the contrast test was written for. */
.booqable-product-list-wrapper .booqable-product-list-notice {
  color: var(--ink);
  background: var(--paper-alt);
  border-color: var(--rule);
}
