/* ===========================================================================
   mijote — design system v1
   The single stylesheet for every page. Sections follow the design document:
   1 foundations · 2 components · 3 states · 4 page layouts · 5 paid messages
   · 6 identity.

   Three rules govern everything here:
     1. The interface is greyscale, and accent is scoped by audience. On any
        surface a listener can reach the accent means "live" and nothing else;
        staff surfaces get --tool instead. See 1.1 and 4.6.
     2. Hierarchy comes from weight and size, not colour.
     3. Anything a machine produced is visibly offered until a person applies it
        — which is what the dashed border means, everywhere, without exception.

   See DESIGN_SYSTEM.md before adding to this file.
   =========================================================================== */

@import url("https://fonts.googleapis.com/css2?family=Schibsted+Grotesk:ital,wght@0,400..900;1,400..900&display=swap");

/* ---------------------------------------------------------------------------
   1.1 Colour — dark (default)
   --------------------------------------------------------------------------- */

:root {
  --bg: #08080a;
  --surface: #101012;
  --surface-2: #17171b;
  --surface-raised: #0d0d0f; /* a row lifted off the page, not off the card */

  --line: #232327;
  --line-soft: #1c1c20;
  --line-input: #26262b;
  --line-strong: #3f3f46;

  --text: #fafafa;
  --text-body: #d4d4d8;
  --text-2: #a1a1aa;
  --text-3: #71717a;
  --text-4: #5c5c64;
  --text-past: #52525b;

  /* The station is audible right now — a programme or the rotation. Nothing
     else in the interface may use this colour. */
  --live: #ff4f1f;
  --live-ink: #ff7a4f;
  --live-line: #4a2b1e;
  --live-wash: transparent; /* dark uses the animated radial, not a flat tint */
  /* A flat live tint, for blocks that state "on air" without the wash — the
     wash belongs to programmes on listener surfaces, this covers rotation too. */
  --live-tint: rgba(255, 79, 31, 0.09);
  --live-tint-line: rgba(255, 79, 31, 0.3);

  /* A different red from --live on purpose, and only ever text or border —
     a destructive control must never read as a live indicator at a glance. */
  --danger: #f04438;
  --danger-line: #4a2320;

  /* The staff accent. Legal ONLY on surfaces no listener can reach — the pages
     carrying data-density="staff". The accent rule is scoped by audience, not
     by app: --live keeps its single meaning (on air now) everywhere, including
     here, and the two never trade jobs. Never a decorative fill, a chart
     colour, or a brand mark. See 4.6. */
  --tool: #5b8def;
  --tool-text: #8fb4f5; /* --tool as text on dark */
  --tool-strong: #5b8def; /* light mode darkens this; dark needs no shift */
  --tool-on-strong: #08080a; /* ink on a solid --tool-strong fill */
  --tool-tint: rgba(91, 141, 239, 0.12);
  --tool-line: rgba(91, 141, 239, 0.4);
  --tool-ring: rgba(91, 141, 239, 0.18);

  /* Primary buttons are white-on-black so they can't compete with the accent. */
  --invert-bg: #fafafa;
  --invert-text: #08080a;
  --invert-bg-soft: #e4e4e7;

  /* 1.4 Space — 4pt */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 24px;
  --s6: 32px;
  --s7: 48px;

  /* 1.4 Radius */
  --r-control: 6px;
  --r-row: 10px;
  --r-card: 14px;
  --r-pill: 999px;

  /* 1.5 Motion — 120ms on hover/press, 200ms on panel open, never past 240ms */
  --t-fast: 120ms ease-out;
  --t-panel: 200ms ease-out;
  --simmer-duration: 7s;
  --working-duration: 1.4s;

  /* 1.4 Density — reset per page by [data-density] below */
  --pad-card: var(--s5);
  --row-h: 44px;

  color-scheme: dark;
}

/* ---------------------------------------------------------------------------
   1.2 Colour — light. A full peer, not an afterthought.
   Opt in explicitly with <html data-theme="light">, or hand the choice to the
   operating system with <html data-theme="auto">. Dark stays the default.
   --------------------------------------------------------------------------- */

:root[data-theme="light"],
:root[data-theme="auto"] {
  color-scheme: light;
}

@media (prefers-color-scheme: light) {
  :root[data-theme="auto"] {
    --bg: #f4f4f5;
    --surface: #fafafa;
    --surface-2: #efeff1;
    --surface-raised: #f0f0f2;

    --line: #dcdce0;
    --line-soft: #e6e6ea;
    --line-input: #d3d3d9;
    --line-strong: #b4b4bc;

    --text: #18181b;
    --text-body: #27272a;
    --text-2: #52525b;
    --text-3: #8b8b93;
    --text-4: #a1a1aa;
    /* Past can't dim downward here, so it dims upward. */
    --text-past: #a1a1aa;

    --live: #ff4f1f;
    --live-ink: #d8380b; /* accent text darkens to clear 4.5:1 */
    --live-line: #f5c4b3;
    --live-wash: #fff1ee; /* a flat tint in light — no gradient */
    --live-tint: #fff1ee;
    --live-tint-line: #f5c4b3;

    --danger: #c92a1d;
    --danger-line: #edb9b4;

    /* #5B8DEF fails 4.5:1 for text and small fills on light, so text and solid
       fills darken to #2F6BD8 with white ink. The focus ring keeps #5B8DEF —
       it's a 3px halo, not a text or fill contrast surface. */
    --tool: #5b8def;
    --tool-text: #2f6bd8;
    --tool-strong: #2f6bd8;
    --tool-on-strong: #fafafa;
    --tool-tint: rgba(47, 107, 216, 0.1);
    --tool-line: rgba(47, 107, 216, 0.35);
    --tool-ring: rgba(91, 141, 239, 0.28);

    --invert-bg: #18181b;
    --invert-text: #fafafa;
    --invert-bg-soft: #27272a;
  }
}

:root[data-theme="light"] {
  --bg: #f4f4f5;
  --surface: #fafafa;
  --surface-2: #efeff1;
  --surface-raised: #f0f0f2;

  --line: #dcdce0;
  --line-soft: #e6e6ea;
  --line-input: #d3d3d9;
  --line-strong: #b4b4bc;

  --text: #18181b;
  --text-body: #27272a;
  --text-2: #52525b;
  --text-3: #8b8b93;
  --text-4: #a1a1aa;
  --text-past: #a1a1aa;

  --live: #ff4f1f;
  --live-ink: #d8380b;
  --live-line: #f5c4b3;
  --live-wash: #fff1ee;
  --live-tint: #fff1ee;
  --live-tint-line: #f5c4b3;

  --danger: #c92a1d;
  --danger-line: #edb9b4;

  /* See the note in the auto block above. */
  --tool: #5b8def;
  --tool-text: #2f6bd8;
  --tool-strong: #2f6bd8;
  --tool-on-strong: #fafafa;
  --tool-tint: rgba(47, 107, 216, 0.1);
  --tool-line: rgba(47, 107, 216, 0.35);
  --tool-ring: rgba(91, 141, 239, 0.28);

  --invert-bg: #18181b;
  --invert-text: #fafafa;
  --invert-bg-soft: #27272a;
}

/* ---------------------------------------------------------------------------
   1.3 Type — Schibsted Grotesk variable, one family. Seven sizes, no more.
   Weights 400–800; never 900, it fills in at small sizes.
   --------------------------------------------------------------------------- */

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--text-body);
  font-family: "Schibsted Grotesk", "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: 14px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

/* display / 52 · 800 — countdowns and clocks only */
.t-display {
  font-size: 52px;
  font-weight: 800;
  letter-spacing: -0.055em;
  line-height: 1;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}

/* title / 26 · 800 — one per screen */
.t-title {
  font-size: 26px;
  font-weight: 800;
  letter-spacing: -0.03em;
  line-height: 1.1;
  color: var(--text);
  margin: 0;
}

/* heading / 17 · 700 — now playing, card headings, row titles */
.t-heading {
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.015em;
  color: var(--text);
  margin: 0;
}

/* body / 14 · 400 — descriptions, chat, notes. Max 68ch. */
.t-body {
  font-size: 14px;
  font-weight: 400;
  line-height: 1.6;
  color: var(--text-body);
  max-width: 68ch;
  text-wrap: pretty;
}

/* ui / 13 · 600 — buttons, fields, table cells */
.t-ui {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-body);
}

/* meta / 12 · 400 — attribution, timestamps, counts */
.t-meta {
  font-size: 12px;
  font-weight: 400;
  color: var(--text-3);
}

/* label / 11 · 700 caps — section labels and state words */
.t-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--text-3);
}

.t-label-quiet {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-4);
}

/* Every number in a schedule, count, duration or confidence. */
.num,
.t-display,
time,
[data-num] {
  font-variant-numeric: tabular-nums;
}

.muted {
  color: var(--text-3);
  font-size: 12px;
}

.past {
  color: var(--text-past);
}

a {
  color: var(--text-body);
  text-decoration: none;
  border-bottom: 1px solid var(--line-strong);
  transition: color var(--t-fast), border-color var(--t-fast);
}
a:hover {
  color: var(--text);
  border-bottom-color: var(--text-3);
}

code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.92em;
  color: var(--text-body);
}

/* ---------------------------------------------------------------------------
   1.4 Density. Listener is generous; staff is compact. Same tokens, one step
   apart. Set on <body data-density="staff">.
   --------------------------------------------------------------------------- */

[data-density="staff"] {
  --pad-card: var(--s4);
  --row-h: 32px;
}

/* ---------------------------------------------------------------------------
   1.5 Motion — the simmer. One 7s loop with eleven uneven keyframes: two
   strong swells, several partial ones, never symmetric. Not a blink, not a
   sine pulse. This is the one ownable detail; don't restyle it per page.
   --------------------------------------------------------------------------- */

@keyframes simmerDot {
  0%   { opacity: 0.38; box-shadow: 0 0 0 0 rgba(255, 79, 31, 0); }
  9%   { opacity: 0.56; }
  16%  { opacity: 0.44; }
  29%  { opacity: 0.97; box-shadow: 0 0 18px 3px rgba(255, 79, 31, 0.34); }
  36%  { opacity: 0.64; }
  44%  { opacity: 0.52; }
  54%  { opacity: 1;    box-shadow: 0 0 28px 6px rgba(255, 79, 31, 0.42); }
  62%  { opacity: 0.58; }
  71%  { opacity: 0.42; box-shadow: 0 0 0 0 rgba(255, 79, 31, 0); }
  82%  { opacity: 0.84; box-shadow: 0 0 14px 2px rgba(255, 79, 31, 0.24); }
  91%  { opacity: 0.46; }
  100% { opacity: 0.38; box-shadow: 0 0 0 0 rgba(255, 79, 31, 0); }
}

@keyframes simmerWash {
  0%   { opacity: 0.10; }
  11%  { opacity: 0.22; }
  19%  { opacity: 0.13; }
  30%  { opacity: 0.46; }
  39%  { opacity: 0.24; }
  52%  { opacity: 0.58; }
  61%  { opacity: 0.27; }
  73%  { opacity: 0.15; }
  84%  { opacity: 0.38; }
  93%  { opacity: 0.18; }
  100% { opacity: 0.10; }
}

@keyframes working {
  0%   { transform: translateX(-100%); }
  100% { transform: translateX(400%); }
}

@media (prefers-reduced-motion: reduce) {
  [data-simmer] {
    animation: none !important;
    opacity: 0.92 !important;
  }
  [data-working] {
    animation: none !important;
  }
  * {
    transition-duration: 1ms !important;
  }
}

/* The ambient radial behind a live block. One per screen, maximum. */
.live-wash {
  position: absolute;
  inset: -20% -10% auto -10%;
  height: 46%;
  pointer-events: none;
  background: radial-gradient(50% 55% at 40% 45%, var(--live), transparent 72%);
  animation: simmerWash var(--simmer-duration) ease-in-out infinite;
  /* Behind its container's content, but inside the container's stacking
     context — a wash never escapes the block it belongs to. */
  z-index: -1;
}

:root[data-theme="light"] .live-wash,
:root[data-theme="auto"] .live-wash {
  background: var(--live-wash);
  animation: none;
  opacity: 1;
}

/* ===========================================================================
   2. COMPONENTS
   =========================================================================== */

/* --- page chrome ---------------------------------------------------------- */

.app-header {
  display: flex;
  align-items: center;
  gap: var(--s5);
  padding: var(--s4) var(--s5);
  border-bottom: 1px solid var(--line-soft);
  flex-wrap: wrap;
}

.wordmark {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  border: none;
}
.wordmark:hover {
  border: none;
}
.wordmark .mark {
  font-size: 20px;
  font-weight: 800;
  letter-spacing: -0.045em;
  color: var(--text);
}
.wordmark .dot {
  margin-bottom: 4px;
}

.page-name {
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.015em;
  color: var(--text);
}

.page-tag {
  font-size: 12px;
  color: var(--text-3);
}

/* Role-aware navigation: one component, two sets. Mark the staff set with
   .nav[data-set="staff"] so it's obvious in the markup which audience it's for. */
.nav {
  display: flex;
  align-items: center;
  gap: var(--s4);
  margin-left: auto;
  flex-wrap: wrap;
}
.nav a {
  font-size: 13px;
  font-weight: 600;
  color: var(--text-2);
  border: none;
}
.nav a:hover {
  color: var(--text);
}
.nav a[aria-current="page"] {
  color: var(--text);
}
.nav .who {
  font-size: 12px;
  color: var(--text-3);
}

main {
  /* `width` alongside `max-width` is doing real work, not belt-and-braces. On
     the listener pages main is a flex item in a column flex container, where
     the auto side margins suppress the default stretch and leave it
     shrink-to-fit — so its width came from whatever its widest child happened
     to want. A block child made that 720px by accident; a grid child asks for
     its min-content and the column collapses. Being explicit makes the measure
     the same on every page regardless of what is inside it. */
  width: 100%;
  max-width: 720px;
  margin: 0 auto;
  padding: var(--s5) var(--s4) var(--s7);
}
main.wide {
  max-width: 1100px;
}
main.mid {
  max-width: 940px;
}

/* --- card ----------------------------------------------------------------- */

.card {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: var(--pad-card);
  margin-bottom: var(--s5);
}
.card > .card-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  margin-bottom: var(--s3);
}
.card > .card-head + .t-body {
  margin-top: 0;
}

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--s3);
}
.row {
  display: flex;
  gap: var(--s2);
  align-items: center;
  flex-wrap: wrap;
}
.row-between {
  display: flex;
  gap: var(--s3);
  align-items: baseline;
  justify-content: space-between;
}
.rule {
  border: 0;
  border-top: 1px solid var(--line-soft);
  margin: var(--s4) 0;
}
/* Inside a card that is only switches, the separator is holding two single
   lines apart and doesn't need a section's worth of air. */
.is-slim .rule {
  margin: var(--s3) 0;
}

/* --- 2.1 Buttons ---------------------------------------------------------- */
/* Primary is white-on-black, never accent — the accent is reserved for
   liveness, so a button can't compete with it. One primary per view. */

.btn {
  font-family: inherit;
  font-size: 13px;
  font-weight: 600;
  line-height: 1;
  color: var(--text-body);
  background: transparent;
  border: 1px solid var(--line-strong);
  border-radius: var(--r-control);
  padding: 11px 14px;
  cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast), color var(--t-fast);
}
.btn:hover:not(:disabled) {
  border-color: var(--text-3);
  color: var(--text);
}
.btn:focus-visible {
  outline: 2px solid var(--text-2);
  outline-offset: 2px;
}

.btn-primary {
  font-weight: 700;
  color: var(--invert-text);
  background: var(--invert-bg);
  border-color: var(--invert-bg);
}
.btn-primary:hover:not(:disabled) {
  background: var(--invert-bg-soft);
  border-color: var(--invert-bg-soft);
  color: var(--invert-text);
}

/* Destructive is text and border only — never a filled surface. */
.btn-danger {
  color: var(--danger);
  border-color: var(--danger-line);
}
.btn-danger:hover:not(:disabled) {
  color: var(--danger);
  border-color: var(--danger);
}

.btn-ghost {
  border-color: transparent;
  color: var(--text-2);
}

.btn-sm {
  font-size: 12px;
  padding: 7px 11px;
}

.btn:disabled {
  color: var(--text-4);
  border-color: var(--line);
  background: transparent;
  cursor: default;
}

/* Listener pages are phones: 44px minimum target. */
[data-density="listener"] .btn {
  min-height: 44px;
}

/* --- 2.2 Fields ----------------------------------------------------------- */

.field {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  margin-bottom: var(--s3);
}
.field > label,
label.t-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--text-3);
}
.field-help {
  font-size: 12px;
  color: var(--text-3);
}
.field-error {
  font-size: 12px;
  color: var(--danger);
}

input,
select,
textarea {
  font-family: inherit;
  font-size: 13px;
  color: var(--text);
  background: var(--surface-2);
  border: 1px solid var(--line-input);
  border-radius: var(--r-control);
  padding: 11px 12px;
  width: 100%;
  transition: border-color var(--t-fast);
}
input:focus,
select:focus,
textarea:focus {
  outline: none;
  border-color: var(--line-strong);
}
input::placeholder,
textarea::placeholder {
  color: var(--text-4);
}
textarea {
  line-height: 1.6;
  resize: vertical;
}
input[type="checkbox"],
input[type="radio"] {
  width: auto;
  accent-color: var(--text-2);
}
input[type="file"] {
  padding: 9px 12px;
  color: var(--text-2);
}
.field-invalid input,
input.invalid,
select.invalid {
  border-color: var(--danger-line);
}

/* A feature that isn't available: dashed, because nothing is written here. */
.field-unavailable {
  font-size: 13px;
  color: var(--text-4);
  background: var(--surface);
  border: 1px dashed var(--line-input);
  border-radius: var(--r-control);
  padding: 11px 12px;
}

.check {
  display: flex;
  align-items: center;
  gap: var(--s2);
  margin: 0;
  font-size: 13px;
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-body);
  cursor: pointer;
}

/* --- 2.3 Pills & badges --------------------------------------------------- */
/* Rounded pill = status or role. Square-ish tag = item type.
   Live is the only coloured one, ever. */

.pill {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  border-radius: var(--r-pill);
  padding: 5px 10px;
  white-space: nowrap;
}

.pill-live,
.pill.live,
.pill.airing {
  color: var(--live-ink);
  border: 1px solid var(--live-line);
  gap: 6px;
}
.pill-planned,
.pill.planned,
.pill.active,
.pill.queued {
  color: var(--text-body);
  border: 1px solid var(--line-strong);
}
.pill-done,
.pill.done,
.pill.played {
  color: var(--text-3);
  border: 1px solid var(--line-input);
}
.pill-archived,
.pill.archived,
.pill.not_a_fit {
  color: var(--text-past);
  border: 1px dashed var(--line-input);
}

/* Role and membership: a filled grey chip, no border. */
.pill-role {
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-2);
  background: var(--surface-2);
}

/* Item type — square-ish, so it can't be mistaken for a status. */
.tag-type {
  display: inline-block;
  font-size: 11px;
  font-weight: 600;
  color: var(--text-3);
  border: 1px solid var(--line-input);
  border-radius: 4px;
  padding: 3px 7px;
  white-space: nowrap;
}

/* --- 2.4 Chips — three variants ------------------------------------------- */

/* A chip that toggles something should be a <button>, so it can be reached and
   pressed from the keyboard — the reset here is what makes that legal, since a
   bare button arrives with the platform's own face and font. */
.chip {
  appearance: none;
  background: none;
  font-family: inherit;
  line-height: 1.2;
  text-align: left;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pill);
  padding: 6px 11px;
  cursor: pointer;
  user-select: none;
  transition: background var(--t-fast), color var(--t-fast), border-color var(--t-fast);
}
.chip:hover {
  color: var(--text);
  border-color: var(--text-3);
}
/* selected */
.chip.is-on {
  font-weight: 600;
  color: var(--invert-text);
  background: var(--invert-bg-soft);
  border-color: var(--invert-bg-soft);
}
.chip.is-on::after {
  content: "✓";
}
/* read-only vocabulary, with its usage count */
.chip-vocab {
  cursor: default;
  border-color: transparent;
  background: var(--surface-2);
}
.chip-vocab:hover {
  color: var(--text-2);
  border-color: transparent;
}
/* A count inside a chip — how many words use this tag, how many tracks are in
   this catalog. Available on any chip, not just the read-only one: choosing what
   a show may draw from is a decision about size as much as about taste. */
.chip .uses {
  color: var(--text-4);
  font-variant-numeric: tabular-nums;
}
/* A chip that stands for something already chosen carries its own way out.
   Where the list shows every option, clicking the chip toggles it and the fill
   tells you which state you are in; where the list shows only what you picked,
   there is nothing to compare against, so removal has to be a visible control
   rather than a second meaning for clicking the label. */
.chip .remove {
  appearance: none;
  border: 0;
  background: none;
  padding: 0 0 0 2px;
  margin-right: -3px;
  font: inherit;
  font-size: 14px;
  line-height: 1;
  color: var(--text-3);
  cursor: pointer;
  border-radius: var(--r-control);
}
.chip .remove:hover {
  color: var(--danger);
}
.chip .remove:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--tool-ring);
}
.chip.is-on .uses {
  color: inherit;
  opacity: 0.6;
}
/* proposed — a word that doesn't exist yet, so: dashed */
.chip-proposed {
  cursor: default;
  color: var(--text-body);
  border: 1px dashed var(--text-past);
}
.chip-proposed:hover {
  color: var(--text-body);
  border-color: var(--text-past);
}

.chiprow {
  display: flex;
  gap: 7px;
  align-items: center;
  flex-wrap: wrap;
}
.chiprow .cat {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-4);
  width: 4.5rem;
  flex: none;
}

/* --- 2.5 Liveness & connection -------------------------------------------- */
/* The dot simmers whenever the station is audible — a listener who can hear
   music is being broadcast to, whether a Selector planned the hour or the
   rotation is filling it. Grey means nothing is going out. The rotation/
   programme distinction is carried by the label and the title, not by colour.
   The SSE indicator is a hollow ring and earns colour only when the connection
   is broken. */

.dot {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  flex: none;
  display: inline-block;
}
.dot-live {
  background: var(--live);
  animation: simmerDot var(--simmer-duration) ease-in-out infinite;
}
/* Nothing is going out: the stream is down, or we can't tell yet. */
.dot-offair {
  background: var(--text-past);
}
.dot-sse {
  border: 1px solid var(--text-past);
  background: transparent;
}
.dot-broken {
  background: var(--danger);
}
.dot-inert {
  background: var(--line-strong);
}

.live-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--live-ink);
}

.liveness {
  display: flex;
  align-items: center;
  gap: var(--s2);
}

/* --- 2.5b The now strip — what the station is putting out ------------------ */
/* A read-only answer to "what am I hearing", for surfaces that can't yet play
   anything: the station gate, the staff sign-in. Solid, not dashed — the data is
   real, it simply isn't audible here.
   It is NOT called a preview. That word belongs to the audition player, and the
   preview/broadcast distinction is a safety property (§2.8) that a second,
   unrelated "preview" would erode. This strip has no transport and never docks,
   so it can't be mistaken for either player. */

.now-strip {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-row);
}
.now-strip .meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
}
.now-strip .where {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-3);
}
.now-strip.is-audible .where {
  color: var(--live-ink);
}
.now-strip .track {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.now-strip .credit {
  font-size: 12px;
  color: var(--text-3);
}
/* Nothing going out, or we couldn't reach the station. */
.now-strip.is-silent .track {
  color: var(--text-3);
  font-weight: 400;
}

/* --- 2.6 Inline status ---------------------------------------------------- */
/* Status text sits immediately below the control it describes and persists.
   There is no toast system and there shouldn't be one — staff work in long
   sessions and need a record they can look back at. */

.status {
  font-size: 12px;
  color: var(--text-3);
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  min-height: 18px;
}
/* Status persists once written, but before anything has happened it is holding
   18px and a margin open for a line that may never come. On a screen with one
   of these under every control that adds up to a card's worth of nothing. It
   reappears the moment there is something to say. */
.status:empty {
  display: none;
}
.status.is-error {
  color: var(--danger);
}
.status.is-quiet {
  color: var(--text-4);
}

/* Named steps, not a spinner. */
.working-bar {
  width: 34px;
  height: 2px;
  background: var(--line-input);
  overflow: hidden;
  flex: none;
}
.working-bar > i {
  display: block;
  width: 25%;
  height: 100%;
  background: var(--text-3);
  animation: working var(--working-duration) ease-in-out infinite;
}

/* A progress bar with a real duration behind it. */
.progress {
  height: 3px;
  background: var(--line);
  border-radius: var(--r-pill);
  overflow: hidden;
}
.progress > i {
  display: block;
  height: 100%;
  background: var(--text-2);
  border-radius: var(--r-pill);
}

/* --- 2.7 The suggestion — machine proposes, human applies ------------------ */
/* One treatment, every use. A dashed container means nothing has been written
   yet. It always carries who proposed it, what it would replace, how sure it is
   when a number exists, and a rejection path with equal visual weight to
   acceptance. Applied, the dash becomes a solid rule and the row states the
   change in plain past tense. */

.suggestion {
  background: var(--surface);
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-card);
  padding: var(--s5);
  display: flex;
  flex-direction: column;
  gap: var(--s3);
}
.suggestion-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s3);
}
.suggestion-source {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--text-3);
}
.suggestion-confidence {
  font-size: 11px;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.suggestion-meter {
  height: 2px;
  background: var(--line);
}
.suggestion-meter > i {
  display: block;
  height: 100%;
  background: var(--text-3);
}
/* what it would replace, against what it proposes */
.suggestion-diff {
  display: grid;
  grid-template-columns: 46px 1fr;
  gap: 6px 10px;
  font-size: 13px;
  align-items: baseline;
}
.suggestion-diff dt {
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--text-4);
}
.suggestion-diff dd {
  margin: 0;
}
.suggestion-diff .was {
  color: var(--text-3);
  text-decoration: line-through;
}
.suggestion-diff .now {
  color: var(--text);
  font-weight: 600;
}
/* Accept and reject carry equal weight: one primary, one secondary, same row. */
.suggestion-actions {
  display: flex;
  gap: var(--s2);
}
.suggestion-actions .btn {
  flex: 1;
  padding: 9px 0;
  text-align: center;
}
.suggestion-note {
  font-size: 12px;
  color: var(--text-4);
  line-height: 1.5;
}

/* Applied: solid rule, plain past tense, who did it and where it came from. */
.applied {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: var(--s4) var(--s5);
  display: flex;
  flex-direction: column;
  gap: var(--s1);
}

/* --- the proposal stack ---------------------------------------------------
   Several offered changes, decided together.

   A working exchange with the agent produces a handful of changes at once, and
   putting an Apply/Reject pair on every one of them turns reading a plan into
   ten decisions — which is how people start clicking Apply without reading. So
   the stack owns the decision and each row owns only its own way out.

   Still dashed, still nothing written: this is `.suggestion` at a different
   granularity, not a different promise. The individual rows stay separately
   rejectable, because "all of this except that one" is the normal answer and a
   stack you can only take whole is a stack you decline whole. */
.proposal-stack {
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-card);
  padding: var(--s4);
  display: flex;
  flex-direction: column;
  gap: var(--s3);
}
.proposal-stack > .head {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  flex-wrap: wrap;
}
.proposal-stack > .head .count {
  margin-left: auto;
  font-size: 12px;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
.proposal-stack .rows {
  border-top: 1px solid var(--line-soft);
}
.proposal-row {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  padding: var(--s3) 0;
  border-bottom: 1px solid var(--line-soft);
}
.proposal-row:last-child {
  border-bottom: none;
}
.proposal-row .what {
  flex: 1 1 16rem;
  min-width: 0;
  font-size: 13px;
  color: var(--text-body);
}
.proposal-row .kind {
  flex: none;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--text-3);
}
.proposal-row .actions {
  margin-left: auto;
  flex: none;
}
/* A row that could not be applied keeps its place and says why. Removing it
   would read as "it went through". */
.proposal-row.is-failed .what {
  color: var(--text-2);
}
.proposal-row .why-failed {
  flex: 1 0 100%;
  font-size: 12px;
  color: var(--danger);
}
.proposal-row.is-decided .what {
  color: var(--text-past);
}
.applied .what {
  font-size: 13px;
  color: var(--text-body);
}
.applied .provenance {
  font-size: 12px;
  color: var(--text-3);
}

/* A suggested row inside an existing list — dashed bottom rule, dimmer text. */
.is-suggested {
  border-bottom: 1px dashed var(--line-strong) !important;
}
.is-suggested .title {
  color: var(--text-2);
}

/* --- 2.8 Audio — broadcast vs preview ------------------------------------- */
/* Safety-critical, so the difference is structural rather than stylistic.
   Broadcast: docked to the bottom edge, full width, accent-marked, says who is
   listening. Preview: inline, dashed, monochrome, always labelled "only you".
   A preview never uses the accent and never docks. */

.player-broadcast {
  position: sticky;
  bottom: 0;
  z-index: 20;
  background: var(--surface);
  /* Grey while the station plays itself; the accent edge is earned only by a
     broadcast that is actually on air. */
  border-top: 1px solid var(--line);
  padding: var(--s3) var(--s5);
  display: flex;
  align-items: center;
  gap: var(--s4);
  overflow: hidden;
}
.player-broadcast.is-live {
  border-top-color: var(--live);
}
.player-broadcast .meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
}
.player-broadcast .track {
  font-size: 14px;
  font-weight: 700;
  color: var(--text);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.player-broadcast .listening {
  font-size: 12px;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
/* No scrubber — you can't rewind the room. Volume and mute only. */
.player-broadcast .transport {
  display: flex;
  align-items: center;
  gap: var(--s2);
}
.player-broadcast input[type="range"] {
  width: 80px;
  padding: 0;
  background: transparent;
  border: none;
  accent-color: var(--text-2);
}

.play-btn {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: var(--invert-bg);
  color: var(--invert-text);
  border: none;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 15px;
  cursor: pointer;
  transition: background var(--t-fast);
}
.play-btn:hover {
  background: var(--invert-bg-soft);
}

.player-preview {
  background: var(--surface);
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-row);
  padding: var(--s3) var(--s4);
  display: flex;
  align-items: center;
  gap: var(--s3);
}
.player-preview .play-btn {
  width: 32px;
  height: 32px;
  background: transparent;
  border: 1px solid var(--text-past);
  color: var(--text-body);
  font-size: 12px;
}
.player-preview .play-btn:hover {
  background: transparent;
  border-color: var(--text-3);
}
.player-preview .meta {
  display: flex;
  flex-direction: column;
  gap: 2px;
  flex: 1;
  min-width: 0;
}
.player-preview .clock {
  font-size: 12px;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
/* Native audio element, when we need one, stays monochrome and inline. */
audio {
  width: 100%;
  margin-top: var(--s2);
  filter: grayscale(1);
}

/* Voice memo recorder — idle / recording / ready / blocked. */
.recorder {
  display: flex;
  align-items: center;
  gap: var(--s3);
}
.rec-btn {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--line-strong);
  background: transparent;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  flex: none;
  transition: border-color var(--t-fast);
}
.rec-btn > i {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--text-body);
  display: block;
}
/* Recording is monochrome and still. This is a listener surface, so the accent
   isn't available — and neither is the simmer, which is the thing that reads as
   liveness. A second pulsing dot near the player teaches people to ignore both.
   The circle becoming a square is the whole state change, and it's enough. */
.rec-btn.is-recording {
  border-color: var(--text);
}
.rec-btn.is-recording > i {
  border-radius: 3px;
  background: var(--text);
}
.rec-btn.is-blocked {
  border-style: dashed;
  cursor: default;
}
.rec-btn.is-blocked > i {
  background: var(--line-strong);
}

/* --- 2.10 The find row — music that isn't the station's yet ---------------- */
/* A result from somewhere outside the library: gruuv today, whatever comes
   next. It follows the same spine as the suggestion — DASHED while it is only
   a candidate, SOLID once a person has pulled it in, and the row then states
   what happened in past tense with where it came from.

   Deliberately monochrome and undecorated: a browsable outside library is a
   tool, not a feature to sell, and none of it is on air. */
.find-row {
  border: 1px solid transparent;
  border-bottom: 1px solid var(--line-soft);
  padding: var(--s3) var(--s3) var(--s3) 0;
  display: flex;
  gap: var(--s3);
  align-items: baseline;
  flex-wrap: wrap;
}
/* A row that IS the action: "create the thing you just searched for".
   It sits at the top of the list because it answers the search rather than
   belonging to the results — searching for something that isn't there is the
   moment you learn you need it, and sending someone to another page to make it
   loses both the name they typed and the reason they wanted it. */
.find-row.is-create {
  width: 100%;
  text-align: left;
  background: none;
  border-top: 0;
  border-left: 0;
  border-right: 0;
  font: inherit;
  color: inherit;
  cursor: pointer;
}
.find-row.is-create:hover {
  background: var(--surface-2);
}
.find-row.is-create:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--tool-ring);
}
.find-row.is-create .mt {
  color: var(--text-3);
}
.find-row:last-child {
  border-bottom-color: transparent;
}
/* A row that opens an editor. The whole row is the target — a title you have to
   hit exactly is a title you miss — so it takes a surface on hover and a
   pointer, and the title underlines to say which words name the thing you would
   be opening. Controls inside the row keep their own click and stop it there.

   The negative margin buys the hover surface room on the left without moving
   the row: .find-row carries no left padding, and a highlight flush against the
   text reads as a rendering fault rather than as a target. */
.find-row.is-openable {
  cursor: pointer;
  padding-left: var(--s3);
  margin-left: calc(-1 * var(--s3));
  border-radius: var(--r-row);
  transition: background var(--t-fast);
}
.find-row.is-openable:hover {
  background: var(--surface-2);
}
.find-row.is-openable:hover .ttl {
  text-decoration: underline;
  text-decoration-color: var(--line-strong);
  text-underline-offset: 3px;
}
/* Keyboard gets the same answer: the focus ring lands on the title, which is
   the element carrying the role, not on the whole row. */
.find-row.is-openable .nm:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--tool-ring);
  border-radius: var(--r-control);
}
/* Not ours yet. The dash is the system's "not yet real", and it is the whole
   difference between this row and a track in the library. */
.find-row.is-new {
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-row);
  padding-left: var(--s3);
  margin-bottom: var(--s2);
}
.find-row .nm {
  flex: 1 1 16rem;
  min-width: 0;
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
}
.find-row .nm .by {
  font-weight: 400;
  color: var(--text-2);
}
.find-row .mt {
  font-size: 12px;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  flex: 0 1 auto;
}
.find-row .actions {
  margin-left: auto;
  display: flex;
  gap: var(--s2);
  align-items: center;
  flex: none;
}
/* Status sits below the whole row and persists, as everywhere else. */
.find-row .status,
.find-row .provenance {
  flex: 1 0 100%;
  margin-top: var(--s1);
}
.find-row .provenance {
  font-size: 12px;
  color: var(--text-3);
}
/* The agent's case for a row it put forward. A suggestion has to carry WHY, or
   it is just a machine asserting — so this sits above the actions, in the
   reading colour rather than the metadata grey, because it is an argument
   addressed to a person and not a caption. */
.find-row .why {
  flex: 1 0 100%;
  margin-top: var(--s2);
  font-size: 13px;
  color: var(--text-2);
}

/* Auditioning a candidate before adopting it. The player here is the PREVIEW
   player (§2.8) — inline, dashed, monochrome, "only you" — because that is
   precisely what it is: one person listening to something no one else can hear.
   It never docks and never takes the accent.

   Not every source needs this. gruuv publishes a hash, so a machine can prove a
   file is what it claims and a Selector may skip the listen. YouTube publishes
   nothing, so the ear IS the check, and the row can't be adopted without it. */
.find-row .audition {
  flex: 1 0 100%;
  margin-top: var(--s3);
  display: flex;
  flex-direction: column;
  gap: var(--s3);
}
/* The credit on a row from outside is a GUESS — split off a video title, or the
   channel name when the title carried no artist at all — so it is offered as two
   fields to correct rather than a value to accept. Approving writes it into the
   library permanently, which is the moment it has to be right. Side by side on
   anything wider than a phone, stacked below.

   Works both as a direct child of the row (a suggestion in prep) and inside an
   audition (the find page), so it needs its own full-width basis. */
.find-row .credit {
  flex: 1 0 100%;
  margin-top: var(--s2);
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--s3);
}
.find-row .audition .credit {
  margin-top: 0;
}
@media (max-width: 560px) {
  .find-row .credit {
    grid-template-columns: 1fr;
  }
}

/* --- figures — a few counted facts about one thing ------------------------
   How big is this catalog: how many tracks, how long it would play, what it
   weighs on disk. Value over label, in a row.

   Greyscale and undecorated on purpose. These are measurements, not a
   dashboard: nothing here is on air, so nothing here takes the accent, and a
   number that needs a colour to be read is a number in the wrong place.

   Four at most, and every one has to be something a person would otherwise
   count by hand. A figure the data can't answer is left out — an invented
   number is worse than a missing one. */
.figures {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s5);
}
.figure {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.figure .value {
  font-size: 19px;
  font-weight: 800;
  letter-spacing: -0.025em;
  color: var(--text);
  font-variant-numeric: tabular-nums;
}
/* The unit rides with the number rather than moving down to the label, so the
   figure reads as one thing: "1.2 GB", not "1.2" over "GB on disk". */
.figure .value .unit {
  font-size: 12px;
  font-weight: 600;
  color: var(--text-3);
  margin-left: 3px;
}
.figure .label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--text-3);
}
/* A figure that is a caveat rather than an achievement — files the disk has
   lost, tracks nobody has tagged. Still grey; it dims instead of shouting,
   because it is a job to do and not a fault to answer for. */
.figure.is-quiet .value {
  color: var(--text-2);
  font-weight: 700;
}

/* --- segmented control ----------------------------------------------------
   One question, two or three mutually exclusive answers, all of them visible:
   upcoming vs past, my catalogs vs all. A select would hide the alternatives
   behind a click, and these are the filters people flip constantly.

   Not a tab strip — it filters what a list contains, it doesn't switch between
   panels. The chosen segment is the only one carrying --tool, and only on staff
   surfaces; on a listener page it stays greyscale, because a filter is not
   liveness. */
.segmented {
  display: inline-flex;
  padding: 2px;
  gap: 2px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-pill);
}
.segmented button {
  appearance: none;
  border: 1px solid transparent;
  background: none;
  border-radius: var(--r-pill);
  padding: 5px var(--s3);
  font: inherit;
  font-size: 12px;
  font-weight: 700;
  letter-spacing: 0.01em;
  color: var(--text-3);
  cursor: pointer;
  transition: color var(--t-fast), background var(--t-fast);
  white-space: nowrap;
}
.segmented button:hover {
  color: var(--text-body);
}
.segmented button[aria-pressed="true"] {
  background: var(--surface);
  border-color: var(--line-strong);
  color: var(--text);
}
.segmented .count {
  margin-left: var(--s1);
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  color: var(--text-3);
}
[data-density="staff"] .segmented button[aria-pressed="true"] {
  background: var(--tool-tint);
  border-color: var(--tool-line);
  color: var(--tool-text);
}
/* Listener pages are phones: the 44px minimum target applies here too. */
[data-density="listener"] .segmented button {
  min-height: 44px;
  padding: 0 var(--s4);
}

/* --- search field ---------------------------------------------------------
   A list long enough to need searching says so with a full-width field above
   it, not a magnifying-glass icon that has to be discovered. The count beside
   it is the honest answer to "did my search do anything". */
.searchfield {
  display: flex;
  align-items: center;
  gap: var(--s3);
  margin-bottom: var(--s3);
}
.searchfield input {
  flex: 1 1 auto;
  min-width: 0;
}
.searchfield .found {
  flex: none;
  font-size: 12px;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* --- toggle ---------------------------------------------------------------
   A permission that is one line, not three paragraphs. The explanation belongs
   under it as .field-help; a switch whose consequences take a screen to read is
   a switch nobody reads.

   Deliberately built on a real checkbox so it keeps keyboard and screen-reader
   behaviour for free — the input is positioned over the track, not hidden. */
.toggle {
  display: flex;
  align-items: center;
  gap: var(--s3);
  cursor: pointer;
  margin: 0;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-body);
}
.toggle > input[type="checkbox"] {
  appearance: none;
  flex: none;
  position: relative;
  width: 34px;
  height: 20px;
  margin: 0;
  border-radius: var(--r-pill);
  background: var(--surface-2);
  border: 1px solid var(--line-strong);
  cursor: pointer;
  transition: background var(--t-fast), border-color var(--t-fast);
}
.toggle > input[type="checkbox"]::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: var(--text-3);
  transition: transform var(--t-fast), background var(--t-fast);
}
.toggle > input[type="checkbox"]:checked {
  background: var(--tool-tint);
  border-color: var(--tool-line);
}
.toggle > input[type="checkbox"]:checked::after {
  transform: translateX(14px);
  background: var(--tool-text);
}
.toggle > input[type="checkbox"]:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--tool-ring);
}
.toggle .t-meta {
  margin-left: auto;
}
/* A toggle and its consequence are one unit: the help text tucks under the
   switch it belongs to, indented past the track so the switches line up as a
   column you can scan. */
.toggle + .field-help {
  display: block;
  margin: var(--s1) 0 0 calc(34px + var(--s3));
  max-width: 68ch;
}
.toggle + .field-help + .status {
  margin-left: calc(34px + var(--s3));
}

/* --- agent instructions ---------------------------------------------------
   The one thing on a prep screen that decides how the hour sounds. It reads as
   a tinted panel rather than a plain card because it is addressed to the agent,
   not to the Selector — everything else on the page is theirs to arrange, this
   is what gets handed over.

   Staff-only. It uses --tool as a TINT with a border, never a solid fill, so it
   can't compete with the one primary button on the screen. */
.instructions {
  background: var(--tool-tint);
  border: 1px solid var(--tool-line);
  border-radius: var(--r-card);
  padding: var(--s4);
  display: flex;
  flex-direction: column;
  gap: var(--s2);
}
/* The eyebrow here uses the existing `.is-focus` modifier rather than an
   override of its own — "the section eyebrow on the one card the screen is
   about" is already what that modifier means, and a second way to say it would
   be a second thing to keep in step. */
/* The instructions read in --tool rather than body grey. Everything else on a
   prep screen is the Selector's own working material; this is the one block
   addressed to the agent, and colouring the words themselves is what separates
   "what I am arranging" from "what I am handing over". */
.instructions .body {
  font-size: 14px;
  line-height: 1.6;
  color: var(--tool-text);
  white-space: pre-wrap;
  max-width: 68ch;
}
/* Nothing written yet is a prompt, not an instruction — so it drops out of the
   accent and reads as the placeholder it is. */
.instructions .body.is-empty {
  color: var(--text-3);
}
.instructions textarea {
  width: 100%;
  background: var(--bg);
}
.instructions .row {
  gap: var(--s2);
}

/* --- modal ----------------------------------------------------------------
   Editing one row out of a list of hundreds. The list stays legible because the
   editors aren't in it; the editor gets room because it isn't in the list.

   Only for a focused edit of one thing. Never for confirmation — destructive
   choices use .confirm inline, where the thing being destroyed is still on
   screen and can be looked at while deciding. */
.modal {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--s5);
  background: rgba(8, 8, 10, 0.72);
}
.modal[hidden] {
  display: none;
}
.modal-sheet {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: var(--s5);
  width: min(560px, 100%);
  max-height: calc(100dvh - var(--s7));
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--s4);
}
.modal-head {
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  padding-bottom: var(--s3);
  border-bottom: 1px solid var(--line);
}
.modal-head .grow {
  min-width: 0;
}
.modal-close {
  appearance: none;
  border: none;
  background: none;
  color: var(--text-3);
  font-size: 20px;
  line-height: 1;
  padding: var(--s1) var(--s2);
  cursor: pointer;
  border-radius: var(--r-control);
}
.modal-close:hover {
  color: var(--text);
}
.modal-close:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--tool-ring);
}
/* A long list inside a sheet scrolls on its own, so the actions underneath stay
   put. Without this the whole sheet scrolls and "Add selected" walks off the
   bottom — which is exactly the button you need after scrolling a long way. */
.modal-list {
  overflow-y: auto;
  max-height: min(46vh, 380px);
  /* The scroll edge needs a line, or the last row looks like the end of the
     list when it is only the end of the window. */
  border-top: 1px solid var(--line-soft);
  border-bottom: 1px solid var(--line-soft);
}
.modal-actions {
  display: flex;
  gap: var(--s3);
  align-items: center;
  padding-top: var(--s3);
  border-top: 1px solid var(--line);
}
/* The destructive way out sits apart from save/cancel, not beside them. */
.modal-actions .apart {
  margin-left: auto;
}
@media (max-width: 560px) {
  .modal {
    padding: 0;
    align-items: flex-end;
  }
  .modal-sheet {
    border-radius: var(--r-card) var(--r-card) 0 0;
    max-height: 92dvh;
  }
}

/* ===========================================================================
   3. CROSS-CUTTING STATES
   Every one says what happened, why, and what you can do next — in that order,
   in plain language. No apologies, no exclamation marks, no error codes in the
   primary line.
   =========================================================================== */

.state {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  padding: var(--s5) 0;
}
.state .headline {
  font-size: 17px;
  font-weight: 700;
  color: var(--text);
}
.state .explain {
  font-size: 13px;
  line-height: 1.6;
  color: var(--text-3);
  max-width: 60ch;
}
.state .btn {
  align-self: flex-start;
  margin-top: var(--s1);
}
/* Degraded: the feature isn't there, so the container is dashed. */
.state-degraded {
  background: var(--surface);
  border: 1px dashed var(--line-strong);
  border-radius: var(--r-card);
  padding: var(--s5);
}
.state-degraded .headline {
  font-size: 15px;
  font-weight: 600;
  color: var(--text-body);
}

/* Destructive confirm: delete is text-only red, keep is a normal secondary. */
.confirm {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
}
.confirm .actions {
  display: flex;
  gap: var(--s2);
  margin-top: var(--s1);
}

/* Full-screen password gate. */
.gate-overlay {
  position: fixed;
  inset: 0;
  background: var(--bg);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
  padding: var(--s5);
}
.gate-overlay[hidden] {
  display: none;
}
.gate-box {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-card);
  padding: var(--s6);
  /* Wide enough that the now strip can show a real track title without
     ellipsing it away. */
  width: 380px;
  max-width: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--s3);
}

/* ===========================================================================
   4. PAGE PATTERNS
   =========================================================================== */

/* --- 4.1 Listener --------------------------------------------------------- */

.listener-shell {
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
  position: relative;
}
.listener-shell > main {
  flex: 1;
  position: relative;
  z-index: 1;
}

.now-block {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
}
.now-block .show-title {
  font-size: 28px;
  font-weight: 800;
  letter-spacing: -0.035em;
  line-height: 1.05;
  color: var(--text);
}
.now-block .credit {
  font-size: 14px;
  color: var(--text-2);
}
.now-playing {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  padding: var(--s4) 0;
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line-soft);
}
.now-playing .track {
  font-size: 19px;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--text);
}

/* A list of items where the type is a tag on the right. */
.itemlist {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
}
.itemlist li {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s3);
  padding: var(--s3) 0;
  border-bottom: 1px solid var(--line-soft);
}
.itemlist li:last-child {
  border-bottom: none;
}
.itemlist .what {
  font-size: 14px;
  color: var(--text-body);
  flex: 1;
  min-width: 0;
}
.itemlist .when {
  font-size: 12px;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.itemlist .credit {
  font-size: 12px;
  color: var(--text-3);
}
.itemlist li.is-past .what {
  color: var(--text-past);
}

/* Where "now" falls in today's list. Greyscale and unmistakably a rule rather
   than a row: on a listener surface the accent means audible, and the time of
   day is not that. The label sits on the right, in line with the times it is
   being read against. */
.itemlist li.now-marker {
  border-bottom: none;
  padding: 0;
  min-height: 0;
  align-items: center;
  gap: var(--s3);
  color: var(--text-2);
}
.itemlist li.now-marker .what {
  flex: 1 1 auto;
  border-top: 1px solid var(--text-3);
}
.itemlist li.now-marker .when {
  color: var(--text-2);
  font-weight: 700;
}

/* --- 4.1b Listener home: schedule and the room alongside ------------------
   Two columns on a wide screen, one on a phone — and the phone order is the
   source order, so what is playing and what is on today come before the
   sidebar. Listener density, so every target stays at 44px. */
.home-cols {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--s6);
  align-items: start;
}
@media (min-width: 900px) {
  .home-cols {
    grid-template-columns: 1fr 280px;
  }
}

/* --- 4.1c Prep: the two music permissions, side by side -------------------
   What the show may draw from inside the library and what it may reach for
   outside it are one decision read together, so they sit on one row.

   Flex rather than a two-column grid on purpose: the outside card is hidden
   whenever the station has neither gruuv nor YouTube switched on, and a grid
   would hold its empty column open. Here the remaining card just takes the row.

   The basis is what decides when they stack, and it is set by the narrower of
   the two: a toggle plus its consequence needs about 420px before the help text
   starts breaking every few words. */
.music-cols {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s5);
  /* Each card is its own height. Stretching a two-switch card to match a tall
     one beside it buys a matched edge and pays for it with a panel that is
     mostly nothing — and empty space inside a bordered box reads as content
     that failed to load. */
  align-items: flex-start;
  margin-bottom: var(--s5);
}
.music-cols > .card {
  flex: 1 1 420px;
  min-width: 0;
  margin-bottom: 0;
}
/* A card holding only switches asks for the width of its longest label, not an
   equal share of the row. */
.music-cols > .is-slim {
  flex: 0 1 280px;
}
/* Once the row has wrapped there is nothing to sit beside, and a 280px card
   under a full-width one just looks like it failed to line up. Below this the
   two never fit together anyway. */
@media (max-width: 720px) {
  .music-cols > .is-slim {
    flex: 1 1 100%;
  }
}

/* --- 4.2 Prep: conversation left, rundown right --------------------------- */
/* On mobile the rundown comes first — you check the plan more often than you
   talk — which is why the source order is rundown, conversation, and the
   desktop grid puts the conversation back on the left. */

.prep-cols {
  display: grid;
  grid-template-columns: 1fr 380px;
  gap: var(--s5);
  align-items: start;
}
.prep-cols > .prep-talk {
  order: 1;
}
/* The right column is a stack, not a single card: the agent's instructions sit
   directly above the rundown they produced, so you can read the brief against
   the plan without scrolling between them. */
.prep-cols > .prep-run {
  order: 2;
  display: flex;
  flex-direction: column;
  gap: var(--s5);
}
/* The column owns the spacing; a card's own bottom margin would double it. */
.prep-cols > .prep-run > .card {
  margin-bottom: 0;
}
@media (max-width: 900px) {
  .prep-cols {
    grid-template-columns: 1fr;
  }
  .prep-cols > .prep-run {
    order: 1;
  }
  .prep-cols > .prep-talk {
    order: 2;
  }
}

.thread {
  max-height: 56vh;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: var(--s4);
  padding-right: var(--s1);
}
.msg {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  max-width: 88%;
}
.msg .who {
  font-size: 11px;
  font-weight: 600;
  color: var(--text-3);
}
.msg .body {
  font-size: 13px;
  line-height: 1.6;
  border-radius: var(--r-row);
  padding: 11px 13px;
  white-space: pre-wrap;
}
/* The person is the solid, high-contrast voice; the machine is a grey surface. */
.msg-user {
  align-self: flex-end;
  align-items: flex-end;
}
.msg-user .body {
  color: var(--invert-text);
  background: var(--invert-bg-soft);
}
.msg-agent .body {
  color: var(--text-body);
  background: var(--surface-2);
}

.rundown {
  display: flex;
  flex-direction: column;
}
.rundown .item {
  display: flex;
  flex-direction: column;
  gap: 3px;
  padding: 11px 0;
  border-bottom: 1px solid var(--line-soft);
}
.rundown .item:last-child {
  border-bottom: none;
}
.rundown .line {
  display: flex;
  gap: var(--s3);
  align-items: baseline;
}
.rundown .n {
  font-size: 11px;
  color: var(--text-4);
  font-variant-numeric: tabular-nums;
  width: 34px;
  flex: none;
}
.rundown .title {
  font-size: 13px;
  font-weight: 600;
  color: var(--text);
}
/* The agent's one-line reason for the choice, indented under the title. */
.rundown .note {
  font-size: 11px;
  line-height: 1.5;
  color: var(--text-3);
  padding-left: 46px;
}
.rundown .item.is-talk {
  background: var(--surface-raised);
}
.rundown .item.is-talk .title {
  color: var(--text-body);
}

/* --- 4.3 Library: the table owns the page, tools go in a left rail --------- */

.library-cols {
  display: grid;
  grid-template-columns: 240px 1fr;
  gap: var(--s5);
  align-items: start;
}
@media (max-width: 900px) {
  .library-cols {
    grid-template-columns: 1fr;
  }
}
.rail {
  display: flex;
  flex-direction: column;
  gap: var(--s5);
  position: sticky;
  top: var(--s4);
}
.rail section {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
}
.rail section + section {
  padding-top: var(--s4);
  border-top: 1px solid var(--line-soft);
}
.rail .rail-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--s2);
  font-size: 13px;
  color: var(--text-2);
}
.rail .rail-row .count {
  font-size: 11px;
  color: var(--text-4);
  font-variant-numeric: tabular-nums;
}

table {
  width: 100%;
  border-collapse: collapse;
}
th {
  text-align: left;
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--text-4);
  padding: 9px 8px;
  border-bottom: 1px solid var(--line-soft);
}
td {
  padding: 9px 8px;
  border-bottom: 1px solid var(--line-soft);
  vertical-align: middle;
  font-size: 13px;
}
td.actions {
  text-align: right;
  white-space: nowrap;
}
td.actions .btn {
  margin-left: var(--s1);
}
td input,
td select {
  padding: 7px 9px;
  font-size: 12px;
}
tr.metarow td {
  border-bottom: none;
  padding-bottom: 0;
}
tr.noterow td {
  border-bottom: 1px solid var(--line-soft);
  padding-top: var(--s1);
  padding-bottom: var(--s3);
}
.dur {
  color: var(--text-3);
  font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.othernote {
  font-size: 12px;
  color: var(--text-3);
  padding: var(--s1) 0 0 var(--s5);
}
.othernote .who {
  color: var(--text-2);
  font-weight: 600;
}

/* --- 4.4 Calendar: week grid, then an agenda on phones -------------------- */

.cal-grid {
  display: grid;
  grid-template-columns: 3.6rem repeat(7, 1fr);
  gap: 1px;
  background: var(--line-soft);
  border: 1px solid var(--line-soft);
  border-radius: var(--r-row);
  overflow: hidden;
}
.cal-grid > div {
  background: var(--surface);
}
.cal-hdr {
  padding: var(--s2) var(--s1);
  text-align: center;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--text-3);
}
.cal-hdr .dnum {
  display: block;
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.015em;
  color: var(--text-2);
  text-transform: none;
  font-variant-numeric: tabular-nums;
}
.cal-hdr.today .dnum {
  color: var(--text);
}
.cal-gutter {
  position: relative;
}
.cal-gutter .hr {
  position: absolute;
  right: var(--s1);
  font-size: 11px;
  color: var(--text-4);
  font-variant-numeric: tabular-nums;
  transform: translateY(-0.5em);
}
.cal-day {
  position: relative;
}
.cal-day .line {
  position: absolute;
  left: 0;
  right: 0;
  border-top: 1px solid var(--line-soft);
}
/* An hour a person planned, against empty air.

   The separation is carried by the top and bottom edges, not by the fill:
   --surface-2 on --surface is 1.05:1, which is no edge at all, and any fill
   pale enough to read here would be a light patch on a dark station. What is
   actually being asked on this grid is "when does this start and when does it
   stop", so those two edges get the contrast (--text-2, 7.4:1) and the sides
   stay quiet. The fill is texture underneath, not the signal. */
.cal-ev {
  position: absolute;
  left: 2px;
  right: 2px;
  border-radius: var(--r-control);
  padding: 4px 7px;
  font-size: 12px;
  overflow: hidden;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-top-color: var(--text-2);
  border-bottom-color: var(--text-2);
  cursor: pointer;
}
/* Wraps to two lines rather than truncating. A grid block is the only place a
   programme's name appears on this screen, and "Wake Up with Geor…" is the
   component failing rather than the name being too long. */
.cal-ev .nm {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  overflow: hidden;
  color: var(--text);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: -0.01em;
}
.cal-ev .mt {
  display: block;
  color: var(--text-2);
  font-size: 11px;
  margin-top: 1px;
  font-variant-numeric: tabular-nums;
}
/* Only a broadcast that is actually on air gets the accent — the same tinted
   fill and accent border the on-air strip uses, not the wash. */
.cal-ev.airing {
  /* The tint is translucent in dark, so it rides on an opaque base — otherwise
     the hour rules behind it show through the block as false seams. */
  background-color: var(--surface);
  background-image: linear-gradient(var(--live-tint), var(--live-tint));
  border-color: var(--live-tint-line);
  border-top-color: var(--live);
  border-bottom-color: var(--live);
}
.cal-ev.airing .nm {
  color: var(--text);
}
/* Aired drops wholly — edges, name and time together. */
.cal-ev.done {
  /* Opaque, matching the column rather than lifting off it — an aired show
     keeps its shape but stops being a raised surface. */
  background: var(--surface);
  border-color: var(--line-soft);
  border-top-color: var(--line-strong);
  border-bottom-color: var(--line-strong);
}
.cal-ev.done .nm,
.cal-ev.done .mt {
  color: var(--text-past);
  font-weight: 400;
}
/* A Selector has said this broadcast is prepared. Staff surfaces only — the
   listener calendar is never sent the field, because how ready a show is is the
   station's business and reads to an audience as a promise.

   A tick and a left edge, not a fill: readiness is a side note next to when the
   show runs, and a filled block would compete with the one thing on this grid
   that is allowed to shout, which is a show that is actually on air. */
[data-density="staff"] .cal-ev.is-ready {
  border-left: 2px solid var(--tool-line);
}
[data-density="staff"] .cal-ev.is-ready .ready-mark {
  color: var(--tool-text);
  margin-right: 3px;
  font-weight: 700;
}
.cal-now {
  position: absolute;
  left: 0;
  right: 0;
  border-top: 1px solid var(--text-past);
  z-index: 3;
}

.agenda {
  display: none;
}
.agenda .day-h {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--text-3);
  margin: var(--s5) 0 var(--s2);
}
.agenda .arow {
  display: flex;
  gap: var(--s3);
  padding: var(--s3) 0;
  border-bottom: 1px solid var(--line-soft);
}
.agenda .arow .t {
  width: 8.5rem;
  flex: none;
  font-size: 12px;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
}
@media (max-width: 760px) {
  .cal-grid {
    display: none;
  }
  .agenda {
    display: block;
  }
}

/* --- schedule: programmes and upcoming broadcasts -------------------------- */

.series {
  padding: var(--s4) 0;
  border-bottom: 1px solid var(--line-soft);
}
.series:last-child {
  border-bottom: none;
}
.series .top {
  display: flex;
  gap: var(--s3);
  align-items: baseline;
  flex-wrap: wrap;
}
/* Row title weight, not heading weight: this is a line you scan a list for,
   and 800 at 15 separates it from the slot beside it without another size. */
.series .nm {
  font-size: 15px;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--text);
}
.series .slot {
  font-size: 12px;
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
}
.series .desc {
  font-size: 13px;
  color: var(--text-3);
  margin-top: var(--s1);
}

/* Archived drops wholly — name, slot, description and pill together. */
.series.is-past .nm,
.series.is-past .slot,
.series.is-past .desc,
.series.is-past .pill {
  color: var(--text-past);
}
.editbox {
  border-left: 1px solid var(--line-strong);
  padding-left: var(--s4);
  margin-top: var(--s3);
}
.day-head {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--text-3);
  padding: var(--s4) 0 var(--s1);
}
.occ {
  display: flex;
  gap: var(--s3);
  align-items: baseline;
  padding: var(--s3) 0;
  border-bottom: 1px solid var(--line-soft);
}
.occ:last-child {
  border-bottom: none;
}
.occ .when {
  width: 15rem;
  flex: none;
  font-size: 13px;
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
}
.occ .nm {
  flex: 1;
  font-size: 15px;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--text);
}
.occ .nm .t-meta {
  font-weight: 400;
  letter-spacing: 0;
}
.occ.is-past .when,
.occ.is-past .nm,
.occ.is-past .nm .t-meta,
.occ.is-past .pill {
  color: var(--text-past);
}

.formgrid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--s3);
}

/* ---------------------------------------------------------------------------
   4.6 Staff surfaces — the only place --tool is legal
   ---------------------------------------------------------------------------
   Everything a coordinator or Selector uses to run the station and no listener
   sees: the schedule builder, accounts, library admin, prep, the booth. They
   are also the screens most likely to go flat, because every surface sits
   within a few points of value of every other one and all the type is one size
   and one weight. Adding a hue to that tints the flatness rather than fixing
   it, so the value and type work below comes FIRST and the accent is the last
   10%.

   --live is still here and still means only "on air now" — the on-air strip
   keeps the orange dot and the simmer, and it is the only orange on a staff
   screen. --tool never touches anything that also renders to a listener.
   --------------------------------------------------------------------------- */

/* Inputs recess BACK to the page value instead of lifting off the card. An
   input must never be the same value as the card it sits in; on staff screens
   the card is doing the lifting, so the field goes the other way. Listener
   pages keep the raised field — a phone form wants a target, not a well. */
[data-density="staff"] input,
[data-density="staff"] select,
[data-density="staff"] textarea {
  background: var(--bg);
  border-color: var(--line-input);
}

/* Focus is the one state that gets the accent on every control, because it
   answers "where am I typing" and nothing else on the screen answers it. */
[data-density="staff"] input:focus,
[data-density="staff"] select:focus,
[data-density="staff"] textarea:focus {
  border-color: var(--tool);
  box-shadow: 0 0 0 3px var(--tool-ring);
}
[data-density="staff"] .btn:focus-visible,
[data-density="staff"] a:focus-visible,
[data-density="staff"] .chip:focus-visible,
[data-density="staff"] .rows-row.is-expandable:focus-visible {
  outline: 2px solid var(--tool);
  outline-offset: 2px;
}

/* The one primary action on the screen. Staff surfaces are the exception to
   white-on-black: there is no liveness to compete with here, and the accent
   the button would compete with isn't legal on this screen anyway. Still one
   per view, and still the ONLY solid --tool fill allowed — tints and borders
   below are unlimited. */
[data-density="staff"] .btn-primary {
  color: var(--tool-on-strong);
  background: var(--tool-strong);
  border-color: var(--tool-strong);
}
[data-density="staff"] .btn-primary:hover:not(:disabled) {
  color: var(--tool-on-strong);
  background: var(--tool);
  border-color: var(--tool);
}

/* A row-level primary — the "apply" inside a suggestion, the "save" inside a
   track — is outlined rather than filled. A library screen can show a dozen of
   these at once, and a dozen solid fills is exactly the "more than one per
   screen" the rule forbids. Border and text still mark it as the accept, so
   the rejection path keeps its equal weight beside it. */
[data-density="staff"] .btn-primary.btn-sm {
  color: var(--tool-text);
  background: transparent;
  border-color: var(--tool-line);
}
[data-density="staff"] .btn-primary.btn-sm:hover:not(:disabled) {
  color: var(--tool-text);
  background: var(--tool-tint);
  border-color: var(--tool);
}
[data-density="staff"] .btn-primary.btn-sm:disabled {
  color: var(--text-4);
  background: transparent;
  border-color: var(--line);
}

/* Active nav: an underline, not a fill. Two marks would be one too many. */
[data-density="staff"] .nav a[aria-current="page"] {
  color: var(--text);
  border-bottom: 2px solid var(--tool);
  padding-bottom: 2px;
}

/* Type. The flatness was as much one weight at one size as it was one value,
   so staff screens step the scale UP at the two places hierarchy is read:
   the card you are working in, and the row you are scanning for. */
[data-density="staff"] .t-card-title {
  font-size: 19px;
  font-weight: 800;
  letter-spacing: -0.025em;
  color: var(--text);
}
[data-density="staff"] .t-row-title {
  font-size: 15px;
  font-weight: 800;
  letter-spacing: -0.02em;
  color: var(--text);
}

/* The section label above a card title. Greyscale by default; it earns --tool
   on the ONE card the screen is actually about, which is what makes it worth
   reading at all. */
[data-density="staff"] .eyebrow {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--text-3);
}
[data-density="staff"] .eyebrow.is-focus {
  color: var(--tool-text);
}
[data-density="staff"] .eyebrow + .t-card-title {
  margin-top: var(--s1);
}

/* A section header inside a card: the heavier rule beneath is what makes the
   rows below read as one group rather than as loose lines. */
[data-density="staff"] .eyebrow.is-section {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s3);
  padding-bottom: var(--s2);
  margin-bottom: var(--s2);
  border-bottom: 1px solid var(--line);
}
[data-density="staff"] .eyebrow.is-section > .t-meta {
  font-weight: 400;
  letter-spacing: 0;
  text-transform: none;
  color: var(--text-3);
}

/* Lists are rules BETWEEN rows, never boxes around them — a box per row is
   another border to price in, and the eye reads the gaps as structure anyway.
   The section header gets the heavier rule so the group reads as a group. */
[data-density="staff"] .rows {
  display: flex;
  flex-direction: column;
}
[data-density="staff"] .rows > .eyebrow {
  padding-bottom: var(--s2);
  border-bottom: 1px solid var(--line);
}
[data-density="staff"] .rows > .rows-row {
  display: flex;
  align-items: center;
  gap: var(--s3);
  min-height: var(--row-h);
  padding: var(--s3) 0;
  border-bottom: 1px solid var(--line-soft);
}
[data-density="staff"] .rows > .rows-row:last-child {
  border-bottom: none;
}
[data-density="staff"] .rows > .rows-row > .t-row-title {
  flex: 1;
  min-width: 0;
}
[data-density="staff"] .rows .meta {
  font-size: 12px;
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* Past and paused drop WHOLLY — title, meta and pill together. A row that dims
   in one place and not another reads as a rendering bug, not as a state. */
[data-density="staff"] .rows-row.is-past,
[data-density="staff"] .rows-row.is-past .t-row-title,
[data-density="staff"] .rows-row.is-past .meta,
[data-density="staff"] .rows-row.is-past .pill {
  color: var(--text-past);
}
[data-density="staff"] .rows-row.is-past .pill {
  border-color: var(--line);
}

/* --- an expandable list row ------------------------------------------------
   A row whose detail is worth keeping but not worth the space by default: past
   broadcasts, where the dates are the index and one setlist is what you go
   looking for. Twenty rows of song titles at the top of a workspace is a wall;
   twenty dates with one opened is a list.

   The row is a <button> and this class carries the reset that makes that legal
   — the same bargain .chip makes, and for the same reason: a thing you press
   has to be reachable from the keyboard. Only the three borders the rule
   between rows doesn't own are reset, so the list keeps its structure.

   The marker is always visible and only rotates, because a list that expands
   has to say so before you click it. The focus ring is declared with the other
   staff controls, further down — this is a control, so it rings like one. */
[data-density="staff"] .rows > .rows-row.is-expandable {
  width: 100%;
  appearance: none;
  background: none;
  border-top: 0;
  border-left: 0;
  border-right: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  transition: background var(--t-fast);
}
[data-density="staff"] .rows > .rows-row.is-expandable:hover {
  background: var(--surface-2);
}
.rows-row .disclosure {
  color: var(--text-3);
  font-size: 10px;
  line-height: 1;
  transition: transform var(--t-fast);
}
.rows-row[aria-expanded="true"] .disclosure {
  transform: rotate(90deg);
}
/* The detail, indented under the row that opened it. It carries the rule so
   the group still reads as one row's worth of list. */
[data-density="staff"] .rows > .rows-panel {
  padding: 0 0 var(--s3) var(--s4);
  border-bottom: 1px solid var(--line-soft);
}
[data-density="staff"] .rows > .rows-panel:last-child {
  border-bottom: none;
}

/* The enabled/active state pill. Read-only state, so it may carry the tint;
   .chip.is-on stays white-on-black because that one is a control a person
   toggles, and a control and a state should not look alike. */
[data-density="staff"] .pill.active,
[data-density="staff"] .pill.enabled {
  color: var(--tool-text);
  background: var(--tool-tint);
  border: 1px solid var(--tool-line);
}

/* The on-air strip. The one orange on a staff screen, and it means what it has
   always meant. Tinted fill and border rather than the wash: the wash is for
   programmes on listener surfaces, and this strip covers rotation too. */
.onair-strip {
  display: flex;
  align-items: center;
  gap: var(--s2);
  padding: 11px var(--s3);
  border-radius: var(--r-row);
  background: var(--live-tint);
  border: 1px solid var(--live-tint-line);
  margin-bottom: var(--s4);
}
.onair-strip .what {
  font-size: 13px;
  font-weight: 700;
  color: var(--text);
}
.onair-strip .meta {
  font-size: 12px;
  color: var(--live-ink);
  font-variant-numeric: tabular-nums;
}
/* Nothing going out: the strip goes inert rather than disappearing, so the
   answer to "are we on air" is in the same place either way. */
.onair-strip.is-offair {
  background: transparent;
  border-color: var(--line);
}
.onair-strip.is-offair .what {
  color: var(--text-2);
}
.onair-strip.is-offair .meta {
  color: var(--text-3);
}

/* ===========================================================================
   5. PAID MESSAGES
   No accent, no simmer, no second brand colour. A visible interruption: a
   full-bleed band with hard rules top and bottom, the buyer named, and the time
   remaining shown so it reads as finite. If paid messages ever need a colour of
   their own, that's the signal there are too many of them.
   =========================================================================== */

.paid-band {
  background: var(--surface-2);
  border-top: 1px solid var(--line-strong);
  border-bottom: 1px solid var(--line-strong);
  padding: var(--s4) var(--s5);
  display: flex;
  align-items: center;
  gap: var(--s4);
}
.paid-band .buyer {
  font-size: 14px;
  font-weight: 700;
  color: var(--text-body);
}
.paid-band .remaining {
  font-size: 12px;
  color: var(--text-3);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.paid-label {
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-3);
}

/* ===========================================================================
   6. IDENTITY
   Avatars are monograms — no uploaded photos anywhere. Squared for Selectors,
   round for listeners. Attention points at the music.
   =========================================================================== */

.avatar {
  width: 40px;
  height: 40px;
  border-radius: var(--r-row);
  background: var(--surface-2);
  border: 1px solid var(--line-input);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 16px;
  font-weight: 800;
  letter-spacing: -0.03em;
  color: var(--text-2);
  flex: none;
}
.avatar-listener {
  border-radius: var(--r-pill);
  font-weight: 700;
}
.avatar-sm {
  width: 26px;
  height: 26px;
  font-size: 11px;
}

/* Four monograms maximum, then a count — the room is a number, not a directory. */
.room {
  display: flex;
  align-items: center;
}
.room .avatar + .avatar {
  margin-left: -7px;
  border-color: var(--surface);
}
.room .count {
  margin-left: var(--s3);
  font-size: 13px;
  color: var(--text-2);
  font-variant-numeric: tabular-nums;
}

/* Credit line: name, plain, never a byline with a photo. */
.credit strong,
.credit .name {
  font-weight: 600;
  color: var(--text);
}

/* --- utilities ------------------------------------------------------------ */

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}
[hidden] {
  display: none !important;
}
.grow {
  flex: 1;
}
.mt-2 { margin-top: var(--s2); }
.mt-3 { margin-top: var(--s3); }
.mt-4 { margin-top: var(--s4); }
