/* GENERATED by scripts/tokens.py from config/design-tokens.json
   DO NOT EDIT — edit the JSON and rebuild. */

:root {
  --bg-canvas: #f7f9fc;
  --bg-texture-from: #eef3fb;
  --bg-texture-fade: rgba(238, 243, 251, 0);
  --surface-raised: #ffffff;
  --surface-sunken: #eef1f6;
  --border-default: #dde3ec;
  --border-subtle: rgba(21, 27, 36, 0.12);
  --border-subtle-strong: rgba(21, 27, 36, 0.2);
  --border-on-action: rgba(255, 255, 255, 0.35);
  --text-primary: #151b24;
  --text-secondary: #4d5a6b;
  --text-on-action: #ffffff;
  --action-bg: #1e3568;
  --brand-accent: #2a4785;
  --brand-accent-soft: #faeec2;
  --focus-ring: #2a4785;
  --danger-fg: #b02525;
  --placeholder-fg: #b02525;
  --scrim-modal: rgba(10, 17, 34, 0.6);
  --shadow-ambient: rgba(16, 26, 51, 0.18);
  --shadow-soft: rgba(16, 26, 51, 0.2);
  --shadow-player: rgba(16, 26, 51, 0.14);
  --badge-louvor-bg: #faeec2;
  --badge-louvor-fg: #5c3f0d;
  --badge-adoracao-bg: #f6dce3;
  --badge-adoracao-fg: #7c2d47;
  --badge-pregacao-bg: #d9e8d9;
  --badge-pregacao-fg: #2c5530;
  --badge-harpa-bg: #e9e2f7;
  --badge-harpa-fg: #543090;
  --badge-geral-bg: #dde3ec;
  --badge-geral-fg: #36414f;
  --share-whatsapp-bg: #1d7c53;
  --share-whatsapp-fg: #ffffff;
  --og-bg-from: #101a33;
  --og-bg-to: #0a1122;
  --og-ink: #ffffff;
  --og-ink-muted: #8aa8db;
  --og-accent: #e8c65a;
  --chart-ink: #151b24;
  --chart-muted: #4d5a6b;
  --chart-grid: #dde3ec;
  --chart-tip-bg: #151b24;
  --chart-tip-ink: #ffffff;
  --chart-series-1: #3a5fa8;
  --chart-step-1: #b3c8ea;
  --chart-step-2: #5c83c6;
  --chart-step-3: #2a4785;
  --chart-step-4: #16264b;
  --chart-step-5: #101a33;
  --typography-family-body: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --typography-family-display: Georgia, "Noto Serif", "Times New Roman", serif;
  --typography-root-percent: 112.5%;
  --typography-leading-body: 1.6;
  --typography-leading-tight: 1.25;
  --typography-size-micro: 0.8rem;
  --typography-size-meta: 0.9rem;
  --typography-size-body: 1rem;
  --typography-size-title: 1.2rem;
  --typography-size-section: 1.44rem;
  --typography-size-page: 1.728rem;
  --typography-tracking-caps: 0.02em;
  --shape-radius-lg: 12px;
  --shape-radius-md: 8px;
  --shape-radius-sm: 4px;
  --shape-layout-content-max: 62rem;
  --shape-control-min-target: 3rem;
}

/* Dark: applied by the explicit toggle... */
:root[data-theme="dark"] {
  --bg-canvas: #0a1122;
  --bg-texture-from: #101a33;
  --bg-texture-fade: rgba(16, 26, 51, 0);
  --surface-raised: #101a33;
  --surface-sunken: #16264b;
  --border-default: #1e3568;
  --border-subtle: rgba(238, 241, 246, 0.12);
  --border-subtle-strong: rgba(238, 241, 246, 0.2);
  --border-on-action: rgba(255, 255, 255, 0.35);
  --text-primary: #eef1f6;
  --text-secondary: #c3ccd9;
  --text-on-action: #0a1122;
  --action-bg: #e8c65a;
  --brand-accent: #d9ad34;
  --brand-accent-soft: #16264b;
  --focus-ring: #e8c65a;
  --danger-fg: #ff9c9c;
  --placeholder-fg: #ff9c9c;
  --scrim-modal: rgba(10, 17, 34, 0.6);
  --shadow-ambient: rgba(0, 0, 0, 0.5);
  --shadow-soft: rgba(0, 0, 0, 0.4);
  --shadow-player: rgba(0, 0, 0, 0.45);
  --badge-louvor-bg: #3f2b09;
  --badge-louvor-fg: #f3dc8e;
  --badge-adoracao-bg: #3a2230;
  --badge-adoracao-fg: #f0b3c8;
  --badge-pregacao-bg: #1f2f22;
  --badge-pregacao-fg: #a8d3ab;
  --badge-harpa-bg: #251a3d;
  --badge-harpa-fg: #bfa9e4;
  --badge-geral-bg: #232c38;
  --badge-geral-fg: #c3ccd9;
  --share-whatsapp-bg: #2ea36f;
  --share-whatsapp-fg: #06130c;
  --og-bg-from: #101a33;
  --og-bg-to: #0a1122;
  --og-ink: #ffffff;
  --og-ink-muted: #8aa8db;
  --og-accent: #e8c65a;
  --chart-ink: #eef1f6;
  --chart-muted: #c3ccd9;
  --chart-grid: #16264b;
  --chart-tip-bg: #eef1f6;
  --chart-tip-ink: #0a1122;
  --chart-series-1: #8aa8db;
  --chart-step-1: #eef3fb;
  --chart-step-2: #d8e4f6;
  --chart-step-3: #8aa8db;
  --chart-step-4: #3a5fa8;
  --chart-step-5: #1e3568;
  --typography-family-body: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  --typography-family-display: Georgia, "Noto Serif", "Times New Roman", serif;
  --typography-root-percent: 112.5%;
  --typography-leading-body: 1.6;
  --typography-leading-tight: 1.25;
  --typography-size-micro: 0.8rem;
  --typography-size-meta: 0.9rem;
  --typography-size-body: 1rem;
  --typography-size-title: 1.2rem;
  --typography-size-section: 1.44rem;
  --typography-size-page: 1.728rem;
  --typography-tracking-caps: 0.02em;
  --shape-radius-lg: 12px;
  --shape-radius-md: 8px;
  --shape-radius-sm: 4px;
  --shape-layout-content-max: 62rem;
  --shape-control-min-target: 3rem;
}

/* ...and by the system preference, unless the user forced light. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg-canvas: #0a1122;
    --bg-texture-from: #101a33;
    --bg-texture-fade: rgba(16, 26, 51, 0);
    --surface-raised: #101a33;
    --surface-sunken: #16264b;
    --border-default: #1e3568;
    --border-subtle: rgba(238, 241, 246, 0.12);
    --border-subtle-strong: rgba(238, 241, 246, 0.2);
    --border-on-action: rgba(255, 255, 255, 0.35);
    --text-primary: #eef1f6;
    --text-secondary: #c3ccd9;
    --text-on-action: #0a1122;
    --action-bg: #e8c65a;
    --brand-accent: #d9ad34;
    --brand-accent-soft: #16264b;
    --focus-ring: #e8c65a;
    --danger-fg: #ff9c9c;
    --placeholder-fg: #ff9c9c;
    --scrim-modal: rgba(10, 17, 34, 0.6);
    --shadow-ambient: rgba(0, 0, 0, 0.5);
    --shadow-soft: rgba(0, 0, 0, 0.4);
    --shadow-player: rgba(0, 0, 0, 0.45);
    --badge-louvor-bg: #3f2b09;
    --badge-louvor-fg: #f3dc8e;
    --badge-adoracao-bg: #3a2230;
    --badge-adoracao-fg: #f0b3c8;
    --badge-pregacao-bg: #1f2f22;
    --badge-pregacao-fg: #a8d3ab;
    --badge-harpa-bg: #251a3d;
    --badge-harpa-fg: #bfa9e4;
    --badge-geral-bg: #232c38;
    --badge-geral-fg: #c3ccd9;
    --share-whatsapp-bg: #2ea36f;
    --share-whatsapp-fg: #06130c;
    --og-bg-from: #101a33;
    --og-bg-to: #0a1122;
    --og-ink: #ffffff;
    --og-ink-muted: #8aa8db;
    --og-accent: #e8c65a;
    --chart-ink: #eef1f6;
    --chart-muted: #c3ccd9;
    --chart-grid: #16264b;
    --chart-tip-bg: #eef1f6;
    --chart-tip-ink: #0a1122;
    --chart-series-1: #8aa8db;
    --chart-step-1: #eef3fb;
    --chart-step-2: #d8e4f6;
    --chart-step-3: #8aa8db;
    --chart-step-4: #3a5fa8;
    --chart-step-5: #1e3568;
    --typography-family-body: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    --typography-family-display: Georgia, "Noto Serif", "Times New Roman", serif;
    --typography-root-percent: 112.5%;
    --typography-leading-body: 1.6;
    --typography-leading-tight: 1.25;
    --typography-size-micro: 0.8rem;
    --typography-size-meta: 0.9rem;
    --typography-size-body: 1rem;
    --typography-size-title: 1.2rem;
    --typography-size-section: 1.44rem;
    --typography-size-page: 1.728rem;
    --typography-tracking-caps: 0.02em;
    --shape-radius-lg: 12px;
    --shape-radius-md: 8px;
    --shape-radius-sm: 4px;
    --shape-layout-content-max: 62rem;
    --shape-control-min-target: 3rem;
  }
}

/* ==========================================================================
   OuvirGospel — "Firmamento" design system
   Deep indigo (transcendência) + gold (glória, ACCENT ONLY). The brand INVERTS
   by mode: blue commands on light, gold shines in dark — which is what makes
   the gold story accessible instead of decorative (see §7.3 of
   docs/engineering/design-config-driven.md, and the arithmetic in
   config/design-tokens.json).
   Warm, reverent, senior-friendly. Mobile-first, no frameworks, no CDN.
   Light + dark via CSS custom properties; WCAG AA contrast throughout, proven
   by tests_py/test_tokens.py TestPaletteContrast rather than by assertion.
   ZERO colour literals live here — every colour is a var() emitted from
   config/design-tokens.json. That is not a convention, it is what makes a
   hardcode unrepresentable.
   ========================================================================== */

/* ---- Elevation geometry ----
   Layout, not brand: the OFFSETS live here, the COLOUR is a token
   (--shadow-*, generated from config/design-tokens.json). Zero colour
   literals, and still one place to tune depth.

   THE RULE (stated in full in config/design-tokens.json $meta.elevation_rule,
   enforced by tests_py/test_tokens.py TestDesignSystemRules):

     Elevation is a CLAIM that something floats above the page. Exactly three
     things are entitled to make it, and each gets one geometry:
       --elevation-overlay  a surface that COVERS content (sign-in sheet,
                            consent banner)
       --elevation-player   the sticky bar — the only shadow pointing UP
       --elevation-control  the primary action control (the play button)

     Everything else — every card, tile, hero and panel — is IN the page and is
     separated by --border-default ALONE. Borders only started responding to
     dark mode when --line became a real token (§0), which is what finally made
     that affordable. If you want a fourth elevation, you want a border.

   Was: three near-identical ramps at 0.35/0.25 alpha applied to everything,
   including a 30px blur under every card. That is the "floating card" template
   look §6 item 2 is about — the shadows were doing decoration, not signalling
   depth, so nothing on the page had a reliable meaning for "above". */
:root {
  --elevation-overlay: 0 6px 20px -8px var(--shadow-ambient);
  --elevation-player: 0 -6px 18px -10px var(--shadow-player);
  --elevation-control: 0 2px 6px -2px var(--shadow-soft);
}

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

/* Author display rules (e.g. .card { display:grid }) would otherwise defeat
   the UA stylesheet's [hidden] rule — keep the hidden attribute authoritative. */
[hidden] { display: none !important; }

html {
  /* 18px base: the 45+ audience reads comfortably without zooming */
  font-size: var(--typography-root-percent);
  -webkit-text-size-adjust: 100%;
}

/* Form controls do NOT inherit type. Every UA hands <button>/<input> its own
   family and its own size — 13.3333px in Chrome — which ignores the 112.5%
   root ENTIRELY: the one element that refuses the "18px so a 45+ reader never
   zooms" decision is the element you tap.

   Two real consequences, both found by rendering the page rather than by
   reading it (tests/visual-hierarchy.spec.js): the search field was set in a
   different TYPEFACE from every word around it, and nine controls on the home
   page rendered a size that is on no scale. Neither is visible to the Python
   lint, which reads styles.css and can only see sizes the sheet states.

   line-height is deliberately left alone: controls are flex boxes with a
   min-height, and inheriting 1.6 would grow every one of them. */
button, input, select, textarea {
  font-family: inherit;
  font-size: inherit;
}

body {
  margin: 0;
  font-family: var(--typography-family-body);
  background-color: var(--bg-canvas);
  /* THE HORIZON (§6 item 5). The canvas lifts toward the top of the page like
     a dawn sky — "os céus proclamam" — and that is the only atmospheric
     gesture in the system.

     The origin is an absolute -60px, NOT the `-10%` it used to be. A
     percentage there resolves against the BODY's height, so the wash was
     painting at 64% on /contato/ (1266px tall) and at 0% — nothing at all — on
     the home page (3974px tall): the same declaration produced opposite
     results depending on how many stations are in the catalogue. It looked
     deliberate and was a side effect. Now every page gets the same sky, fading
     out ~230px down, and the geometry lives here while the COLOUR stays a
     token, like every other rule in this file. */
  background-image: radial-gradient(1200px 420px at 50% -60px,
    var(--bg-texture-from) 0%, var(--bg-texture-fade) 70%);
  background-repeat: no-repeat;
  color: var(--text-primary);
  line-height: var(--typography-leading-body);
  /* Room for the sticky player bar */
  padding-bottom: 7.5rem;
}

/* The editorial voice: headings and the wordmark only.
   .btn/.chip were in this list and are NOT any more — a button is UI, not
   editorial copy, and it should speak in the same face as the body text it
   sits next to. (It also mattered once display became a serif: serif buttons
   read as a 2009 church website.) */
h1, h2, h3, .brand-word {
  font-family: var(--typography-family-display);
}

/* ---- The type scale ----
   Stated in full in config/design-tokens.json $meta.type_scale_rule, enforced
   by tests_py/test_tokens.py test_font_size_uses_only_the_scale (no allowlist).

     page    the one h1; the admin KPI number
     section an h2; the wordmark; the h1 of a phone-width screen
     title   an h3; the NAME of a thing in a list; a lead; the text you type into
     body    prose, buttons, chips — the default and the floor for reading
     meta    text ABOUT the thing: status, breadcrumb, footer, notes, consent
     micro   labels that are never sentences: badges, chart axes, hints

   Was: h1/h2/h3 tokens and then 20 ad-hoc literals below them, in 45
   declarations. Naming the steps after ELEMENTS was the bug — a station name
   in a card had no step to reach for that was not called "h3", so it invented
   1.02rem, and nineteen other places did the same. */
h1 {
  font-size: var(--typography-size-page);
  line-height: var(--typography-leading-tight);
  margin: 0 0 0.5rem;
}
h2 { font-size: var(--typography-size-section); margin: 2rem 0 0.75rem; }
h3 { font-size: var(--typography-size-title); margin: 0; }
p  { margin: 0.5rem 0 1rem; }
a  { color: var(--brand-accent); text-underline-offset: 3px; }
a:hover { color: var(--action-bg); }
img { max-width: 100%; }

:focus-visible {
  outline: 3px solid var(--focus-ring);
  outline-offset: 3px;
  border-radius: var(--shape-radius-sm);
}

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

.wrap { max-width: var(--shape-layout-content-max); margin: 0 auto; padding: 0 1rem; }

/* ---- Header ---- */
.site-header {
  display: flex; align-items: center; justify-content: space-between;
  gap: 0.75rem; padding: 0.9rem 0;
}
.brand {
  display: inline-flex; align-items: center; gap: 0.6rem;
  text-decoration: none; color: var(--text-primary);
}
.brand svg { width: 2.6rem; height: 2.6rem; flex: none; }
.brand-word { font-size: var(--typography-size-section); font-weight: 700; }
.brand-word b { color: var(--brand-accent); font-weight: 700; }

.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: var(--shape-control-min-target);
  min-height: var(--shape-control-min-target);
  padding: 0.4rem;
  border: 2px solid var(--border-default); border-radius: 999px;
  background: var(--surface-raised); color: var(--text-primary);
  cursor: pointer; font-size: var(--typography-size-title);
}
.icon-btn:hover { border-color: var(--brand-accent); }
/* Inline SVG icons inherit the button's colour token and are sized here — the
   emoji they replaced could do neither (§6 item 3). */
.icon-btn svg { width: 1.35rem; height: 1.35rem; display: block; }

/* Theme toggle shows the icon of the mode it switches TO */
#theme-toggle .icon-moon { display: flex; }
#theme-toggle .icon-sun  { display: none; }
#theme-toggle span { align-items: center; }
:root[data-theme="dark"] #theme-toggle .icon-moon { display: none; }
:root[data-theme="dark"] #theme-toggle .icon-sun  { display: flex; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) #theme-toggle .icon-moon { display: none; }
  :root:not([data-theme="light"]) #theme-toggle .icon-sun  { display: flex; }
}

/* ---- Hero / intro ---- */
.hero { padding: 1.2rem 0 0.4rem; }
.hero p.lead { font-size: var(--typography-size-title); color: var(--text-secondary); max-width: 42rem; margin-top: 0; }

/* The "site limpo" pledge — a REASSURANCE, not the headline, and it finally
   reads like one.

   Was: a bold pale-gold PILL sitting ABOVE the h1, wrapping to three lines at
   390px. It was the loudest thing above the fold and the first thing the eye
   landed on — for a sentence whose whole job is to be believed AFTER you know
   what the site is. Three things were wrong, and each has a rule behind it
   rather than a preference:

     1. it came first  -> it now follows the h1 and the lead, which is the
                          order the reader actually needs: what is this, why
                          should I stay, ...and by the way it is clean.
     2. it was bold    -> weight is how this page says "this is the thing".
                          A reassurance is not the thing.
     3. it was a pill  -> $meta.radius_rule: a pill means "you TAP this".
                          This is not tappable, and a pill that wraps to three
                          lines is a rounded rectangle in denial. It is a
                          surface sitting on the canvas, so it takes lg.

   What deliberately did NOT change: the WORDING (carefully made honest — it
   claims only what the SITE controls, because the radios' own audio is
   theirs), and the pale-gold surface, which is WHERE GOLD LIVES IN LIGHT MODE
   (§7.3 and the $note on brand.accent-soft). Quieting this by dropping the
   gold would have quietly deleted half the brand from the theme most users
   never leave — the accent has exactly two homes on a light page and this is
   one of them. */
.clean-pledge {
  display: inline-flex; align-items: flex-start; gap: 0.5rem;
  background: var(--brand-accent-soft); color: var(--brand-accent);
  font-size: var(--typography-size-meta);
  padding: 0.5rem 0.9rem; border-radius: var(--shape-radius-lg);
  margin: 0.25rem 0 0; max-width: 42rem;
}
/* The mark sits ON the first line. align-items:center left it floating in the
   vertical middle of a three-line block, pointing at nothing. */
.clean-pledge svg { width: 1rem; height: 1rem; flex: none; margin-top: 0.2rem; }

/* ---- Search + chips ---- */
.controls { margin: 1rem 0 1.25rem; display: grid; gap: 0.9rem; }
.search-box { position: relative; }
.search-box input {
  width: 100%; font-size: var(--typography-size-title); color: var(--text-primary);
  padding: 0.95rem 1.1rem 0.95rem 3rem;
  border: 2px solid var(--border-default); border-radius: 999px;
  background: var(--surface-raised);
}
.search-box input::placeholder { color: var(--text-secondary); opacity: 1; }
.search-box svg {
  position: absolute; left: 1rem; top: 50%; transform: translateY(-50%);
  width: 1.25rem; height: 1.25rem; color: var(--text-secondary); pointer-events: none;
}

.chips { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.chip {
  font-size: var(--typography-size-body); font-weight: 600; cursor: pointer;
  padding: 0.55rem 1.1rem; min-height: var(--shape-control-min-target);
  border: 2px solid var(--border-default); border-radius: 999px;
  background: var(--surface-raised); color: var(--text-primary);
}
.chip[aria-pressed="true"] {
  background: var(--action-bg); border-color: var(--action-bg);
  color: var(--text-on-action);
}

/* DOIS eixos de filtro na MESMA trilha (tradição · país), 2026-07-21. Uma
   trilha própria para a tradição custaria 57px medidos e derrubaria o
   invariante de duas rádios na dobra do telefone — o rail já rola na
   horizontal, então mais dois chips custam 0px de altura.
   Os grupos são flex CONTAINERS (não display:contents, que ainda apaga o
   role="group" para leitores de tela em navegadores de suporte irregular):
   cada grupo é um item do rail, e os chips fluem dentro dele. */
.chip-group { display: flex; gap: 0.5rem; }
.chip-sep {
  align-self: center; flex: none;
  width: 2px; height: 1.4rem; border-radius: 999px;
  background: var(--border-default);
}

/* ---- The logo tile ----
   ONE tile, three sizes. Was three near-identical seven-line blocks
   (.card .logo, .station-hero .logo, .player-bar .logo) plus three identical
   `.logo img` rules — differing only in how big the tile was, and each
   hand-computing its own initial size.

   The container declares how big its tile is (--logo-size) and everything
   else follows from that, including the card's grid column, which used to
   repeat `3.4rem` in a second place.

   THE INITIAL IS 40% OF ITS TILE. That ratio is not an invention: 6x0.4=2.4,
   3x0.4=1.2 and 3.4x0.4~=1.4 are exactly the three literals it replaces — the
   ratio was always the intent and those numbers were its hand-computed
   instances. It is a GLYPH FITTED TO A BOX, so it is deliberately NOT on the
   type scale: the scale governs text that is READ (see
   $meta.type_scale_rule, and §1.6's "spacing: no, deliberately" — this is the
   same argument). It is not dead code either: 17 of the 60 stations ship no
   favicon, and any favicon that 404s falls back to it. */
.logo {
  width: var(--logo-size); height: var(--logo-size);
  border-radius: var(--shape-radius-md);
  background: var(--surface-sunken); color: var(--brand-accent);
  display: flex; align-items: center; justify-content: center;
  font-weight: 700; font-size: calc(var(--logo-size) * 0.4);
  overflow: hidden; flex: none;
}
.logo img { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; }

/* ---- Station grid & cards ---- */
.grid {
  display: grid; gap: 0.9rem;
  grid-template-columns: repeat(auto-fill, minmax(17rem, 1fr));
  list-style: none; padding: 0; margin: 0.75rem 0 0;
}

.card {
  --logo-size: 3.4rem;
  display: grid;
  grid-template-columns: var(--logo-size) 1fr auto;
  gap: 0.8rem; align-items: center;
  background: var(--surface-raised);
  border: 1px solid var(--border-default);
  border-radius: var(--shape-radius-lg);
  padding: 0.9rem;
}
.card:hover { border-color: var(--brand-accent); }

.card .meta { min-width: 0; }
.card .meta .name {
  font-weight: 700; font-size: var(--typography-size-title); line-height: 1.3;
  /* No MÁXIMO 2 linhas em QUALQUER largura — um nome de 90 chars ("LA VOZ 94.7
     (Laguna) - 94.7 FM - XHTJ-FM - GPS Media...") esticava o card a 293px. É
     este seletor (não .card .name) que precisa do clamp: o display:block dele
     anulava o -webkit-box. Correção de raiz, vale em todo viewport. */
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden; color: inherit; text-decoration: none;
  overflow-wrap: anywhere;
}
.card .meta .name:hover { text-decoration: underline; color: var(--brand-accent); }
.card .meta .sub { display: flex; flex-wrap: wrap; gap: 0.4rem; align-items: center; margin-top: 0.3rem; }
.card .state { color: var(--text-secondary); font-size: var(--typography-size-meta); }

.badge {
  font-size: var(--typography-size-micro); font-weight: 700;
  letter-spacing: var(--typography-tracking-caps);
  padding: 0.15rem 0.6rem; border-radius: 999px; white-space: nowrap;
}
.badge-louvor   { background: var(--badge-louvor-bg);   color: var(--badge-louvor-fg); }
.badge-adoracao { background: var(--badge-adoracao-bg); color: var(--badge-adoracao-fg); }
.badge-pregacao { background: var(--badge-pregacao-bg); color: var(--badge-pregacao-fg); }
.badge-harpa    { background: var(--badge-harpa-bg);    color: var(--badge-harpa-fg); }
.badge-geral    { background: var(--badge-geral-bg);    color: var(--badge-geral-fg); }
.badge-off      { background: var(--danger-fg); color: var(--bg-canvas); }

/* TRADIÇÃO: contornado, não preenchido. Ele fica ao LADO do badge de estilo, e
   um sexto pill preenchido leria como um sexto estilo — a distinção que a
   feature existe para fazer (o que TOCA vs. de QUEM É) morreria na cor. A
   forma carrega a diferença, e não inventa um hue que ainda teria de brigar
   com gold/rose/sage/violet/neutral. Reusa tokens já submetidos ao piso AA
   (--text-secondary sobre superfície), em vez de criar um par novo. */
.badge-tradicao {
  background: transparent; color: var(--text-secondary);
  border: 1px solid var(--border-default);
  padding: 0.1rem 0.55rem;  /* compensa a borda: mesma altura do pill cheio */
}

.card .actions { display: flex; flex-direction: column; gap: 0.4rem; align-items: center; }

.play-btn {
  width: 3.5rem; height: 3.5rem; border-radius: 999px; border: none;
  background: var(--action-bg); color: var(--text-on-action);
  display: inline-flex; align-items: center; justify-content: center;
  cursor: pointer; box-shadow: var(--elevation-control);
  transition: transform 120ms ease;
}
.play-btn svg { width: 1.4rem; height: 1.4rem; }
.play-btn:hover { transform: scale(1.06); }
.play-btn:disabled { background: var(--surface-sunken); color: var(--text-secondary); cursor: not-allowed; transform: none; }
.play-btn[aria-pressed="true"] { outline: 3px solid var(--brand-accent); outline-offset: 2px; }

/* Dead-set state: the station proved offline this session — the button is
   muted, loses the play icon and reads "Fora do ar" (no play option). */
.play-btn.is-off { background: var(--surface-sunken); color: var(--text-secondary); box-shadow: none; }
.play-btn.is-off svg { display: none; }
.play-btn.is-off::after {
  content: "Fora\a do ar"; white-space: pre; text-align: center;
  /* Was 0.62rem = 11.2px — the smallest text on the site, on a 45+ radio, for
     the one label that tells you WHY the button does nothing. It was sized to
     fit the circle rather than to be read. At micro it is 14.4px and still
     fits (the widest line, "DO AR", is ~45px inside a 63px button). */
  font-size: var(--typography-size-micro); font-weight: 700; line-height: 1.15;
  letter-spacing: var(--typography-tracking-caps); text-transform: uppercase;
}
.station-hero .play-btn.is-off::after { font-size: var(--typography-size-meta); }

/* The favourite toggle. aria-pressed is the ONLY state: it is both the
   semantics a screen reader reads and the switch that picks which of the two
   hearts renders. They are the same silhouette (build.py _HEART_PATH), outlined
   or filled — so "favourited" is carried by fill, and the fill can finally take
   the action colour, which an emoji ♥ never could. */
.fav-btn {
  display: inline-flex; align-items: center; justify-content: center;
  border: none; background: none; cursor: pointer;
  line-height: 1; color: var(--text-secondary);
  min-width: 2.6rem; min-height: 2.6rem; border-radius: 999px;
}
.fav-btn svg { width: 1.5rem; height: 1.5rem; display: block; }
.fav-btn .ic-fav-on { display: none; }
.fav-btn[aria-pressed="true"] .ic-fav-on { display: block; }
.fav-btn[aria-pressed="true"] .ic-fav-off { display: none; }
.fav-btn[aria-pressed="true"] { color: var(--action-bg); }
.fav-btn:hover { background: var(--surface-sunken); }

/* Share-to-WhatsApp, beside the heart on every card. Same silhouette and tap
   target as .fav-btn (a pill, >=44px), but an <a> not a <button>: sharing needs
   no account while favouriting does — this is the growth channel (recommendation,
   not ads), so it must reach the 100% of visitors who are logged out. It wears
   --share-whatsapp-* (WhatsApp's own brand, exempt from harmonisation per the
   $note in config): recognisability IS the affordance. */
.wa-btn {
  display: inline-flex; align-items: center; justify-content: center;
  text-decoration: none; cursor: pointer; line-height: 1;
  min-width: 2.6rem; min-height: 2.6rem; border-radius: 999px;
  color: var(--share-whatsapp-bg);
}
.wa-btn svg { width: 1.5rem; height: 1.5rem; display: block; }
.wa-btn:hover { background: var(--surface-sunken); }
.wa-btn:focus-visible { outline: 3px solid var(--focus-ring); outline-offset: 2px; }

/* ---- Favorites section ---- */
.fav-section h2 { display: flex; align-items: center; gap: 0.5rem; }
.fav-section h2 svg { width: 1.05em; height: 1.05em; flex: none; color: var(--brand-accent); }
.fav-tools { display: flex; flex-wrap: wrap; gap: 0.6rem; margin: 0.5rem 0 0.25rem; }
.fav-empty { color: var(--text-secondary); background: var(--surface-sunken); padding: 0.9rem 1.1rem; border-radius: var(--shape-radius-md); }
.fav-note { font-size: var(--typography-size-meta); color: var(--text-secondary); margin-top: 0.4rem; }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem;
  min-height: var(--shape-control-min-target); padding: 0.6rem 1.25rem;
  font-size: var(--typography-size-body); font-weight: 700; text-decoration: none;
  border-radius: 999px; border: 2px solid var(--action-bg);
  background: var(--action-bg); color: var(--text-on-action); cursor: pointer;
}
.btn:hover { filter: brightness(1.08); color: var(--text-on-action); }
.btn-outline { background: transparent; color: var(--brand-accent); }
.btn-outline:hover { color: var(--action-bg); }
.btn svg { width: 1.2rem; height: 1.2rem; }

.result-count { color: var(--text-secondary); font-size: var(--typography-size-meta); margin-top: 0.75rem; }

/* ---- Station page ---- */
.breadcrumb { font-size: var(--typography-size-meta); color: var(--text-secondary); padding-top: 0.75rem; }
.breadcrumb ol { list-style: none; display: flex; flex-wrap: wrap; gap: 0.35rem; padding: 0; margin: 0; }
.breadcrumb li + li::before { content: "›"; margin-right: 0.35rem; color: var(--text-secondary); }

.station-hero {
  --logo-size: 6rem;
  background: var(--surface-raised); border: 1px solid var(--border-default);
  border-radius: var(--shape-radius-lg);
  padding: 1.5rem; margin-top: 1rem;
  display: grid; gap: 1rem; justify-items: center; text-align: center;
}
.station-hero .play-btn { width: 5rem; height: 5rem; }
.station-hero .play-btn svg { width: 2rem; height: 2rem; }
.station-facts { display: flex; flex-wrap: wrap; gap: 0.5rem; justify-content: center; }
.fact {
  background: var(--surface-sunken); color: var(--text-secondary);
  font-size: var(--typography-size-meta); font-weight: 600;
  padding: 0.25rem 0.75rem; border-radius: 999px;
}
/* WhatsApp's own brand green — a token so the sheet stays literal-free,
   but see the $note in config: a palette swap must NOT re-tint it. */
.wa-share {
  background: var(--share-whatsapp-bg);
  border-color: var(--share-whatsapp-bg);
  color: var(--share-whatsapp-fg);
}
.wa-share:hover { color: var(--share-whatsapp-fg); }

.similar h2 { margin-top: 2.25rem; }

/* Offline station page: clear "fora do ar" panel replaces the play button */
.offline-panel {
  max-width: 34rem; margin: 0 auto; text-align: center;
  background: var(--surface-sunken); border-left: 4px solid var(--danger-fg);
  padding: 0.9rem 1.1rem; border-radius: var(--shape-radius-md);
}
.offline-panel .offline-title { margin: 0 0 0.4rem; font-weight: 700; color: var(--danger-fg); }
.offline-panel p { margin: 0; }

/* ---- Sticky player bar ---- */
.player-bar {
  --logo-size: 3rem;
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 50;
  background: var(--surface-raised);
  border-top: 2px solid var(--border-default);
  box-shadow: var(--elevation-player);
  padding: 0.6rem 0 calc(0.6rem + env(safe-area-inset-bottom));
}
.player-bar .wrap {
  display: grid; grid-template-columns: auto 1fr auto; gap: 0.75rem; align-items: center;
}
.player-bar .info { min-width: 0; }
.player-bar .info .name {
  font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.player-bar .status { font-size: var(--typography-size-meta); color: var(--text-secondary); }
.player-bar .status.is-error { color: var(--danger-fg); font-weight: 600; }
.player-controls { display: flex; align-items: center; gap: 0.6rem; }

/* The stopwatch is static markup; only .timer-count changes on each tick, so
   the icon survives (writing the whole label with textContent would delete it). */
.timer-btn { font-size: var(--typography-size-meta); font-weight: 700; width: auto; padding: 0 0.7rem; gap: 0.3rem; }
.timer-btn svg { width: 1.15rem; height: 1.15rem; }
.timer-count:empty { display: none; }
.timer-btn.is-on { border-color: var(--action-bg); color: var(--brand-accent); }

/* One-line sleep-timer explanation, shown while the user cycles options */
.timer-hint {
  max-width: var(--shape-layout-content-max); margin: 0 auto; padding: 0.5rem 1rem 0;
  font-size: var(--typography-size-meta); color: var(--text-secondary);
}

.volume { display: none; align-items: center; gap: 0.4rem; }
.volume input[type="range"] { width: 7rem; accent-color: var(--action-bg); }
@media (min-width: 40rem) { .volume { display: inline-flex; } }

/* ---- Prose pages (sobre, contato, ...) ----
   margin-inline auto: the narrow reading column sits CENTERED inside the
   wrap on desktop — never packed left with dead space on the right only
   (binding responsive fix 2026-07-14, seen live on /contato/ @1600px). */
.prose { max-width: 44rem; padding-top: 1rem; margin-inline: auto; }
.prose h1 { margin-top: 1rem; }
.prose ul { padding-left: 1.4rem; }
.prose .note {
  background: var(--surface-sunken); border-left: 4px solid var(--action-bg);
  padding: 0.9rem 1.1rem; border-radius: var(--shape-radius-md); margin: 1.25rem 0;
}

/* ---- Footer ---- */
.site-footer {
  margin-top: 3rem; padding: 1.5rem 0 1rem;
  border-top: 1px solid var(--border-default);
  color: var(--text-secondary); font-size: var(--typography-size-meta);
}
.site-footer nav { display: flex; flex-wrap: wrap; gap: 1rem; margin-bottom: 0.75rem; }
.site-footer a { color: var(--text-secondary); }
.site-footer a:hover { color: var(--brand-accent); }

/* ---- Header actions (sign-in + theme) ---- */
.header-actions { display: flex; align-items: center; gap: 0.6rem; min-width: 0; }
.auth-entrar { white-space: nowrap; }
.auth-user { display: flex; align-items: center; gap: 0.5rem; min-width: 0; }
.auth-avatar {
  width: 2.4rem; height: 2.4rem; border-radius: 999px; object-fit: cover;
  border: 2px solid var(--border-default); flex: none;
}
.auth-name {
  font-weight: 700; max-width: 7rem;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* ---- Sign-in modal (bottom sheet on mobile, centered on desktop) ---- */
.auth-modal {
  position: fixed; inset: 0; z-index: 80;
  background: var(--scrim-modal);
  display: flex; flex-direction: column; justify-content: flex-end;
}
.auth-sheet {
  background: var(--surface-raised); color: var(--text-primary);
  border-radius: var(--shape-radius-lg) var(--shape-radius-lg) 0 0;
  box-shadow: var(--elevation-overlay);
  padding: 1.5rem 1.25rem calc(1.5rem + env(safe-area-inset-bottom));
  width: 100%; max-width: 30rem; margin: 0 auto;
  display: grid; gap: 0.8rem;
}
.auth-sheet h2 { margin: 0; }
.auth-benefit { margin: 0; color: var(--text-secondary); }
.auth-consent {
  margin: 0; font-size: var(--typography-size-meta); color: var(--text-secondary);
  background: var(--surface-sunken); border-radius: var(--shape-radius-md);
  padding: 0.7rem 0.9rem;
}
/* Falha REAL de sign-in (rede, pop-up bloqueado, provedor indisponível) tinha
   um catch vazio — o clique não fazia nada visível e o usuário não tinha como
   saber se era ele, o navegador ou o site. Cancelamento intencional (fechar o
   pop-up) continua em silêncio: só falha que o usuário NÃO escolheu aparece
   aqui. Ver mensagemErro() em auth.js. */
.auth-error {
  margin: 0; font-size: var(--typography-size-meta); color: var(--danger-fg);
  background: var(--surface-sunken); border-radius: var(--shape-radius-md);
  padding: 0.7rem 0.9rem; border-left: 3px solid var(--danger-fg);
}

/* ---- The two providers ----
   Was: #auth-google and #auth-apple were both plain `.btn` — two identical
   fills in the action colour, stacked, competing for the same decision. Two
   primaries is no primary: the eye is forced to choose, and there is nothing
   to choose BETWEEN, because the providers are equal in function and neither
   is recommended over the other.

   The hierarchy that is honest here is NOT Google-over-Apple — inventing that
   would be a recommendation the product does not make. It is DECISION over
   DOORS. The decision ("entrar no OuvirGospel") is the sheet's title; the two
   providers are the doors into it, and a door is not a call to action. So
   they stop being primaries and become a matched pair: same surface, same
   border, same weight — deliberately indistinguishable from EACH OTHER, and
   clearly distinct from the title above and the way out below.

   The sheet now reads top to bottom as one thing:
     h2 (section)          what this is
     .auth-benefit (body)  why you would
     .auth-consent (meta)  the mandatory LGPD notice, ABOVE the buttons —
                           tests/auth.spec.js pins that by geometry, not by
                           source order, so this must stay true visually
     the two doors         equal to each other
     cancel                a way out, not a third option with equal standing

   They are `.btn.btn-outline` in the markup — the system's EXISTING secondary
   treatment (the header's "Entrar" and the banner's "Só essenciais" already
   wear it), so this adds no vocabulary. That is also why they are outlined
   rather than neutral-filled: the sheet is --surface-raised, so a raised
   button on it would have been #ffffff on #ffffff, separated only by
   --border-default at 1.29:1 — an edge you cannot see. On a raised surface the
   only honest way to draw a control is its border, and the border has to be a
   colour that reads. Nothing is FILLED here, which is the point: a fill is a
   claim of primacy, and no provider gets to make it. */
.auth-provider { width: 100%; }
.auth-provider:disabled {
  background: var(--surface-sunken); border-color: var(--border-default);
  color: var(--text-secondary); cursor: not-allowed;
}
.auth-provider .soon {
  font-size: var(--typography-size-micro); font-weight: 700; margin-left: 0.5rem;
  background: var(--brand-accent-soft); color: var(--brand-accent);
  padding: 0.1rem 0.55rem; border-radius: 999px;
}
/* The way out, and the quietest control in the sheet. It WAS the `.btn-outline`
   and the providers were filled `.btn`s; the two got swapped. Now the providers
   ARE the outline and Cancelar is a ghost: no border, no fill, secondary text.
   Leaving must never out-shout the two doors it sits under, and an outline in
   the action colour (what btn-outline is) would have done exactly that. */
.auth-cancel {
  background: none; border-color: transparent;
  color: var(--text-secondary); font-weight: 600;
}
.auth-cancel:hover {
  background: var(--surface-sunken); color: var(--text-primary); filter: none;
}
@media (min-width: 40rem) {
  .auth-modal { justify-content: center; padding: 1rem; }
  .auth-sheet { border-radius: var(--shape-radius-lg); }
}

/* ---- Cookie/consent banner (senior-friendly, above the player) ---- */
.consent-banner {
  position: fixed; left: 0.75rem; right: 0.75rem;
  bottom: calc(0.75rem + env(safe-area-inset-bottom));
  z-index: 70; max-width: 34rem; margin: 0 auto;
  background: var(--surface-raised); border: 2px solid var(--border-default);
  border-radius: var(--shape-radius-lg); box-shadow: var(--elevation-overlay);
  /* 2026-07-21: o banner tinha 194px — o MAIOR bloco da tela do celular,
     maior que cabeçalho e h1 somados — e tapava a 2ª rádio de quem chega pela
     1ª vez. A folga sai do padding vertical; o horizontal e os alvos de toque
     ficam. Ver tests/dobra-mobile.spec.js. */
  padding: 0.6rem 1.1rem;
}
.consent-banner p { margin: 0 0 0.45rem; font-size: var(--typography-size-body); }
.consent-actions { display: flex; flex-wrap: wrap; gap: 0.6rem; }

/* ---- Player failure panel: message + suggestions + retry ---- */
#player-error {
  border-bottom: 1px solid var(--border-default);
  background: var(--surface-sunken);
  padding: 0.75rem 0;
}
/* The bar's .wrap is a 3-column grid — the panel needs plain flow. */
#player-error .wrap { display: block; }
.player-error-msg { margin: 0 0 0.6rem; font-weight: 700; color: var(--danger-fg); }
.player-suggestions { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.6rem; }
.suggestion-chip {
  display: inline-flex; align-items: center; gap: 0.4rem;
  font-size: var(--typography-size-body); font-weight: 600; cursor: pointer;
  padding: 0.5rem 1rem; min-height: var(--shape-control-min-target); max-width: 100%;
  border: 2px solid var(--border-default); border-radius: 999px;
  background: var(--surface-raised); color: var(--text-primary);
}
.suggestion-chip svg { width: 1rem; height: 1rem; flex: none; }
/* The name ellipsises, the play mark never does — which is why the chip is a
   flex row with the label in its own element instead of one text node. */
.suggestion-chip .suggestion-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.suggestion-chip:hover { border-color: var(--brand-accent); }

/* ---- Buffering spinner (bar status + active play button) ---- */
@keyframes og-spin { to { transform: rotate(1turn); } }
.play-btn { position: relative; }
.play-btn.is-buffering svg { opacity: 0.15; }
.play-btn.is-buffering::after {
  content: ""; position: absolute; width: 1.3rem; height: 1.3rem;
  border: 3px solid var(--border-on-action); border-top-color: currentColor;
  border-radius: 50%; animation: og-spin 0.8s linear infinite;
}
.player-bar.is-buffering .status::after {
  content: ""; display: inline-block; width: 0.75rem; height: 0.75rem;
  margin-left: 0.4rem; vertical-align: -0.1rem;
  border: 2px solid var(--border-default); border-top-color: var(--brand-accent);
  border-radius: 50%; animation: og-spin 0.8s linear infinite;
}

/* ---- Session "instável" badge ---- */
.badge-unstable {
  background: var(--surface-sunken); color: var(--danger-fg);
  border: 1px dashed var(--danger-fg);
}

/* ---- [VERMELHO] legal placeholders — same red in BOTH themes ---- */
.placeholder-legal {
  color: var(--placeholder-fg) !important;
  font-weight: 700;
}

/* ---- 404 search ---- */
.search-404 { display: flex; flex-wrap: wrap; gap: 0.6rem; margin: 1rem 0; }
.search-404 input {
  flex: 1 1 12rem; font-size: var(--typography-size-title); color: var(--text-primary);
  padding: 0.8rem 1.1rem; min-height: var(--shape-control-min-target);
  border: 2px solid var(--border-default); border-radius: 999px;
  background: var(--surface-raised);
}

/* ---- Small screens ----
   A responsive override STEPS DOWN the scale; it never invents a size.
   1.05rem (.brand-word) was exactly that invention — a value that exists
   nowhere else, chosen once, by eye; it steps down to `title` now.

   The h1 override is GONE, not ported. It was `1.45rem`, which on the scale is
   `section` — the SAME step as h2. 1.45 vs h2's 1.44 was 0.18px apart, so the
   collapse was always there; the ad-hoc value only hid it, and making the
   scale exact made it visible (the legal page's h1 and h2 rendered identically
   at 390px). The fix is not a seventh step wedged between section and page —
   it is that h1 is the largest thing on a screen on EVERY viewport, which is
   what `page` means.

   O h1 do telefone é `section`, NÃO `page` — e isso é on-scale: a própria escala
   diz "section = an h2; the wordmark; the h1 of a phone-width screen".

   Por que mudou (2026-07-17): aqui estava escrito "a long hero h1 wraps a line
   more on a phone; that is the correct trade for an audience that reads big".
   Isso era racionalização escrita SEM olhar a tela. No iPhone do Gabriel o h1 em
   `page` (31px) não custava "uma linha a mais": quebrava em 3 linhas, comia ~30%
   da altura útil e empurrava TODA rádio para fora da dobra — o print provou.
   Em `section` (26px) o mesmo texto cabe em 2 linhas e o 1º card sobe para dentro
   da tela. A hierarquia continua íntegra: h1(section) > lead(body). */
@media (max-width: 30rem) {
  .hero h1 { font-size: var(--typography-size-title); }
  .hero { padding: 0.6rem 0 0.2rem; }
  .hero p.lead { font-size: var(--typography-size-body); }
  h2 { margin: 1.1rem 0 0.5rem; }
  .brand-word { font-size: var(--typography-size-title); }
  .auth-entrar { padding: 0.45rem 0.85rem; min-height: 2.75rem; }

  /* COM SESSÃO, A IDENTIDADE OCUPA O LUGAR DA MARCA (2026-07-22).
     Medido em 390px com sessão ativa: a marca (ícone + palavra) leva 197,3px
     — mais da METADE da barra — e o que sobra não cobre avatar (43,2) +
     nome + "Sair" (82,7) + tema (~54). O nome é o único elemento elástico,
     então era ele que colapsava: mediu 0px, e no iPhone do Gabriel apareceu
     como "G…". Não era max-width de menos; eram cinco elementos onde cabem
     três.
     Some a PALAVRA, não a marca: o ícone continua ali e clicável, e o h1
     logo abaixo já diz "Rádio Gospel Ao Vivo — ...", então o site não perde
     identificação. Devolve os 139,7px medidos ao nome, e a conta fecha com
     folga: 57,6 + 43,2 + nome + 82,7 + 54 = 357 de 390.
     Deslogado a marca continua por extenso — quem ainda não entrou é quem
     mais precisa saber onde está. Guardado por tests/auth.spec.js. */
  .has-sessao .brand-word { display: none; }
  .has-sessao .auth-name { max-width: 9rem; }

  .auth-name { max-width: 4.5rem; }
  .header-actions { gap: 0.4rem; }
  /* Os chips continuam com alvo de toque >=44px (a11y); o que encolhe é a
     folga vertical entre as trilhas, não o alvo do dedo. */
  .filters { gap: 0.4rem; margin: 0.6rem 0; }
  .chip { padding: 0.4rem 0.9rem; }

  /* CARD COMPACTO NO TELEFONE — o requisito é ver "as rádios" (plural) sem
     rolar. Com 205px de altura só UMA cabia, e pela metade. Encolhe o logo, a
     folga e trava o nome em 1 linha; o alvo de toque do play/fav/share NÃO
     encolhe (min-target continua >=44px — a11y não é moeda de troca). */
  .card { --logo-size: 2.6rem; padding: 0.6rem; gap: 0.6rem; }
  .card .name { font-size: var(--typography-size-body); }
  #busca { margin: 0.5rem 0; }
  .grid-count { margin: 0.2rem 0 0.6rem; }

  /* AÇÕES EM LINHA NO TELEFONE — era a pilha vertical (play/WhatsApp/coração)
     que esticava o card para ~200px e deixava um buraco vazio no meio; no
     screenshot lido em 2026-07-17 cabiam só 3 rádios na tela inteira. Em linha,
     a altura do card passa a ser a do maior item (~44px + folga) e cabem ~2x
     mais rádios. Os alvos de toque NÃO encolhem (>=44px continua). */
  .card .actions { flex-direction: row; gap: 0.5rem; }

  /* NOME EM LINHA PRÓPRIA — dívida da correção acima. Pôr as ações em linha
     encurtou o card, mas alargou .actions para 175px e espremeu o `1fr` do
     nome para 87px. A palavra "Christmas" renderiza a 104px: mais larga que a
     coluna INTEIRA, então toda quebra caía no meio da palavra ("Christm/as
     Vin…"). Em 390px eram 142 dos 273 cards; em 320px, 270 de 273.

     Não é `overflow-wrap` — medido em 2026-07-21: `anywhere` e `break-word`
     dão resultado IDÊNTICO, porque nenhum modo de quebra salva uma palavra
     mais larga que o contêiner. E não é tipografia: encolher fonte ou botão
     está vedado ("Público 45+: fontes/botões grandes" + alvo tátil >=44px).
     A causa é distribuição de espaço — 25% do card para o conteúdo que o
     usuário lê, 49% para os controles.

     Dando ao nome a própria linha ele passa de 87px para ~297px e a quebra
     no meio da palavra some em ambas as larguras. Custo assumido e aprovado:
     ~+38px de altura por card, logo menos rádios por tela.
     Guardado por tests/card-name-wrap.spec.js. */
  .card {
    grid-template-areas:
      "logo nome nome"
      "logo sub  acoes";
  }
  .card .logo { grid-area: logo; }
  /* display:contents dissolve a caixa do .meta para que .name e .sub virem
     itens do grid do card — sem isso não dá para posicioná-los em áreas
     diferentes sem mexer no HTML gerado. */
  .card .meta { display: contents; }
  .card .meta .name { grid-area: nome; }
  .card .meta .sub { grid-area: sub; }
  .card .actions { grid-area: acoes; }

  /* Últimos 26px para o 1º card caber INTEIRO na tela real (724px úteis).
     Só folga vertical — nenhum texto sai, nenhum alvo de toque encolhe. */
  .hero { padding: 0.4rem 0 0.1rem; }

  /* Os 9px que faltavam (2026-07-22). O card de uma rádio CATÓLICA tem dois
     selos e por isso uma linha a mais: 165px contra 129px. Com uma delas no
     topo do ranking, 439 + 165 + 129 = 733px contra 724px úteis — a dobra
     perdia a segunda rádio por NOVE pixels.
     Antes de chegar aqui eu tentei duas reformas do card (mover a localização
     para junto do nome; alargar o .sub para duas colunas) e MEDI as duas:
     pioraram, 133/160/126px e 144px. A coluna do .sub tem 87px porque a de
     ações tem 175px, e ações não encolhem — são alvo de toque de um público
     45+. Então o que cede é a FOLGA entre busca e trilhas de chip, que não é
     alvo de nada: 0.9rem -> 0.5rem em duas lacunas devolve ~14px.
     Guardado por tests/tradicao.spec.js ("a dobra aguenta uma CATÓLICA em 1º"). */
  .controls { gap: 0.5rem; margin: 0.7rem 0 0.9rem; }
  /* Últimos 15px da dobra (2026-07-21): faltavam exatamente 14,7px para o 2º
     card caber inteiro nos 724px úteis. Sai de folga vertical — margem do h2 e
     gap entre cards — e não de conteúdo, fonte ou alvo de toque. */
  h2 { margin: 0.2rem 0 0.3rem; }
  .grid { gap: 0.55rem; margin-top: 0.2rem; }
  /* O cabeçalho tinha 86px para um logo e dois botões: a folga vertical é que
     era grande, não os alvos (que continuam >=44px — o conteúdo é que dita a
     altura). Vale em TODAS as páginas, não só na home. */
  .site-header { padding: 0.45rem 0; }
  .grid-count { margin: 0.1rem 0 0.4rem; }

  /* A frase de apoio some NO TELEFONE (continua no HTML pro Google) — o h1 já
     diz "Rádio Gospel Ao Vivo — Rádios Evangélicas e Católicas", então era
     redundância; cortá-la traz o 1º card inteiro para dentro da tela real.

     CONSEQUÊNCIA QUE NÃO É ÓBVIA (2026-07-22): esta regra vale em TODAS as
     páginas, então a prosa dos hubs (/estilo/, /estado/) não informa ninguém
     no telefone. Quem carrega a tradição e a origem para o público mobile é o
     CARD: o selo "Católica" e a bandeira do país. Por isso os dois são
     requisito, e não enfeite — mudar isto sem mexer no card devolve o site ao
     estado em que a promessa e o conteúdo não se encontravam.
     Pinado por tests/origem-copy.spec.js. */
  .hero p.lead { display: none; }
}
@media (max-width: 26rem) {
  .player-bar .logo { display: none; }
  .timer-btn { min-width: 2.6rem; }
}

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

/* ==========================================================================
   Admin dashboard (/admin/) — KPI tiles, charts, tables.
   The data-viz palette (--chart-*) is generated from the `chart` group in
   config/design-tokens.json; the validation record for the slot-1 hue and the
   ordinal ramp lives beside the values, in that file's $note.
   ========================================================================== */

.admin-state { padding: 2rem 0; }
.admin-state h2 { margin-bottom: 0.4rem; }
.admin-error-detail { color: var(--text-secondary); font-family: ui-monospace, monospace; font-size: var(--typography-size-meta); word-break: break-word; }
.admin-scope {
  color: var(--text-secondary);
  font-size: var(--typography-size-body);
  border-left: 3px solid var(--brand-accent);
  padding-left: 0.7rem;
  margin-bottom: 1.2rem;
}
.admin-caveats { margin-top: 2rem; }
.admin-caveats ul { margin: 0.4rem 0 0 1.1rem; }
.admin-caveats li { margin-bottom: 0.35rem; }

/* --- KPI tiles: the number IS the chart (no one-bar bar charts) --- */
.kpi-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
  gap: 0.7rem;
  margin-bottom: 1.6rem;
}
.kpi-tile {
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  border-radius: var(--shape-radius-md);
  padding: 0.8rem 0.9rem;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  min-width: 0;
}
.kpi-label { font-size: var(--typography-size-meta); color: var(--text-secondary); font-weight: 600; }
/* Proportional figures on a large standalone number (never tabular-nums). */
.kpi-value { font-size: var(--typography-size-page); font-weight: 700; line-height: 1.15; overflow-wrap: anywhere; }
.kpi-hint { font-size: var(--typography-size-micro); color: var(--text-secondary); }

/* --- chart cards --- */
.chart-card {
  background: var(--surface-raised);
  border: 1px solid var(--border-subtle);
  border-radius: var(--shape-radius-md);
  padding: 0.9rem;
  margin: 0 0 1rem;
  min-width: 0;
}
.chart-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 0.6rem;
  margin-bottom: 0.6rem;
}
.chart-titles h3 { font-size: var(--typography-size-title); margin: 0; }
.chart-sub { font-size: var(--typography-size-micro); color: var(--text-secondary); margin: 0.1rem 0 0; }
.chart-table-toggle {
  flex: none;
  background: none;
  border: 1px solid var(--border-subtle-strong);
  color: var(--text-secondary);
  border-radius: 999px;
  padding: 0.3rem 0.7rem;
  font-size: var(--typography-size-micro);
  min-height: 2rem;
  cursor: pointer;
}
.chart-table-toggle:hover { color: var(--text-primary); border-color: var(--brand-accent); }
/* Wide content scrolls inside its own box — the page body never scrolls x. */
/* Charts are drawn at the container's real width (viewBox == CSS pixels), so
   they never need to scroll and their labels are never pushed out of view. */
.chart-plot { overflow-x: auto; }
.chart-plot svg { width: 100%; display: block; }
.chart-empty { color: var(--text-secondary); font-size: var(--typography-size-meta); padding: 0.8rem 0; }

/* --- marks --- */
.chart-plot .bar { fill: var(--chart-series-1); }
.chart-plot .funnel-bar[data-step="1"] { fill: var(--chart-step-1); }
.chart-plot .funnel-bar[data-step="2"] { fill: var(--chart-step-2); }
.chart-plot .funnel-bar[data-step="3"] { fill: var(--chart-step-3); }
.chart-plot .funnel-bar[data-step="4"] { fill: var(--chart-step-4); }
.chart-plot .funnel-bar[data-step="5"] { fill: var(--chart-step-5); }
.chart-plot .line { stroke: var(--chart-series-1); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.chart-plot .dot { fill: var(--chart-series-1); stroke: var(--surface-raised); stroke-width: 2; }
.chart-plot .grid { stroke: var(--chart-grid); stroke-width: 1; }
.chart-plot .hit { fill: transparent; }
.chart-plot .bar:focus-visible, .chart-plot .funnel-bar:focus-visible, .chart-plot .hit:focus-visible {
  outline: 2px solid var(--brand-accent);
  outline-offset: 1px;
}
/* Text wears text tokens, never the series colour — and it wears the type
   scale too. These were 12px/12px/11px: three hand-picked pixel values that
   the SVG's viewBox happens to map 1:1, and an 11-vs-12 "distinction" that
   carried no information (nobody perceives 8%, and the two classes mean
   different things for reasons that have nothing to do with size).
   The px were also a DUAL SOURCE OF TRUTH: static/js/admin.js reserved label
   space with constants commented "~6.6px per char at 12px" — the stylesheet's
   font-size, retyped in JavaScript, where no test could see it disagree.
   The JS measures the resolved size now, so the chart follows the scale. */
.chart-plot .bar-name { fill: var(--chart-ink); font-size: var(--typography-size-micro); }
.chart-plot .bar-label { fill: var(--chart-ink); font-size: var(--typography-size-micro); font-weight: 700; }
.chart-plot .axis-label { fill: var(--chart-muted); font-size: var(--typography-size-micro); font-variant-numeric: tabular-nums; }

/* --- table view (the WCAG-clean twin of every chart) --- */
.chart-table {
  width: 100%;
  border-collapse: collapse;
  margin-top: 0.7rem;
  font-size: var(--typography-size-meta);
}
.chart-table th, .chart-table td {
  text-align: left;
  padding: 0.4rem 0.5rem;
  border-bottom: 1px solid var(--border-subtle);
}
.chart-table thead th { color: var(--text-secondary); font-size: var(--typography-size-micro); text-transform: uppercase; letter-spacing: var(--typography-tracking-caps); }
.chart-table td { font-variant-numeric: tabular-nums; }

.chart-tip {
  position: absolute;
  z-index: 60;
  background: var(--chart-tip-bg);
  color: var(--chart-tip-ink);
  padding: 0.3rem 0.55rem;
  border-radius: var(--shape-radius-sm);
  font-size: var(--typography-size-micro);
  pointer-events: none;
  max-width: 16rem;
}

@media (min-width: 60rem) {
  #admin-charts, #admin-tables {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 1rem;
  }
  #admin-charts .chart-card, #admin-tables .chart-card { margin: 0; }
}

/* ---- Rádios por estado (hubs /estado/<uf>/) ---- */
/* Links leves no rodapé da home — o 2º eixo de navegação. Pills como os chips
   de estilo (mesma linguagem "isto filtra"), mas são <a> navegando para o hub
   do estado, não botões de filtro client-side. */
.estado-links {
  list-style: none; padding: 0; margin: 0.75rem 0 0;
  display: flex; flex-wrap: wrap; gap: 0.5rem;
}
.estado-links a {
  display: inline-flex; align-items: center;
  font-size: var(--typography-size-body); font-weight: 600;
  padding: 0.55rem 1.1rem; min-height: var(--shape-control-min-target);
  border: 2px solid var(--border-default); border-radius: 999px;
  background: var(--surface-raised); color: var(--text-primary);
  text-decoration: none;
}
.estado-links a:hover {
  border-color: var(--brand-accent); color: var(--brand-accent);
}

/* Bandeira do país no card/página. Emoji deliberado (única exceção viva à
   purga do §6 item 3): bandeira é símbolo de CORES FIXAS que currentColor não
   representa; em plataformas sem emoji-flag degrada para as letras do código
   ("BR"/"PT") — que é a informação. Ver build.flag_emoji. */
.flag { font-size: var(--typography-size-meta); line-height: 1; margin-right: 0.15rem; }

/* ---- Filtros compactos: rails de rolagem horizontal (2026-07-17) ----
   REQUISITO (Gabriel): "o usuário tem que entrar e JÁ ver as rádios para tocar
   sem scroll; tudo tem que funcionar no smartphone".

   Medido antes: no celular os 39 chips de país empilhavam em 810px — UMA TELA
   INTEIRA — e o 1º card caía em y=1932 (2,3 telas de scroll).

   Rail horizontal em vez de acordeão: o acordeão custa um toque e esconde o
   estado (quais filtros estão ativos). O rail mantém tudo VISÍVEL e alcançável
   com o polegar — é o que Spotify/YouTube/Netflix fazem com trilhas longas —
   e custa 1 linha em vez de 4. O overflow é do RAIL, nunca da página
   (tests/above-the-fold.spec.js prova os dois). */
.chips {
  flex-wrap: nowrap;
  overflow-x: auto;
  scrollbar-width: none;              /* Firefox */
  -webkit-overflow-scrolling: touch;  /* inércia no iOS */
  scroll-snap-type: x proximity;
  /* margem negativa + padding: os chips das pontas encostam na borda da tela
     em vez de "flutuar" — sinaliza que há mais coisa para rolar. */
  margin-inline: -1rem;
  padding-inline: 1rem;
  padding-bottom: 0.15rem;
}
.chips::-webkit-scrollbar { display: none; }
.chips > * { flex: none; scroll-snap-align: start; }

/* O selo desceu para o rodapé do conteúdo: informação de confiança, não
   manchete — não paga mais o aluguel da dobra. */
.clean-pledge { margin-top: 2rem; }

/* CONTINUAR OUVINDO — o atalho para a rádio de sempre.
   O ouvinte deste site não explora um catálogo: ele volta para A rádio dele,
   quase todo dia. Fica entre o h1 e a busca porque é a PRIMEIRA coisa que quem
   volta procura; quem chega pela primeira vez nem o vê (o JS só o revela
   quando há uma última rádio AINDA no catálogo). */
.retomar {
  display: flex; width: 100%; margin: 0.2rem 0 0.6rem;
  /* `justify-content: center` vem do .btn base. Aqui o conteúdo é ícone +
     frase longa: centralizado, o ícone flutua longe do texto e a linha fica
     torta — lido no screenshot de 22/07. */
  justify-content: flex-start; text-align: left;
}
.retomar .retomar-txt {
  /* min-width:0 é o que FAZ a reticência funcionar: sem ele o filho flex
     recusa encolher abaixo do conteúdo, o nowrap não tem onde cortar e o nome
     estoura em 2 linhas (foi o que o screenshot mostrou com "Christian Power
     Praise Dot Net"). */
  min-width: 0; flex: 1;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* INSTALAR NA TELA DE INÍCIO — convite discreto, não manchete.
   Fica ABAIXO da busca de propósito: quem chega quer ouvir rádio, não instalar
   nada. É contorno (btn-outline) e não preenchido, para não competir com o
   "Continuar ouvindo" nem com o play dos cards — a hierarquia é ouvir primeiro,
   voltar depois. Compacto para não custar a dobra (tests/instalar-app.spec.js
   guarda isso). */
.instalar {
  display: flex; width: 100%; justify-content: center;
  margin: 0 0 0.6rem; min-height: 2.75rem;
  font-size: var(--typography-size-meta);
}
