/* ==========================================================================
   Faith to Faith Radio — Design System & Site Styles
   Plain CSS, no build step, so any volunteer can open and edit this file.
   ========================================================================== */

/* ---------- Design tokens ---------- */
:root {
  /* Warm neutral palette */
  --bg: #fbf7ef;              /* warm cream page background */
  --bg-alt: #f3ecdb;          /* deeper cream for alternating sections */
  --surface: #ffffff;         /* card surfaces */
  --border: #e6dcc4;          /* hairline borders */

  /* Ink */
  --ink: #1c1712;             /* true near-black for body text */
  --ink-soft: #55493c;        /* muted secondary text */
  --ink-faint: #86796a;       /* tertiary / placeholder text */

  /* Brand accents — the church's colors are black and gold; cream stays as
     the light-mode ground since it already reads as an elegant, warm
     pairing with both (think ivory stationery, not stark white). */
  --gold: #8a5d12;            /* accessible gold — safe for text on cream/white */
  --gold-deep: #6e4a0e;       /* hover/active state of gold */
  --gold-bright: #d4af37;     /* classic metallic gold — dark backgrounds only */
  --terracotta: #af4530;      /* deep red accent, used sparingly (focus rings, required-field marks) */
  --dark: #16130f;            /* true black (warm undertone) — headers/footers/player */
  --dark-soft: #241f1a;       /* lighter charcoal panel on dark sections */

  /* Feedback */
  --success: #2f6b3f;
  --success-bg: #e7f2e6;
  --error: #a3312a;
  --error-bg: #fbeae8;

  /* Typography */
  --font-display: "Fraunces", Georgia, "Times New Roman", serif;
  --font-body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;

  /* Spacing scale */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-5: 1.5rem;
  --space-6: 2rem;
  --space-7: 3rem;
  --space-8: 4.5rem;
  --space-9: 6rem;

  /* Layout */
  --container: 72rem;
  --radius-sm: 8px;
  --radius-md: 14px;
  --radius-lg: 22px;
  --radius-pill: 999px;

  --shadow-sm: 0 1px 2px rgba(28, 23, 18, 0.06), 0 1px 1px rgba(28, 23, 18, 0.04);
  --shadow-md: 0 8px 24px rgba(28, 23, 18, 0.10);
  --shadow-lg: 0 20px 48px rgba(28, 23, 18, 0.16);

  --transition: 180ms ease;
}

/* ---------- Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

img, svg { display: block; max-width: 100%; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: 1.2;
  margin: 0 0 var(--space-4);
  color: var(--ink);
}

h1 { font-size: clamp(2.1rem, 4vw, 3.2rem); font-weight: 500; }
h2 { font-size: clamp(1.6rem, 2.6vw, 2.2rem); }
h3 { font-size: 1.3rem; }
h4 { font-size: 1.05rem; }

p { margin: 0 0 var(--space-4); }
p:last-child { margin-bottom: 0; }

a {
  color: var(--gold);
  text-decoration-thickness: 1.5px;
  text-underline-offset: 3px;
}
a:hover { color: var(--gold-deep); }

/* Phone numbers wrap mid-digit-group in narrow spots (the footer columns,
   the Contact page's info list) otherwise — e.g. "+234 913 329" stranding
   "2147" alone on the next line. Keeping the whole number on one line
   reads far better than a mid-number break ever could. */
a[href^="tel:"] { white-space: nowrap; }

ul, ol { margin: 0 0 var(--space-4); padding-left: 1.3em; }
li + li { margin-top: var(--space-2); }

/* Visible, consistent focus ring everywhere (keyboard accessibility) */
a:focus-visible,
button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
summary:focus-visible,
[tabindex]:focus-visible {
  outline: 3px solid var(--terracotta);
  outline-offset: 2px;
  border-radius: 4px;
}

/* Skip link */
.skip-link {
  position: absolute;
  left: var(--space-4);
  top: -60px;
  background: var(--dark);
  color: #fff;
  padding: var(--space-3) var(--space-5);
  border-radius: var(--radius-sm);
  z-index: 200;
  transition: top var(--transition);
}
.skip-link:focus { top: var(--space-4); }

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

/* ---------- Layout helpers ---------- */
.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--space-5);
}

.section { padding-block: var(--space-8); }
.section--alt { background: var(--bg-alt); }
.section--dark { background: var(--dark); color: #f4ede0; }
.section--dark h1, .section--dark h2, .section--dark h3, .section--dark p { color: inherit; }
.section--dark .eyebrow { color: var(--gold-bright); }

.section-head {
  max-width: 42rem;
  margin: 0 0 var(--space-6);
}
.section-head.center { margin-inline: auto; text-align: center; }

.eyebrow {
  display: inline-block;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 0.78rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--gold);
  margin-bottom: var(--space-3);
}

.lede {
  font-size: 1.15rem;
  color: var(--ink-soft);
}

/* ---------- Buttons ---------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  font-family: var(--font-body);
  font-weight: 600;
  font-size: 0.98rem;
  line-height: 1;
  padding: 0.85em 1.5em;
  border-radius: var(--radius-pill);
  border: 2px solid transparent;
  cursor: pointer;
  text-decoration: none;
  transition: background var(--transition), color var(--transition), border-color var(--transition), transform var(--transition);
  white-space: nowrap;
}
.btn:active { transform: translateY(1px); }

.btn--primary {
  background: var(--dark);
  color: #fdf9ef;
}
.btn--primary:hover { background: var(--dark-soft); color: #fff; }

.btn--gold {
  background: var(--gold);
  color: #fff;
}
.btn--gold:hover { background: var(--gold-deep); color: #fff; }

.btn--ghost {
  background: transparent;
  border-color: var(--border);
  color: var(--ink);
}
.btn--ghost:hover { border-color: var(--ink-soft); }

.btn--ghost-light {
  background: rgba(22, 17, 12, 0.25);
  border-color: rgba(244, 237, 224, 0.55);
  color: #f4ede0;
}
.btn--ghost-light:hover { border-color: #f4ede0; background: rgba(244, 237, 224, 0.12); }

.btn--block { width: 100%; }
.btn--sm { padding: 0.55em 0.9em; font-size: 0.86rem; }

/* ---------- Header / Navigation ---------- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: rgba(251, 247, 239, 0.92);
  backdrop-filter: saturate(180%) blur(10px);
  /* A dark hairline (or a near-invisible shadow) disappears against dark
     page content — the hero photo, dark CTA sections, etc. Gold is bright
     and saturated enough to read as a clear edge against both a light
     page and a dark one, so it's a single separator that actually works
     everywhere instead of only in one context. */
  border-bottom: 1px solid rgba(212, 175, 55, 0.45);
}

.nav-bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding-block: var(--space-3);
}

.brand {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  text-decoration: none;
  color: var(--ink);
  flex-shrink: 0;
}
.brand:hover { color: var(--ink); }
.brand-mark {
  width: 42px;
  height: 42px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.brand-mark img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.brand-name {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.05rem;
  line-height: 1.1;
}
.brand-name small {
  display: block;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: 0.68rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.nav-toggle {
  display: none;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
}
.nav-toggle svg { flex-shrink: 0; }

.primary-nav { flex: 1; min-width: 0; }
.primary-nav > ul {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px;
  margin: 0;
  padding: 0;
}
.primary-nav ul li { margin-top: 0; }
.primary-nav a {
  display: inline-block;
  padding: 0.5em 0.4em;
  border-radius: var(--radius-pill);
  color: var(--ink-soft);
  font-weight: 600;
  font-size: 0.83rem;
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--transition), color var(--transition);
}
.primary-nav a:hover { background: var(--bg-alt); color: var(--ink); }
.primary-nav a[aria-current="page"] {
  background: var(--dark);
  color: #fdf9ef;
}

.nav-actions {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-shrink: 0;
}

/* "More From Us" — links out to separate, affiliated websites (Digital
   Library, Faith to Faith TV). These are real external sites, not sections
   of this one, so this is a native <details>/<summary> disclosure rather
   than a fake in-page tab: it works with zero JS, and the small enhancement
   script only adds outside-click/Escape-to-close polish on top. */
.nav-dropdown { position: relative; list-style: none; }
.nav-dropdown-toggle {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  padding: 0.5em 0.4em;
  border-radius: var(--radius-pill);
  color: var(--ink-soft);
  font-weight: 600;
  font-size: 0.83rem;
  cursor: pointer;
  user-select: none;
  white-space: nowrap;
  transition: background var(--transition), color var(--transition);
}
.nav-dropdown-toggle { list-style: none; }
.nav-dropdown-toggle::-webkit-details-marker { display: none; }
.nav-dropdown-toggle::marker { content: ""; }
.nav-dropdown-toggle:hover { background: var(--bg-alt); color: var(--ink); }
.nav-dropdown-caret { transition: transform var(--transition); flex-shrink: 0; }
.nav-dropdown[open] > .nav-dropdown-toggle .nav-dropdown-caret { transform: rotate(180deg); }

.nav-dropdown-menu {
  list-style: none;
  margin: var(--space-2) 0 0;
  padding: var(--space-2);
  position: absolute;
  top: 100%;
  left: 0;
  min-width: 14rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  z-index: 50;
}
.nav-dropdown-menu li { margin: 0; }
.primary-nav .nav-dropdown-menu a {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: 0.65em 0.75em;
  border-radius: var(--radius-sm);
  color: var(--ink);
  font-weight: 600;
  font-size: 0.92rem;
}
.primary-nav .nav-dropdown-menu a:hover { background: var(--bg-alt); color: var(--gold); }
.nav-dropdown-menu .external-icon { color: var(--ink-faint); flex-shrink: 0; }

@media (max-width: 1220px) {
  /* With .primary-nav dropped out of the row (below), .nav-bar's
     space-between is left distributing space across just two items
     (brand and the hamburger) — which floats the hamburger dead-center
     in the row rather than pinning it to an edge. Giving .nav-actions
     (the Give button) auto margin instead pulls all the free space to
     sit before it, so brand hugs the left edge and Give + the hamburger
     (ordered right after it, with the row's normal gap between them)
     land together in the top-right corner instead. */
  .nav-toggle { display: inline-flex; order: 2; }
  .nav-actions { order: 1; margin-left: auto; }
  /* .nav-bar itself is never given flex-wrap: wrap, since the desktop row
     (brand + nav + Give, no toggle) always fits on one line. But
     .primary-nav's flex-basis: 100% below relies on being able to wrap
     onto its own row once opened — without this, the browser has nowhere
     to put that full-width row except squeezed onto the same line as
     everything else, which is what was pushing the open menu off the
     right edge of the screen instead of dropping it below the bar. */
  .nav-bar { flex-wrap: wrap; }
  .primary-nav {
    order: 3;
    flex-basis: 100%;
    display: none;
  }
  .primary-nav.is-open { display: block; }
  .primary-nav ul { flex-direction: column; align-items: stretch; gap: var(--space-1); padding-block: var(--space-3); }
  /* The open menu is a full-width column now, not a row of pill-shaped
     tags squeezed together — nowrap (needed there to stop a tag's label
     from wrapping mid-pill) is exactly what caused a long label to run
     past the screen edge instead of wrapping inside its own full-width
     row here. */
  .primary-nav a { display: block; padding: 0.8em 1em; white-space: normal; }
  .nav-actions .btn span.label-long { display: none; }

  .nav-dropdown-toggle { display: flex; justify-content: space-between; padding: 0.8em 1em; white-space: normal; }
  .nav-dropdown-menu {
    position: static;
    margin-top: 0;
    padding: 0 0 0 var(--space-4);
    border: none;
    box-shadow: none;
    background: transparent;
  }
}

/* Small phones: brand + hamburger + Give button alone (nav links are
   hidden behind the hamburger here) can still overflow a narrow screen
   without a bit more trimming. */
@media (max-width: 400px) {
  .container { padding-inline: var(--space-4); }
  .nav-bar { gap: var(--space-2); }
  .brand-name { font-size: 0.92rem; }
  .brand-name small { display: none; }
  .nav-actions .btn { padding: 0.5em 0.75em; font-size: 0.82rem; }
}

/* ---------- Mini live player (persistent) ---------- */
.mini-player {
  background: var(--dark);
  color: #f4ede0;
  border-top: 1px solid rgba(244, 237, 224, 0.12);
}
.mini-player-inner {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  padding-block: var(--space-3);
}

.play-toggle,
.jp-play,
.jp-pause {
  flex-shrink: 0;
  width: 48px;
  height: 48px;
  border-radius: 50%;
  background: var(--gold-bright);
  color: var(--dark);
  border: none;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: transform var(--transition), background var(--transition);
}
.play-toggle:hover,
.jp-play:hover,
.jp-pause:hover { background: #ddbf5f; }
.play-toggle:active,
.jp-play:active,
.jp-pause:active { transform: scale(0.96); }
.play-toggle svg,
.jp-play svg,
.jp-pause svg { width: 20px; height: 20px; }

/* .jp-play / .jp-pause are two separate elements — RadioJar's own script
   toggles which one is visible (display:block/none) as playback state
   changes, rather than us swapping an icon inside one button. The homepage
   card's .play-toggle is a proxy button (no real RadioJar element of its
   own), so it still uses the old single-button icon-swap approach, driven
   by the .is-playing class js/main.js mirrors onto it. */
.play-toggle .icon-pause { display: none; }
.player-card.is-playing .play-toggle .icon-play { display: none; }
.player-card.is-playing .play-toggle .icon-pause { display: block; }

/* RadioJar lazy-loads its actual streaming logic on the first play
   request, so there's a real ~1-2s gap between "clicked" and "audibly
   playing" the first time each page loads. Without this, repeat clicks
   during that gap look like nothing is happening. */
.is-connecting .play-toggle,
.is-connecting .jp-play,
.is-connecting .jp-pause { opacity: 0.6; cursor: wait; pointer-events: none; }

/* Play/pause + volume for the one real player instance (header). RadioJar
   owns this DOM shape (container id + these class names) — see README. */
.rjp-player-container {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  flex-shrink: 0;
}
#rjp-radiojar-player { width: 0; height: 0; overflow: hidden; }
.rj-player-controls {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.jp-volume-bar-wrapper {
  display: none;
  width: 70px;
  height: 6px;
  border-radius: 3px;
  background: rgba(244, 237, 224, 0.2);
  cursor: pointer;
}
.jp-volume-bar { width: 100%; height: 100%; position: relative; }
.jp-volume-bar-value {
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 70%;
  border-radius: 3px;
  background: var(--gold-bright);
}
@media (min-width: 640px) {
  .jp-volume-bar-wrapper { display: block; }
}


.live-badge {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  color: var(--gold-bright);
  flex-shrink: 0;
}
.live-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--ink-faint);
}
.mini-player.is-playing .live-dot,
.player-card.is-playing .live-dot {
  background: #e5544a;
  animation: pulse 2s infinite ease-in-out;
}
@keyframes pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.55; transform: scale(0.82); }
}

.now-playing-text {
  min-width: 0;
  flex: 1;
}
.now-playing-text .np-label {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: rgba(244, 237, 224, 0.6);
  margin-bottom: 2px;
}
.now-playing-text .np-title {
  font-family: var(--font-display);
  font-size: 1.02rem;
  font-weight: 600;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  transition: opacity 400ms ease;
}
.now-playing-text .np-title.is-fading { opacity: 0; }
.now-playing-text .np-title-track {
  font-family: var(--font-display);
  font-size: 1.02rem;
  font-weight: 600;
  transition: opacity 400ms ease;
}
.now-playing-text .np-title-track.is-fading { opacity: 0; }

/* ---------- Now-playing marquee (scrolls the title across the header bar
   while the stream is playing, ticker-style) ----------
   No permanent width cap here — the viewport is only narrowed down (via
   .is-marquee-active, toggled by JS's updateMarqueeOverflow) once a title
   is both too long AND actually playing. Any other time — paused, or a
   title short enough to fit on its own — the viewport stays as wide as it
   needs to be, so the text is never clipped.
   .np-title-track holds two back-to-back .np-title-copy spans; animating
   the track from 0 to -50% scrolls the first copy fully off just as the
   second reaches the start, so the loop is seamless instead of jumping. */
.np-title-viewport {
  overflow: hidden;
  white-space: nowrap;
  position: relative;
}
.np-title-viewport.is-marquee-active {
  max-width: 11.5rem;
}
.np-title-track {
  display: inline-flex;
  white-space: nowrap;
}
.np-title-copy {
  display: inline-block;
  white-space: nowrap;
  /* ch = the width of "0" in the current font — a literal 5-character gap
     of blank space between the end of the message and its repeat. */
  padding-right: 5ch;
}
/* The second copy exists only to make the scroll loop seamless once the
   marquee is actually active. Keep it out of layout otherwise, so a
   static title never shows the text twice side by side. */
.np-title-copy:nth-child(2) { display: none; }
.np-title-viewport.is-marquee-active .np-title-copy:nth-child(2) { display: inline-block; }
.np-title-viewport.is-marquee-active .np-title-track {
  animation: np-marquee 14s linear infinite;
}
@keyframes np-marquee {
  0% { transform: translateX(0); }
  100% { transform: translateX(-50%); }
}
@media (prefers-reduced-motion: reduce) {
  .mini-player.is-playing .np-title-track {
    animation: none;
  }
}

/* ---------- Hero ---------- */
/* The community photo is a full-bleed background behind the whole hero —
   text and player card both sit on top of it — with a dark gradient
   overlay keeping everything readable regardless of what's underneath. */
.hero {
  position: relative;
  padding-block: var(--space-9) var(--space-8);
  /* The hero photo fills the screen: exactly what's left below the sticky
     header, so header + hero together are one viewport tall — no more (which
     would push the photo's bottom edge below the fold) and no less (which
     would show a strip of the next section). A flat 100vh would overflow by
     the height of the header, so js/main.js measures the real header into
     --header-height and this subtracts it.
     Deliberately uncapped. An earlier max-height here made the photo shorter
     on tall screens to tighten the space around the copy — but the photo
     filling the viewport is the point of this hero, so the copy is sized to
     hold that space instead (see .hero .hero-copy h1 below). */
  min-height: calc(100vh - var(--header-height, 7.5rem));
  display: flex;
  align-items: center;
  overflow: hidden;
  isolation: isolate;
  /* Fallback so any sub-pixel rounding at the header/hero seam shows dark,
     not a stray sliver of the page's cream bg. */
  background: var(--dark);
}
.hero-inner { width: 100%; }
/* Full-bleed, flush with the bottom of the header — no gap. A buffer strip
   sized to the header's height was tried so the sticky header would never
   slide over the photo, but in practice it just looked like a large,
   disconnected pale band sitting under the header rather than anything
   deliberate. Standard sticky-header behavior (it overlaps whatever
   scrolls beneath it) already applies to every other section of the site,
   so the photo isn't special-cased here either. */
.hero-bg {
  position: absolute;
  inset: -1px;
  z-index: -2;
}
.hero-bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center top;
  display: block;
}
/* A flat, fairly heavy wash rather than a directional gradient — we don't
   yet know what the real photo will look like, so contrast needs to hold
   up (buttons, text) no matter how bright or busy it is. */
.hero-overlay {
  position: absolute;
  inset: -1px;
  z-index: -1;
  background:
    linear-gradient(0deg, rgba(10, 8, 6, 0.55), transparent 45%),
    rgba(10, 8, 6, 0.6);
}

/* Even split rather than favouring the copy. The player card is the working
   part of this page — it carries the live stream — and the extra width goes
   straight into the now-playing title, which would otherwise be squeezed into
   a narrow strip beside the larger album art and wrap to four or five lines. */
.hero-inner {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-8);
  align-items: center;
}
@media (max-width: 900px) {
  .hero-inner { grid-template-columns: 1fr; gap: var(--space-6); }
}

.hero-copy { color: #f4ede0; }
.hero-copy .eyebrow { color: var(--gold-bright); }
.hero-copy h1 { color: #fff; }
.hero-copy .lede { color: rgba(244, 237, 224, 0.85); }

/* The homepage hero lost its supporting paragraph, leaving the headline as
   the only copy in the column. It takes a larger size (and more air before
   the buttons) so the text side still holds its weight against the player
   card rather than floating in the middle of a full-viewport section.
   Scoped to .hero so the interior .page-hero headings, which still run with
   a lede beneath them, keep the base scale. */
.hero .hero-copy h1 { font-size: clamp(2.4rem, 5vw, 4rem); }
.hero .hero-actions { margin-top: var(--space-7); }

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin-top: var(--space-6);
}

/* ---------- Now Playing card (homepage feature) ---------- */
/* The hero's centrepiece — sized to stand as a real panel beside the
   headline rather than a small widget tucked in the corner. The height comes
   from the artwork and a full-width control bar rather than from padding and
   large gaps, so the card reads as filled instead of as three short rows
   floating apart. */
.player-card {
  background: var(--dark);
  color: #f4ede0;
  border-radius: var(--radius-lg);
  /* Heavier bottom padding on purpose. Above the artwork sit the card's top
     padding, the ON AIR NOW row and the row gap — about 74px in total — so
     matching that below (--space-8, 72px) optically centres the artwork in
     the card instead of letting it ride high with the badge's height only
     ever counted on one side. */
  padding: var(--space-6) var(--space-6) var(--space-8);
  box-shadow: var(--shadow-lg);
  position: relative;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
}
.player-card::before {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(70% 60% at 90% -10%, rgba(212, 175, 55, 0.25), transparent 60%);
  pointer-events: none;
}
.player-card-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  position: relative;
}

/* Artwork, track info and the play button share one row, so the card is two
   rows rather than three — the button no longer takes a line of its own. The
   gap is tighter than the old two-item row because the button and its gap now
   come out of the width the title had. This is also the card's last row, so
   it carries no bottom margin; the card's own padding closes it out. */
.player-visual {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin: var(--space-5) 0 0;
  position: relative;
}
/* Carries the card's height. The artwork absorbing the space that used to sit
   in padding and row gaps is what keeps the card its full size while the
   contents stay packed. */
.player-art {
  width: 160px;
  height: 160px;
  border-radius: var(--radius-md);
  background: linear-gradient(155deg, var(--gold-bright), var(--terracotta));
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}
.player-art svg { width: 56px; height: 56px; color: rgba(22, 19, 15, 0.75); }
.player-art img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.player-art img[data-now-playing-art="logo"] {
  object-fit: contain;
  padding: 22px;
}

/* Takes whatever the artwork and play button leave, so the title fills the
   middle of the row instead of the button being pushed onto its own line. */
.player-meta { min-width: 0; flex: 1; }
.player-meta .np-label {
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: rgba(244, 237, 224, 0.6);
  margin-bottom: 5px;
}
/* The hero card never scrolls (it's not a .np-title-track), so it should
   never inherit the marquee viewport's overflow:hidden/nowrap clipping —
   let the full title show, wrapping onto a second line if it needs to. */
.player-meta .np-title-viewport {
  overflow: visible;
  white-space: normal;
}
.player-meta .np-title {
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 600;
  white-space: normal;
  transition: opacity 400ms ease;
}
.player-meta .np-title.is-fading { opacity: 0; }
.player-meta .np-sub {
  font-size: 1rem;
  color: rgba(244, 237, 224, 0.72);
  margin-top: 3px;
}

/* Keeps the shared circular treatment, just scaled up for the larger card.
   The header's own .jp-play / .jp-pause stay at the base 48px. Sitting inline
   at the end of the track-info row, it holds to 56px — every pixel here comes
   straight out of the title's width. */
.player-card .play-toggle {
  width: 56px;
  height: 56px;
  /* Dropped to the bottom of the row rather than centred in it, so its lower
     edge lines up with the artwork's and the control lands in the content
     area's bottom-right corner. The row is taller than the button, so
     centring left it floating at no particular height. */
  align-self: flex-end;
}
.player-card .play-toggle svg { width: 24px; height: 24px; }

/* Single-column hero below 900px puts the card at full width, where the
   desktop artwork reads oversized on a phone. */
@media (max-width: 560px) {
  .player-card { padding: var(--space-5) var(--space-5) var(--space-7); }
  .player-art { width: 104px; height: 104px; }
  .player-art svg { width: 40px; height: 40px; }
  .player-art img[data-now-playing-art="logo"] { padding: 14px; }
  .player-meta .np-title { font-size: 1.25rem; }
  .player-card .play-toggle { width: 52px; height: 52px; }
}

.eq-bars {
  display: inline-flex;
  align-items: flex-end;
  gap: 3px;
  height: 16px;
}
.eq-bars span {
  width: 3px;
  background: var(--gold-bright);
  border-radius: 2px;
  animation: eq 1.1s ease-in-out infinite;
  opacity: 0.4;
}
.mini-player.is-playing .eq-bars span,
.player-card.is-playing .eq-bars span { opacity: 1; }
.eq-bars span:nth-child(1) { height: 40%; animation-delay: -0.9s; }
.eq-bars span:nth-child(2) { height: 100%; animation-delay: -0.5s; }
.eq-bars span:nth-child(3) { height: 65%; animation-delay: -0.2s; }
.eq-bars span:nth-child(4) { height: 85%; animation-delay: -0.7s; }
@keyframes eq {
  0%, 100% { transform: scaleY(0.35); }
  50% { transform: scaleY(1); }
}
.player-card:not(.is-playing) .eq-bars span,
.mini-player:not(.is-playing) .eq-bars span { animation-play-state: paused; }

/* ---------- Cards ---------- */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: var(--space-5);
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  transition: transform var(--transition), box-shadow var(--transition);
}
a.card { text-decoration: none; color: inherit; }
a.card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }
a.card:hover h3 { color: var(--gold); }

.card-icon {
  width: 48px;
  height: 48px;
  border-radius: var(--radius-sm);
  background: var(--bg-alt);
  color: var(--gold);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.card-icon svg { width: 24px; height: 24px; }

.card h3 { margin-bottom: 0; transition: color var(--transition); }
.card p { color: var(--ink-soft); margin: 0; }
.card .card-cta {
  margin-top: auto;
  font-weight: 600;
  font-size: 0.9rem;
  color: var(--gold);
  display: inline-flex;
  align-items: center;
  gap: 4px;
}

/* ---------- Community / social strip ---------- */
.social-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}
.social-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: 0.7em 1.2em;
  border-radius: var(--radius-pill);
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--ink);
  text-decoration: none;
  font-weight: 600;
  font-size: 0.92rem;
  transition: border-color var(--transition), transform var(--transition);
}
.social-link:hover { border-color: var(--gold); color: var(--gold); transform: translateY(-2px); }
.social-link svg { width: 18px; height: 18px; }

/* ---------- Page hero (interior pages) ---------- */
.page-hero {
  padding-block: var(--space-8) var(--space-7);
  background: var(--bg-alt);
  border-bottom: 1px solid var(--border);
}
.page-hero .lede { max-width: 40rem; }

/* Photo variant — the page's hero photo lives here instead of appearing
   further down the page after the text/form, so the first viewport is
   never text-only while the visual loads in later. Reuses the homepage
   hero's .hero-bg/.hero-overlay/.hero-copy building blocks. */
.page-hero--photo {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  background: var(--dark);
  border-bottom: none;
  /* Without a set height this band is only as tall as the heading text,
     which left most photos showing nothing but a thin cropped sliver of
     whatever happened to be at the very top of the frame — background,
     lighting, the tops of heads. A real min-height plus a centered (not
     top-anchored) crop keeps the actual subject in view. */
  min-height: 22rem;
  display: flex;
  align-items: center;
}
.page-hero--photo .hero-bg img { object-position: center; }
/* The Message of the Week hero photo puts the stage and singers in its top
   third and the backs of the congregation's heads in the lower two — a
   centre crop lands on the audience. Pinning to the top keeps the service
   itself in the banner. Scoped to that one page, since .hero-bg is shared
   by every page's hero. */
.page-hero--focus-top .hero-bg img { object-position: center 20%; }
@media (max-width: 640px) {
  .page-hero--photo { min-height: 18rem; }
}

/* ---------- Placeholder callouts ---------- */
.placeholder-box {
  border: 1.5px dashed #c9a25a;
  background: #fbf3de;
  border-radius: var(--radius-md);
  padding: var(--space-4) var(--space-5);
  color: #6e4a0e;
  font-size: 0.92rem;
  display: flex;
  gap: var(--space-3);
  align-items: flex-start;
  margin-bottom: var(--space-6);
}
.placeholder-box svg { flex-shrink: 0; width: 20px; height: 20px; margin-top: 2px; color: var(--terracotta); }
.placeholder-box strong { color: #4a3208; }

.placeholder-text {
  color: var(--ink-faint);
  font-style: italic;
}

/* ---------- Photo placeholders ---------- */
/* Marked spots for real photography. Swap by replacing the .photo-frame div
   with an <img> (keep the class for sizing/aspect-ratio, e.g.
   <img class="photo-frame photo-frame--wide" src="…" alt="…">). */
.photo-frame {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  text-align: center;
  padding: var(--space-4);
  border: 1.5px dashed #c9a25a;
  border-radius: var(--radius-md);
  background: repeating-linear-gradient(135deg, var(--bg-alt), var(--bg-alt) 10px, #ede2c8 10px, #ede2c8 20px);
  color: #8a6a2e;
}
.photo-frame svg { width: 30px; height: 30px; flex-shrink: 0; }
.photo-frame span { font-size: 0.82rem; font-style: italic; max-width: 18rem; line-height: 1.4; }

/* Once a real (or demo) photo is dropped in, fill the frame instead of showing the dashed placeholder look. */
.photo-frame:has(img) {
  padding: 0;
  border-style: solid;
  background: var(--bg-alt);
  overflow: hidden;
}
.photo-frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.photo-frame--wide { aspect-ratio: 16 / 9; }
.photo-frame--square { aspect-ratio: 1 / 1; }

.photo-frame--round {
  aspect-ratio: 1 / 1;
  width: 96px;
  height: 96px;
  border-radius: 50%;
  padding: var(--space-2);
  margin-inline: auto;
  flex-shrink: 0;
}
.photo-frame--round svg { width: 22px; height: 22px; }
.photo-frame--round span { display: none; }
.photo-frame--round.photo-frame--sm { width: 56px; height: 56px; margin-inline: 0; }
.photo-frame--round.photo-frame--sm svg { width: 18px; height: 18px; }

.photo-gallery {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  gap: var(--space-5);
}
.photo-gallery .photo-frame { aspect-ratio: 4 / 3; }

/* ---------- Carousel (homepage "Moments" / Partner With Us) ---------- */
/* A true filmstrip: every photo is one flex item in a single row, sized so
   3 are visible at once (2 on tablet, 1 on mobile) — bigger than the old
   4-per-row grid. The arrows step the whole strip by exactly one item's
   width (measured in JS), so one image leaves on one side and exactly one
   new image slides in on the other, rather than jumping a whole page at a
   time. */
.carousel {
  --carousel-arrow-w: 44px;
  max-width: 88rem;
  margin: var(--space-7) auto 0;
  padding-inline: var(--space-4);
}
.carousel-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.carousel-viewport {
  flex: 1;
  min-width: 0;
  overflow: hidden;
  position: relative;
}
.carousel-track {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  transition: transform 650ms ease;
}
/* Shared column sizing for both the fixed link-card row and the
   sliding photo row — same flex-basis math on both means the 4 cards
   line up above the 4 currently-visible photos at every breakpoint,
   even as the photos themselves scroll behind them. */
.carousel-col {
  flex: 0 0 calc((100% - 3 * var(--space-4)) / 4);
  min-width: 0;
}
@media (max-width: 900px) {
  .carousel-col { flex-basis: calc((100% - 1 * var(--space-4)) / 2); }
}
@media (max-width: 560px) {
  .carousel-col { flex-basis: 100%; }
}

.carousel-item {
  /* .carousel-item is a <figure> — reset the browser's default
     1em/40px margin, which was silently adding ~80px of extra spacing
     between photos on top of the intended gap. */
  margin: 0;
}
.carousel-item img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  display: block;
  border-radius: var(--radius-md);
}

.carousel-links-row {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin-bottom: var(--space-4);
  /* Matches the arrow button's width + the row gap that indents
     .carousel-viewport below, so these columns still land above the
     photo columns even though this row isn't flanked by arrows itself. */
  padding-inline: calc(var(--carousel-arrow-w) + var(--space-3));
  /* Room for the hover lift (translateY, below) so it doesn't get cut
     off — this row isn't inside an overflow:hidden ancestor, so a
     couple of px of padding is enough. */
  padding-top: 6px;
}
.carousel-link-btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-4) var(--space-3);
  text-decoration: none;
  color: var(--ink);
  font-weight: 700;
  font-size: 0.92rem;
  text-align: center;
  box-shadow: var(--shadow-sm);
  transition: transform var(--transition), box-shadow var(--transition), border-color var(--transition);
}
.carousel-link-btn:hover {
  transform: translateY(-3px);
  box-shadow: var(--shadow-md);
  border-color: var(--gold);
}
.carousel-link-btn .card-icon {
  width: 44px;
  height: 44px;
  background: var(--bg-alt);
  color: var(--gold);
}
.carousel-link-btn .card-icon svg { width: 22px; height: 22px; }
.carousel-arrow {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid var(--border);
  background: var(--surface);
  color: var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  transition: background var(--transition), border-color var(--transition);
}
.carousel-arrow:hover { background: var(--bg-alt); border-color: var(--ink-faint); }
.carousel-dots {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-top: var(--space-4);
}
.carousel-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  border: none;
  padding: 0;
  background: var(--border);
  cursor: pointer;
  transition: background var(--transition), transform var(--transition);
}
.carousel-dot:hover { background: var(--ink-faint); }
.carousel-dot.is-active { background: var(--gold); transform: scale(1.2); }

@media (max-width: 640px) {
  .carousel { --carousel-arrow-w: 36px; gap: var(--space-2); padding-inline: var(--space-3); }
  .carousel-arrow { width: 36px; height: 36px; }
}

.speaker-row {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: var(--space-3) 0;
}

/* ---------- About statement ----------
   The ministry's mission copy, set as a centered statement rather than one
   long block of body text: the lead line carries the actual mission at
   display size, with the supporting line beneath it. */
.about-statement {
  max-width: 46rem;
  margin-inline: auto;
  text-align: center;
}
.about-statement .statement-lead {
  font-family: var(--font-display);
  font-size: clamp(1.25rem, 2.2vw, 1.65rem);
  font-weight: 500;
  line-height: 1.45;
  color: var(--ink);
  /* Gold rules above and below lift the mission line out of the surrounding
     copy without boxing it in. */
  padding-block: var(--space-5);
  border-block: 1px solid rgba(212, 175, 55, 0.5);
  margin-bottom: var(--space-5);
}

/* ---------- Ministry split (summary + wide photo) ----------
   Deliberately uneven. The copy here is a short summary that hands off to the
   About page, so it doesn't need half the row — and a 16:9 photo can only get
   bigger by getting wider, since its height is locked to its width. Giving the
   photo 5/8 of the row instead of 1/2 takes it from 528x297 to 660x371: a
   quarter wider and taller, half again the area, still the wide crop. */
.ministry-split {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(0, 5fr);
  /* Tighter than the usual --space-7 so the saved width goes to the photo.
     This is close to the practical limit: the text column is the constraint,
     not the photo — below ~400px it drops under ~46 characters a line, which
     is too narrow to read comfortably. */
  gap: var(--space-6);
  align-items: start;
}
/* Extra air before the CTA so the copy column reaches further down the row
   rather than stopping well above the photo beside it. */
.ministry-split .prose .btn { margin-top: var(--space-6); }
@media (max-width: 860px) {
  .ministry-split { grid-template-columns: 1fr; gap: var(--space-6); }
}

/* ---------- Two column content ---------- */
.two-col {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-7);
  align-items: start;
}
@media (max-width: 860px) {
  .two-col { grid-template-columns: 1fr; }
}

/* ---------- Leadership split (portrait + bio) ----------
   The portrait is a real studio shot at 4:5, so it sets its own height and is
   never cropped — no aspect-ratio, no object-fit, just the file's own shape.
   That makes the column width the only lever on height: at an even split the
   photo would stand 660px against ~440px of bio. 26rem keeps it a substantial
   picture (520px tall, more presence than the 336px square it replaces) while
   leaving the bio a comfortable 640px measure beside it. */
.leader-split {
  display: grid;
  grid-template-columns: minmax(0, 26rem) minmax(0, 1fr);
  gap: var(--space-7);
  align-items: center;
}
/* The source portrait (864x1080) carries the photographer's watermark across
   its bottom edge, and there's dead headroom above the subject. The frame
   trims to 864x972 — 108px of slack — and the 14% split puts ~15px of that
   at the top and ~93px at the bottom, clearing the watermark while tightening
   the crop above his head. Done here rather than by re-cutting the JPEG so
   the original asset stays intact. */
.leader-portrait {
  display: block;
  width: 100%;
  aspect-ratio: 864 / 972;
  object-fit: cover;
  object-position: center 14%;
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
}

/* Caps the line length. Run full-bleed the bio column is 640px, which at
   1.08rem is ~74 characters — past the 60-70 that reads comfortably. 34rem
   lands at 63. It also closes the column gap for free: a narrower measure
   wraps to more lines, so the text grows ~50px taller against the portrait
   rather than sitting short beside it. */
.leader-split .prose { max-width: 34rem; }

/* A short rule under the heading, separating the label from the bio and
   echoing the gold rules bracketing the mission statement further up the
   page. ::after rather than an <hr> so the markup stays clean. */
.leader-split h2 { margin-bottom: var(--space-5); }
.leader-split h2::after {
  content: "";
  display: block;
  width: 3rem;
  height: 2px;
  margin-top: var(--space-4);
  background: var(--gold);
}

/* ---------- Prose block ----------
   One voice for a run of body paragraphs. A .lede opening paragraph reads as
   larger but lighter, so every paragraph after it stepped *down* in size
   while stepping *up* in weight to near-black — hierarchy is meant to fade,
   not harden, and that jump was the most conspicuous thing on the page.
   :not(.eyebrow) matters: this selector outranks .eyebrow (0,1,1 vs 0,1,0),
   so without it the small gold section label would be repainted as body
   copy. */
.prose > p:not(.eyebrow) {
  font-size: 1.08rem;
  color: var(--ink-soft);
}
@media (max-width: 860px) {
  .leader-split { grid-template-columns: 1fr; gap: var(--space-6); }
  /* Stacked, the portrait would otherwise run the full container width on a
     tablet. Generous but bounded, and centred over the bio beneath it. */
  .leader-split .leader-portrait { max-width: 26rem; margin-inline: auto; }
}

/* ---------- Forms ---------- */
.form-card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  box-shadow: var(--shadow-sm);
}

.form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-4);
}
@media (max-width: 620px) {
  .form-row { grid-template-columns: 1fr; }
}

.field { margin-bottom: var(--space-5); }

.field label {
  display: block;
  font-weight: 600;
  font-size: 0.92rem;
  margin-bottom: var(--space-2);
  color: var(--ink);
}
.field .hint {
  display: block;
  font-size: 0.82rem;
  color: var(--ink-faint);
  margin-top: var(--space-1);
}
.field .req { color: var(--terracotta); }

.field input[type="text"],
.field input[type="email"],
.field input[type="tel"],
.field select,
.field textarea {
  width: 100%;
  font: inherit;
  font-size: 0.98rem;
  color: var(--ink);
  background: var(--bg);
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 0.75em 0.9em;
  transition: border-color var(--transition), background var(--transition);
}
.field input:hover, .field select:hover, .field textarea:hover { border-color: #d3c393; }
.field input:focus, .field select:focus, .field textarea:focus {
  border-color: var(--gold);
  background: var(--surface);
}

/* Field-level validation — set by JS (see initGivePage in js/main.js)
   pointing at the exact input that's wrong, rather than one generic
   status message the giver has to go hunting from. Visibility is the
   plain `hidden` attribute, toggled by JS; this only styles what shows. */
.field-error[hidden] { display: none; }
.field-error {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 0.85rem;
  color: var(--terracotta);
  margin-top: var(--space-2);
}
.field-error svg { flex-shrink: 0; width: 15px; height: 15px; }
.field.has-error input,
.field.has-error select,
.field.has-error textarea,
[data-amount-fieldset].has-error .amount-option label {
  border-color: var(--terracotta);
}
.field textarea { resize: vertical; min-height: 9rem; }

.field select {
  appearance: none;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20' fill='%2355493c'><path d='M5.5 7.5l4.5 4.5 4.5-4.5' stroke='%2355493c' stroke-width='1.6' fill='none' stroke-linecap='round' stroke-linejoin='round'/></svg>");
  background-repeat: no-repeat;
  background-position: right 0.85em center;
  background-size: 14px;
  padding-right: 2.2em;
}

fieldset {
  border: 1.5px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--space-4);
  margin: 0 0 var(--space-5);
}
fieldset legend {
  font-weight: 600;
  font-size: 0.92rem;
  padding-inline: var(--space-2);
}
.radio-group { display: flex; gap: var(--space-5); flex-wrap: wrap; }
.radio-option { display: flex; align-items: center; gap: var(--space-2); font-weight: 500; }
.radio-option input { accent-color: var(--gold); width: 18px; height: 18px; }

.form-status {
  margin-top: var(--space-5);
  padding: var(--space-4) var(--space-5);
  border-radius: var(--radius-sm);
  font-size: 0.95rem;
  display: none;
}
.form-status.is-success {
  display: flex;
  gap: var(--space-3);
  background: var(--success-bg);
  color: var(--success);
}
.form-status.is-error {
  display: flex;
  gap: var(--space-3);
  background: var(--error-bg);
  color: var(--error);
}
.form-status svg { flex-shrink: 0; width: 20px; height: 20px; margin-top: 2px; }

/* ---------- Contact info blocks ---------- */
.info-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}
.info-list li {
  display: flex;
  gap: var(--space-4);
  margin: 0;
}
.info-icon {
  width: 44px;
  height: 44px;
  border-radius: var(--radius-sm);
  background: var(--bg-alt);
  color: var(--gold);
  display: flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
}
.info-icon svg { width: 21px; height: 21px; }
.info-list h4 { margin-bottom: 2px; }
.info-list p { color: var(--ink-soft); margin: 0; }

/* Live OpenStreetMap embed of the Norfolk office. The iframe is given an
   explicit aspect-ratio (it has no intrinsic size of its own) and clipped by
   the rounded wrapper, so it sits in the page like any other framed media
   rather than as a bare browser frame. */
.map-embed {
  margin: var(--space-8) 0 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--bg-alt);
}
/* Runs the full container width beneath the form/details columns, so it takes
   a wide band rather than the 4:3 box it needed when it sat inside a column.
   The OSM bbox is cut to match this ratio — see the markup — so the embed
   isn't letterboxed. Narrow screens go back to a taller frame, where a 5:2
   strip would be too short to read. */
.map-embed iframe {
  display: block;
  width: 100%;
  aspect-ratio: 5 / 2;
  border: 0;
}
@media (max-width: 860px) {
  .map-embed { margin-top: var(--space-7); }
  .map-embed iframe { aspect-ratio: 4 / 3; }
}
.map-embed figcaption {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-4);
  padding: var(--space-3) var(--space-4);
  border-top: 1px solid var(--border);
  background: var(--surface);
  font-size: 0.88rem;
  color: var(--ink-soft);
}
.map-embed figcaption a {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
}
.map-embed figcaption a:hover { text-decoration: underline; }
.map-embed figcaption .external-icon { flex-shrink: 0; }

/* ---------- Give page ---------- */
/* Several fieldsets here exist only to group radios for screen readers, not
   to draw a box — this drops the default chrome without inline styles. */
.bare-fieldset {
  border: none;
  padding: 0;
  margin: 0 0 var(--space-5);
}
/* The wrapping fieldset owns the gap to the next block, so the control inside
   it doesn't add a second one — together they were pushing 56px between the
   frequency toggle and the amount grid. */
.bare-fieldset > .freq-toggle { margin-bottom: 0; }

.give-intro {
  color: var(--ink-soft);
  font-size: 0.95rem;
  margin-top: calc(var(--space-4) * -0.5);
}

.give-footnote {
  text-align: center;
  color: var(--ink-faint);
  margin-top: var(--space-8);
}

/* The page's opening beat (heading, intro line, then each gate button)
   fades and lifts in one after another rather than all at once — "both"
   keeps each element invisible until its own delay is up, instead of
   flashing at full opacity before the animation starts. */
@keyframes give-gate-fade-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}
.form-card > h2:first-of-type,
.give-intro,
.give-gate button {
  animation: give-gate-fade-in 0.5s ease-out both;
}
.form-card > h2:first-of-type { animation-delay: 0s; }
.give-intro { animation-delay: 0.1s; }
.give-gate button:nth-child(1) { animation-delay: 0.2s; }
.give-gate button:nth-child(2) { animation-delay: 0.3s; }

.give-rule {
  border: 0;
  border-top: 1px solid var(--border);
  margin-block: var(--space-6);
}

/* The very first choice on the page — United States leads straight out to
   PayPal, the other option reveals the Paystack form below (see
   initGivePage). Two full-width buttons rather than a form field, since
   nothing else on the page is usable yet until one is picked. */
.give-gate {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-3);
}
@media (max-width: 560px) { .give-gate { grid-template-columns: 1fr; } }

.amount-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--space-3);
  margin-bottom: var(--space-5);
}
@media (max-width: 480px) { .amount-grid { grid-template-columns: repeat(2, 1fr); } }

.amount-option { position: relative; }
.amount-option input { position: absolute; opacity: 0; inset: 0; cursor: pointer; }
.amount-option label {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0.95em 0.5em;
  border-radius: var(--radius-sm);
  border: 1.5px solid var(--border);
  background: var(--bg);
  font-weight: 700;
  font-size: 1.05rem;
  cursor: pointer;
  transition: border-color var(--transition), background var(--transition), color var(--transition);
}
.amount-option input:checked + label {
  border-color: var(--gold);
  background: #fbf3de;
  color: var(--gold-deep);
}
.amount-option input:focus-visible + label {
  outline: 3px solid var(--terracotta);
  outline-offset: 2px;
}

.freq-toggle {
  display: inline-flex;
  border: 1.5px solid var(--border);
  border-radius: var(--radius-pill);
  padding: 4px;
  margin-bottom: var(--space-6);
}
.freq-toggle input { position: absolute; opacity: 0; }
.freq-toggle label {
  padding: 0.55em 1.2em;
  border-radius: var(--radius-pill);
  font-weight: 600;
  font-size: 0.9rem;
  cursor: pointer;
  color: var(--ink-soft);
}
.freq-toggle input:checked + label { background: var(--dark); color: #fdf9ef; }
.freq-toggle input:focus-visible + label { outline: 3px solid var(--terracotta); outline-offset: 2px; }

/* ---------- Message of the Week ---------- */
.featured-message {
  position: relative; /* anchors .message-download--corner */
  display: grid;
  grid-template-columns: 0.9fr 1.1fr;
  gap: var(--space-7);
  align-items: center;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  box-shadow: var(--shadow-sm);
}
@media (max-width: 860px) { .featured-message { grid-template-columns: 1fr; } }

/* A quick pulse whenever a past message is promoted into this card (see
   js/main.js's renderFeaturedMessage) — the content swap is otherwise
   instant, so this is what tells a visitor their click actually did
   something. Re-added fresh on every swap (see the class being removed
   and forced to reflow before re-adding) so it replays even on back-to-
   back clicks, not just the first one. */
@keyframes featured-message-swap {
  from { opacity: 0.4; transform: translateY(6px); }
  to { opacity: 1; transform: translateY(0); }
}
.featured-message.is-swapping { animation: featured-message-swap 0.35s ease-out; }

.featured-media { display: flex; flex-direction: column; gap: var(--space-4); }
/* Message thumbnails are often tall portraits (composer paintings, headshots)
   getting cropped into this wide 16:9 frame — object-fit: cover's default
   center crop cuts off the top of the head, since a portrait's subject
   usually isn't at the image's exact vertical middle. Biasing toward the
   top and left keeps the speaker's face in frame instead.

   Note the horizontal half only bites on images WIDER than this 16:9 frame.
   A tall portrait cropped by object-fit: cover fills the width exactly, so
   there's no horizontal overflow left to pan across. */
[data-featured-thumb] img { object-position: 40% 10%; }

/* A plain embed sitting directly on the page background reads as
   unfinished — the card gives it the same surface/border treatment as
   everything else nearby so it looks like a designed part of the page. */
.audio-bar {
  background: var(--bg-alt);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  padding: var(--space-3);
}
.message-audio { width: 100%; display: block; }

.message-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}
.message-row {
  display: flex;
  gap: var(--space-4);
  align-items: center;
  padding: var(--space-4);
  border: 1px solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  transition: border-color var(--transition), box-shadow var(--transition);
}
.message-row:hover { border-color: var(--gold-bright); box-shadow: var(--shadow-sm); }
.message-row-meta { flex: 1; min-width: 0; }
.message-row-meta h4 { margin-bottom: 2px; display: flex; align-items: center; gap: var(--space-2); flex-wrap: wrap; }
.message-row-meta p { margin: 0; color: var(--ink-faint); font-size: 0.88rem; }
/* Flags the row that's actually this week's featured message whenever it's
   the one bumped into the archive (see js/main.js) — hidden by default,
   shown only on that one row. */
/* The [hidden] rule has to come with the display it's overriding: JS hides
   this flag on every archive row but the featured slot's own (see
   renderMessageArchive), and a bare display:inline-flex would otherwise
   beat the browser's default [hidden] styling and show it on all of them. */
.featured-flag[hidden] { display: none; }
.featured-flag {
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  color: var(--gold);
  font-size: 0.78rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}
.featured-flag svg { flex-shrink: 0; }
/* The play glyph riding in the "Listen" button is decorative, matching the
   one on the featured photo — sits a hair left of the label instead of
   dead-center against it. */
.message-row .btn svg { flex-shrink: 0; }
/* Listen and Download sit side by side at every width. They used to stack on
   narrow screens, from when Download carried a text label — but now that it's
   an icon the pair is narrow enough to stay in a row, and stacking made the
   square .btn--icon stretch to full width. */
.message-row-actions {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  flex-shrink: 0;
}
/* aspect-ratio sizes from the width, so an icon button must never be told to
   stretch — pin the box instead of letting a block/full-width rule inflate it. */
.message-row-actions .btn--icon {
  width: 34px;
  height: 34px;
  flex: 0 0 auto;
  padding: 0;
}
/* Icon-only buttons: square rather than pill-shaped, since there's no label
   to give them width. The label lives in aria-label/title instead. */
.btn--icon {
  padding-inline: var(--space-2);
  aspect-ratio: 1;
  justify-content: center;
}
.message-download {
  align-self: flex-start;
  text-decoration: none;
}
.message-download[hidden] { display: none; }

/* Pinned to the featured card's bottom-right corner rather than sitting on
   its own line under the player, where it pushed the media column taller
   than the text beside it. Absolute, so it never affects the grid's height. */
.message-download--corner {
  position: absolute;
  right: var(--space-4);
  bottom: var(--space-4);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  border-radius: var(--radius-sm);
  color: var(--ink-faint);
  background: var(--bg-alt);
  border: 1px solid var(--border);
  transition: color var(--transition), border-color var(--transition);
}
.message-download--corner:hover { color: var(--gold); border-color: var(--gold); }

/* ---------- Loading state ---------- */
/* visibility rather than display: the card keeps its real height, so the
   page doesn't jolt when the message drops in. */
.featured-message.is-loading > *:not(.card-loading) { visibility: hidden; }
.card-loading[hidden] { display: none; }
.card-loading {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  color: var(--ink-faint);
  font-size: 0.92rem;
}
.spinner {
  width: 26px;
  height: 26px;
  border: 2px solid var(--border);
  border-top-color: var(--gold);
  border-radius: 50%;
  animation: spinner-spin 0.7s linear infinite;
}
@keyframes spinner-spin { to { transform: rotate(360deg); } }
.card-notice[hidden] { display: none; }
.card-notice {
  margin-top: var(--space-3);
  padding: var(--space-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm, 8px);
  background: var(--surface-alt, rgba(0, 0, 0, 0.03));
  color: var(--ink-faint);
  font-size: 0.9rem;
}
/* A spinning ring is exactly the kind of motion this setting is meant to
   stop — the ring still marks the wait, it just pulses instead. */
@media (prefers-reduced-motion: reduce) {
  .spinner { animation: spinner-fade 1.2s ease-in-out infinite; }
  @keyframes spinner-fade { 50% { opacity: 0.35; } }
}
/* The archive's own loading row, so the list doesn't sit empty meanwhile. */
.message-row--loading {
  justify-content: center;
  gap: var(--space-3);
  color: var(--ink-faint);
}

/* The home page's Message of the Week card is an <a>, and an <a> can't hold
   another one — so the download icon is a sibling pinned over the card's
   bottom-right corner by this wrapper. */
.carousel-link-wrap { position: relative; display: flex; }
.carousel-link-wrap .carousel-link-btn { flex: 1; }
.card-download {
  position: absolute;
  right: var(--space-2);
  bottom: var(--space-2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  border-radius: var(--radius-sm);
  color: var(--ink-faint);
  background: var(--bg-alt);
  border: 1px solid var(--border);
  transition: color var(--transition), border-color var(--transition);
}
.card-download:hover { color: var(--gold); border-color: var(--gold); }
.card-download[hidden] { display: none; }

/* ---------- Footer ---------- */
/* Dark, regardless of what light/dark section precedes it. A thin gold
   seam (below, on .site-footer's top edge) marks the break instead of
   relying on a background-color change to read as intentional. */
.site-footer {
  background: var(--dark);
  color: rgba(244, 237, 224, 0.82);
  padding-block: var(--space-8) var(--space-6);
  border-top: 1px solid rgba(212, 175, 55, 0.35);
}
.footer-grid {
  display: grid;
  grid-template-columns: 1.3fr 1fr 1fr 1fr 1fr;
  gap: var(--space-6);
}
@media (max-width: 980px) {
  .footer-grid { grid-template-columns: 1fr 1fr 1fr; }
}
@media (max-width: 620px) {
  .footer-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 420px) {
  .footer-grid { grid-template-columns: 1fr; }
}
.footer-brand { display: flex; align-items: center; gap: var(--space-3); margin-bottom: var(--space-4); }
.footer-brand .brand-name { color: #fdf9ef; }
.footer-brand .brand-name small { color: rgba(244, 237, 224, 0.55); }
.footer p { color: rgba(244, 237, 224, 0.7); }
.footer-col h4 {
  color: #fdf9ef;
  font-family: var(--font-body);
  font-size: 0.82rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  margin-bottom: var(--space-4);
}
.footer-col ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--space-3); }
.footer-col ul li { margin-top: 0; }
.footer-col a {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: rgba(244, 237, 224, 0.78);
  text-decoration: none;
  font-size: 0.92rem;
}
.footer-col a:hover { color: var(--gold-bright); }
/* .footer-col a is inline-flex, which makes each link one atomic box: if it
   doesn't fit in what's left of the line after its "Nigeria:" label, the whole
   box drops to the next line rather than wrapping inside the number. The
   Nigerian number is long enough to trigger that where the US one isn't, so
   the label was left stranded on its own line. Keeping label and number
   together fixes it. (The number itself was already unbreakable — see the
   tel: rule near the top of this file — which is why it dropped whole.) */
.footer-col li:has(a[href^="tel:"]) { white-space: nowrap; }
.footer-col a .external-icon { color: rgba(244, 237, 224, 0.45); flex-shrink: 0; }
.footer-col .placeholder-text { color: rgba(244, 237, 224, 0.45); font-size: 0.88rem; }

.footer-bottom {
  margin-top: var(--space-7);
  padding-top: var(--space-5);
  border-top: 1px solid rgba(244, 237, 224, 0.14);
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-3);
  font-size: 0.85rem;
  color: rgba(244, 237, 224, 0.55);
}

/* ---------- Misc utility ---------- */
.text-center { text-align: center; }
.mt-0 { margin-top: 0; }
.stack-sm > * + * { margin-top: var(--space-3); }
