/* ==========================================================================
   GRUMLAW — COMPONENTS
   Every component is namespaced by a single root class and depends on no
   sibling or parent, so each one lifts cleanly into an Elementor saved
   template / global widget. See docs/elementor-port.md
   ========================================================================== */

/* ==========================================================================
   BUTTON
   Pill geometry is the brand's. Hover moves the ground, not the geometry —
   no scale transforms, no shadow bloom.
   ========================================================================== */
.btn {
  --btn-bg: transparent;
  --btn-fg: var(--text-primary);
  --btn-border: transparent;
  --btn-bg-hover: var(--surface-hover);

  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  padding: 0.75rem 1.5rem;
  min-height: 2.75rem;            /* 44px — pointer target floor */
  border: var(--border-width) solid var(--btn-border);
  border-radius: var(--radius-pill);
  background-color: var(--btn-bg);
  color: var(--btn-fg);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  /* Type matches the header exactly: 500 with --tracking-ui, the same pair
     .nav__link sets. A button is a label on a shape, so the shape already
     does the emphasis 700 was doing — and bold at 14px next to a medium
     16px header read as two different UI voices on the same page. */
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-ui);
  line-height: 1;
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out),
              border-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.btn:hover { background-color: var(--btn-bg-hover); }
.btn:active { transform: translateY(1px); }
.btn[disabled], .btn[aria-disabled="true"] {
  opacity: 0.45;
  pointer-events: none;
}

.btn--primary {
  --btn-bg: var(--surface-accent);
  --btn-fg: var(--text-on-accent);
  --btn-bg-hover: var(--surface-accent-hover);
}
.btn--secondary {
  --btn-bg: var(--surface-inverse);
  --btn-fg: var(--text-on-inverse);
  /* Hover lifts the ground toward the label, not toward sage. Mixing in the
     accent tinted the ink button green on hover, which read as a second brand
     colour appearing under the cursor. Mixing in --text-on-inverse keeps the
     button neutral and stays correct when the pair flips in dark mode — the
     same move .panel--accent .btn--secondary already makes with its pinned
     ink/stone-50 pair. */
  --btn-bg-hover: color-mix(in srgb, var(--surface-inverse) 86%, var(--text-on-inverse));
}
.btn--outline {
  /* A control edge, not a hairline — see --border-control. */
  --btn-border: var(--border-control);
  --btn-bg-hover: var(--surface-hover);
}

/* Glass — the header's secondary action, matching the hero's "Watch Live".
   It reads as frosted only where there is something behind it to frost, so it
   falls back to a plain tinted fill where backdrop-filter is unavailable. */
.btn--glass {
  --btn-bg: color-mix(in srgb, var(--surface-inverse) 12%, transparent);
  --btn-fg: var(--text-primary);
  --btn-border: transparent;
  --btn-bg-hover: color-mix(in srgb, var(--surface-inverse) 20%, transparent);
  -webkit-backdrop-filter: blur(10px) saturate(1.4);
          backdrop-filter: blur(10px) saturate(1.4);
}
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .btn--glass { --btn-bg: var(--surface-sunken); --btn-border: transparent; }
}
.btn--ghost { --btn-bg-hover: var(--surface-hover); }

/* For placement on the ink media cards, where page tokens do not apply. */
.btn--on-media {
  /* No border. The blur and tint already separate it from whatever is behind;
     an outline on top of that reads as a second, competing edge. The fill is
     carried slightly higher than it would be with a border to keep the shape
     legible on its own. */
  --btn-bg: rgb(250 250 249 / 0.18);
  --btn-fg: var(--gl-stone-50);
  --btn-border: transparent;
  --btn-bg-hover: rgb(250 250 249 / 0.28);
  -webkit-backdrop-filter: blur(10px) saturate(1.4);
          backdrop-filter: blur(10px) saturate(1.4);
}

.btn--lg { padding: 1rem 2rem; min-height: 3.25rem; font-size: var(--text-base); }
.btn--sm { padding: 0.5rem 1rem;  min-height: 2.25rem; font-size: var(--text-xs); }
/* The header's own actions: 16px at a 44px target, per the live site. The
   weight and tracking now come from .btn itself — the whole site matches
   the header, so the header no longer has to opt out of the default. */
.header__actions .btn {
  font-size: var(--text-base);
  min-height: 2.75rem;
  padding: 0.25rem 1rem;
}
.btn--block { display: flex; width: 100%; }

.btn__icon { width: 1em; height: 1em; flex: none; }

/* Text link with a travelling arrow — the site's tertiary action. The arrow
   moves on hover; nothing else does. */
.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  color: var(--text-accent);
  --uline-color: var(--accent-mark);
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  text-decoration: none;
}
.link-arrow::after {
  content: "";
  width: 1em; height: 1em;
  background-color: currentColor;
  -webkit-mask: var(--icon-arrow) center / contain no-repeat;
          mask: var(--icon-arrow) center / contain no-repeat;
  transition: transform var(--dur-fast) var(--ease-out);
}
.link-arrow:hover { text-decoration: underline; }
.link-arrow:hover::after { transform: translateX(3px); }

:root {
  /* Lucide arrow-right, same geometry as ICONS["arrow"]. URL-encoded, which is
     why the original sweep to Lucide missed it. */
  --icon-arrow: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M5 12h14'/%3E%3Cpath d='m12 5 7 7-7 7'/%3E%3C/svg%3E");
}

/* ==========================================================================
   TAG
   ========================================================================== */
.tag {
  font-family: var(--font-condensed);
  display: inline-flex;
  align-items: center;
  gap: var(--space-3xs);
  padding: 0.25rem 0.5rem;
  border-radius: var(--radius-xs);
  background-color: var(--surface-sunken);
  color: var(--text-secondary);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
}
.tag--accent {
  background-color: var(--surface-accent-subtle);
  color: var(--text-accent);
}
.tag--on-media {
  background-color: rgb(250 250 249 / 0.16);
  color: var(--gl-stone-50);
}

/* ==========================================================================
   ANNOUNCEMENT BAR
   Optional sage band above the header. It scrolls away rather than joining the
   sticky stack: it is an interruption, not chrome, and pinning it would cost
   permanent vertical space and shift every sticky offset on the site
   (--header-h, scroll-padding, the tone band, the menu's top inset).

   Sage is a ground colour here, as everywhere — it carries ink text at 7.17:1
   and does not shift between themes.
   ========================================================================== */
.announce {
  position: relative;
  /* Same layer as the rest of the chrome, one step under the sticky header.
     Without it the bar loses to any section that reaches up behind the chrome
     with a negative margin — the ministry hero does exactly that at -129px,
     and being later in the DOM at z-index:auto it painted straight over the
     whole bar. The symptom was a bar-shaped gap at the top of the page with
     the announcement's own text and CTA underneath it. */
  z-index: calc(var(--z-sticky) - 10);
  background-color: var(--surface-accent);
  color: var(--text-on-accent);
}
[data-announce-hidden] .announce { display: none; }

.announce__inner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  flex-wrap: wrap;
  min-height: 2.75rem;
  padding-block: 0.5rem;
  /* Clear the dismiss control so centred text never crowds or collides with it. */
  padding-inline-end: 3.5rem;
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-ui);
  text-align: center;
}
.announce--bare .announce__inner { padding-inline-end: 0; }

.announce__link {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;                       /* label to arrow */
  margin-inline-start: 0.15em;      /* message to link */
  color: inherit;
  font-weight: var(--weight-bold);
  text-decoration: none;            /* moved to the label span, see below */
}
/* The rule is carried on the label span, not the link: on an inline-flex link
   it would span the arrow too and read as a strikethrough through it. */
.announce__link > span { position: relative; }
.announce__link svg {
  width: 1.05em;
  height: 1.05em;
  flex: none;
  transition: transform var(--dur-fast) var(--ease-out);
}
.announce__link:hover svg { transform: translateX(3px); }

.announce__close {
  position: absolute;
  inset-inline-end: max(var(--space-2xs), calc(var(--gutter) - 0.75rem));
  top: 50%;
  translate: 0 -50%;
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: inherit;
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out);
}
.announce__close:hover { background-color: rgb(40 40 39 / 0.12); }
.announce__close svg { width: 1rem; height: 1rem; }

/* ==========================================================================
   HEADER
   ========================================================================== */
.header {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
  /* Anything in the header that paints from `currentColor` rather than from a
     token resolves it HERE: the chevron inside the disclosure button, the brand
     SVGs. Deriving this from --text-primary rather than restating a literal per
     tone is what keeps them in lockstep with the links, because the token below
     is interpolated and a literal is not — the chevron used to SNAP to its new
     colour on the frame the tone flipped while the labels beside it eased over
     240ms. Declared unconditionally so both tones ride the same ramp. */
  color: var(--text-primary);
  /* Interpolate the tone tokens themselves. Every colour in the header derives
     from these, so this one declaration smooths the wordmark, links, underline,
     both toggles and the glass button together — and keeps them in lockstep,
     which per-element transitions would not guarantee. */
  transition: background-color var(--dur-normal) var(--ease-out),
              --text-primary   var(--dur-normal) var(--ease-out),
              --text-secondary var(--dur-normal) var(--ease-out),
              --text-muted     var(--dur-normal) var(--ease-out),
              --border-subtle  var(--dur-normal) var(--ease-out),
              --border-default var(--dur-normal) var(--ease-out),
              --surface-hover  var(--dur-normal) var(--ease-out);
}

/* ---------------------------------------------------------------------------
   PROGRESSIVE BLUR
   A single masked backdrop-filter does not fade a blur — it fades the *opacity*
   of a uniformly blurred layer, which leaves a visible seam where the mask
   ends. A real gradient of blur needs several layers, each blurring more than
   the last over a shorter region. Stacked, their radii accumulate toward the
   top and taper to nothing past the header's bottom edge, so there is no line
   at all where the header ends.

   The stack extends BELOW the header (bottom: -2.5rem) so the tail has
   somewhere to land; without that the ramp would be cut off mid-fade.
   --------------------------------------------------------------------------- */
.header__blur {
  position: absolute;
  /* Bounded by the header exactly — no overhang. Each layer's mask already
     tapers to fully transparent at the bottom of this box, so the composite
     blur radius reaches zero AT the header's edge and meets the unblurred
     content below continuously. There is no seam to hide, which is what an
     overhang would have been for — and an overhang would instead sit on top of
     the hero card, which is flush against the header.

     Because it resolves to zero on its own, the stack can simply always be on:
     nothing to fade in, nothing to pop, and the first element stays crisp at
     rest on every screen size. */
  inset: 0;
  z-index: -1;
  pointer-events: none;
}
.header__blur > i {
  position: absolute;
  inset: 0;
  -webkit-backdrop-filter: blur(var(--b));
          backdrop-filter: blur(var(--b));
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 var(--hold), transparent var(--end));
          mask-image: linear-gradient(to bottom, #000 0%, #000 var(--hold), transparent var(--end));
}
/* Only the bottom layer carries the saturation lift. Each layer filters the
   result of the one below it, so repeating saturate() four times would compound
   to ~5x. One pass keeps colour alive under the blur, which is what stops the
   frosted area looking milky — it is the same trick native platform materials
   use, and it removes the need for a heavy tint. */
.header__blur > i:nth-child(1) {
  --b: 1px;  --hold: 45%; --end: 100%;
  -webkit-backdrop-filter: blur(var(--b)) saturate(1.6);
          backdrop-filter: blur(var(--b)) saturate(1.6);
}
.header__blur > i:nth-child(2) { --b: 2px;  --hold: 30%; --end: 78%;  }
.header__blur > i:nth-child(3) { --b: 4px;  --hold: 18%; --end: 58%;  }
.header__blur > i:nth-child(4) { --b: 10px; --hold: 6%;  --end: 38%;  }

/* Tint, so text stays legible over whatever scrolls beneath.

   Every stop is the same colour at a different alpha, ending at alpha 0 rather
   than at `transparent` — see the note on --header-scrim. Same hue throughout
   means the ramp cannot shift colour, only presence. The alpha curve is eased
   rather than linear: perceived opacity is not linear in alpha, so a linear
   ramp reads as a visible band.

   TWO scrims are painted, light and dark, and cross-faded by tone. A single
   scrim whose colour variable flipped would snap — gradients cannot transition
   — and a hard swap under a moving hero is exactly the kind of thing that
   catches the eye. Opacity on two stacked layers interpolates cleanly. */
.header__scrim {
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}
.header__scrim::before,
.header__scrim::after {
  content: "";
  position: absolute;
  inset: 0;
  transition: opacity var(--dur-normal) var(--ease-out);
  background: linear-gradient(to bottom,
    rgb(var(--c) / calc(var(--a) * 1))      0%,
    rgb(var(--c) / calc(var(--a) * 0.88))  24%,
    rgb(var(--c) / calc(var(--a) * 0.63))  44%,
    rgb(var(--c) / calc(var(--a) * 0.37))  61%,
    rgb(var(--c) / calc(var(--a) * 0.15))  76%,
    rgb(var(--c) / calc(var(--a) * 0.04))  89%,
    rgb(var(--c) / 0)                     100%);
}
/* Light scrim — carries DARK header content. */
.header__scrim::before { --c: 250 250 249; --a: 0.86; opacity: var(--scrim-light, 1); }
/* Dark scrim — carries LIGHT header content. */
.header__scrim::after  { --c: 12 10 9;     --a: 0.62; opacity: var(--scrim-dark, 0); }

/* ---------------------------------------------------------------------------
   ADAPTIVE TONE
   The header restates its own foreground tokens based on what is passing
   underneath, so contrast is maintained against content rather than against
   the page ground. site.js sets data-tone from sections that declare
   data-tone="dark"; pixel-sampling is not an option here because the hero is a
   cross-origin iframe whose frames cannot be read.
   --------------------------------------------------------------------------- */
[data-theme="dark"] .header,
.header[data-tone="dark"] {
  --scrim-light: 0;
  --scrim-dark: 1;
}
[data-theme="dark"] .header[data-tone="light"] {
  --scrim-light: 1;
  --scrim-dark: 0;
}

.header[data-tone="dark"] {
  --text-primary: var(--gl-stone-50);
  --text-muted: var(--gl-stone-300);
  /* The current-page link draws with --text-accent, which this block used to
     leave alone — so every other link flipped to paper at 14:1 while the link
     for the page you are actually on stayed sage-700 at 2.96:1 on the same
     ground. The one link carrying state was the one that went unreadable. */
  --text-accent: var(--gl-sage-400);
  --border-subtle: rgb(250 250 249 / 0.22);
  --border-default: rgb(250 250 249 / 0.32);
  --border-control: rgb(250 250 249 / 0.55);
  --surface-hover: rgb(250 250 249 / 0.12);
  --focus-ring: var(--gl-sage-400);
}
/* The glass button tints from the inverse surface, which would invert with the
   rest — pin it to a light film so it stays glass and never becomes a slab. */
.header[data-tone="dark"] .btn--glass {
  --btn-bg: rgb(250 250 249 / 0.18);
  --btn-fg: var(--gl-stone-50);
  --btn-bg-hover: rgb(250 250 249 / 0.28);
}
/* Fallback: no backdrop-filter means no blur to ramp, so the header goes solid
   and takes a hairline rule to separate it — the job the blur was doing. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .header__blur { display: none; }
  .header { background-color: var(--surface-page); }
  .header[data-scrolled="true"] { box-shadow: var(--shadow-sticky); }
}

.header__inner {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  min-height: var(--header-h, 72px);
}

.header__brand {
  display: inline-flex;
  align-items: center;
  color: var(--text-primary);
  text-decoration: none;
  flex: none;
}
/* Inline SVG wordmark inherits currentColor, so it needs no theme variant. */
/* ---------------------------------------------------------------------------
   BRAND MARK — scroll crossfade
   The wordmark and the GL monogram are stacked at the same origin and cross-
   faded on scroll. Both are absolutely positioned inside a box sized to the
   WIDER mark, so the swap costs no layout: the nav beside it never shifts,
   which is the whole reason this is not done by toggling `display`.

   Both SVGs share a 154.99 viewBox height, so a single rendered height keeps
   them optically matched — 28px yields 121px and 55px, as on the live site.
   --------------------------------------------------------------------------- */
.brand-mark {
  position: relative;
  display: block;
  width: 121px;          /* the wordmark's width — reserved for both states */
  height: 1.75rem;       /* 28px */
}
.brand-mark > svg {
  position: absolute;
  inset-inline-start: 0;
  top: 50%;
  translate: 0 -50%;
  height: 100%;
  width: auto;
  /* Scale pivots on the shared left anchor — the one point the two marks
     actually have in common — so the swap never drifts horizontally and the
     nav beside it sees nothing move. Individual `scale` composes after
     `translate`, so the -50% centring above survives untouched. */
  transform-origin: left center;
}

/* Resting state: wordmark shown. Because a transition is read off the rule
   the element is arriving AT, these two declarations are simultaneously the
   rest values and the timings for scrolling back to the top — where the
   wordmark is the one entering and the monogram the one leaving. The scrolled
   rules below mirror them exactly. See tokens.css for why the direction is
   carried this way rather than with transition-delay.

   The scale pair is the reason this reads as one gesture instead of two
   fades. grumlaw -> GL is a contraction, so on the way down BOTH marks shrink
   (word 1 -> 0.96, monogram 1.04 -> 1) and the eye follows a single movement
   inward. Reversing the same two values makes both marks grow on the way back
   up, so the return is an expansion without a second set of declarations.
   0.04 is deliberately below the threshold of being noticed as movement — at
   28px tall it is 5px at the wordmark's far edge. It is felt, not seen. */
.brand-mark__word {
  opacity: 1;
  scale: 1;
  transition: opacity var(--dur-brand-in) var(--ease-brand-in),
              scale var(--dur-brand-settle) var(--ease-brand-settle);
}
.brand-mark__icon {
  opacity: 0;
  scale: 1.04;
  transition: opacity var(--dur-brand-out) var(--ease-brand-out),
              scale var(--dur-brand-settle) var(--ease-brand-settle);
}

/* Two independent reasons to show the monogram, OR'd together:
     - the page is scrolled
     - the mobile menu is open (the compact mark suits the menu's header, and
       it reads as a mode change rather than a static logo)

   Nothing latches. `data-scrolled` keeps holding the true scroll position
   while the menu is open — the body is scroll-locked, so no scroll events fire
   and the value cannot drift. Closing the menu drops `data-drawer-open` and the
   mark falls back to whatever the scroll state alone says, animating on the
   same pair of curves. So it returns to the state it was in before, rather
   than being reset to the wordmark. */
.header:is([data-scrolled="true"], [data-drawer-open="true"]) .brand-mark__word {
  opacity: 0;
  scale: 0.96;
  transition: opacity var(--dur-brand-out) var(--ease-brand-out),
              scale var(--dur-brand-settle) var(--ease-brand-settle);
}
.header:is([data-scrolled="true"], [data-drawer-open="true"]) .brand-mark__icon {
  opacity: 1;
  scale: 1;
  transition: opacity var(--dur-brand-in) var(--ease-brand-in),
              scale var(--dur-brand-settle) var(--ease-brand-settle);
}

/* The drawer's brand is static — it is not inside the scrolling header. */
.drawer .header__brand svg { width: auto; height: 1.75rem; }

.header__spacer { flex: 1 1 auto; }

.header__actions { display: flex; align-items: center; gap: var(--space-2xs); }
/* Below the nav breakpoint the header carries only the wordmark, the theme
   control and the menu toggle — as on the live site. Watch and Give are not
   dropped, they are already in the menu, which is where they belong at this
   width; leaving them here overflowed the header and widened the document. */
@media (max-width: 63.999rem) {
  .header__actions > .btn { display: none; }
}

/* --- primary nav -------------------------------------------------------- */
.nav { display: none; }
@media (min-width: 64rem) { .nav { display: block; } }

.nav__list {
  display: flex;
  align-items: center;
  gap: var(--space-3xs);
  list-style: none;
  margin: 0; padding: 0;
}
.nav__link {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.3em;
  padding: 0.5rem 0.25rem;
  color: var(--text-primary);
  font-size: var(--text-base);      /* 16px, per the live header */
  font-weight: var(--weight-medium);/* 500 */
  letter-spacing: var(--tracking-ui);
  text-decoration: none;
  background: none;
  border: 0;
  cursor: pointer;
  /* No `transition: color` here. --text-primary is already interpolated on the
     header, so a transition on this element would chase a moving target and
     land 140ms behind it — and behind every sibling that reads the token
     directly. The lockstep the header comments about only holds if nothing
     re-times the token downstream. Hover changes the underline, not the ink,
     so nothing else needs it. */
}

/* UNDERLINE WIPE — shared by the primary nav and the announcement CTA.
   Wipes in from the left and out to the right: the origin flips on hover so the
   rule never retracts the way it came. scaleX on a pseudo-element stays off the
   layout and on the compositor; an animated border or text-decoration would
   reflow the row on every hover.

   Defined once for both call sites so the two cannot drift. Position is tuned
   per site through --uline-inset / --uline-bottom. */
.nav__link::after,
.announce__link > span::after,
.arrowlink > span::after,
.footer__link--arrow > span::after {
  content: "";
  position: absolute;
  left: var(--uline-inset, 0);
  right: var(--uline-inset, 0);
  bottom: var(--uline-bottom, -0.18em);
  height: 2px;
  /* currentColor by default; components that want the brand rule under ink
     text set --uline-color. */
  background-color: var(--uline-color, currentColor);
  border-radius: 2px;
  transform: scaleX(0);
  transform-origin: right center;
  transition: transform var(--dur-normal) var(--ease-out),
              background-color var(--dur-normal) var(--ease-out);
}
.nav__link:hover::after,
.nav__link:focus-visible::after,
.nav__item[data-open="true"] .nav__link::after,
.announce__link:hover > span::after,
.announce__link:focus-visible > span::after,
.arrowlink:hover > span::after,
.arrowlink:focus-visible > span::after,
.footer__link--arrow:hover > span::after,
.footer__link--arrow:focus-visible > span::after {
  transform: scaleX(1);
  transform-origin: left center;
}
.nav__link { --uline-inset: 0.25rem; --uline-bottom: 0.35rem; }
/* The active link matches its siblings and the sage rule underneath is what
   marks it. --text-primary rather than --text-accent, so this stays correct in
   every combination: ink on the light page, white on a dark tone or in dark
   mode. A sage label sitting beside white ones reads as a different KIND of
   link, not as the current one — and in dark mode it also lost the contrast
   its neighbours had. */
.nav__link[aria-current="page"] { color: var(--text-primary); }
.nav__link[aria-current="page"]::after {
  transform: scaleX(1);
  background-color: var(--accent-mark);
}
.nav__list { gap: var(--space-sm); }

.nav__chevron {
  width: 0.7em; height: 0.7em;
  transition: transform var(--dur-fast) var(--ease-out);
}
.nav__link[aria-expanded="true"] .nav__chevron { transform: rotate(180deg); }

/* --- dropdown ----------------------------------------------------------- */
.nav__item { position: relative; }
.nav__menu {
  position: absolute;
  top: calc(100% + 0.75rem);
  left: 50%;
  translate: -50% 0;
  z-index: var(--z-menu);
  display: grid;
  grid-auto-flow: column;
  /* Three EQUAL columns. Implicit columns (grid-auto-columns) size to their own
     content, which made the "Connect Groups" column wider than the "Kids" one.
     Explicit `1fr` tracks share space equally, and `width: max-content` lets the
     panel grow to fit the widest label — so every column ends up the width of
     that label rather than each column sizing itself. minmax() holds the live
     site's 150px floor when the labels are short. */
  width: max-content;
  grid-template-columns: repeat(3, minmax(9.375rem, 1fr));
  grid-template-rows: repeat(2, auto);
  gap: 0.375rem 1.5rem;                  /* 24px pitch, per the live panel */
  padding: var(--space-lg);
  background-color: var(--gl-black);
  border: 0;
  border-radius: var(--radius-xl);       /* 32px, same as the hero card */
  box-shadow: var(--shadow-menu);
  list-style: none;
  margin: 0;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-6px);
  transition: opacity var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out),
              visibility var(--dur-fast);
}
.nav__item[data-open="true"] .nav__menu {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}
.nav__menu-link {
  display: block;
  min-width: 0;                          /* the grid track sets the width */
  white-space: nowrap;
  padding: 0;
  color: var(--gl-stone-50);
  font-size: 1.25rem;                    /* 20px */
  font-weight: var(--weight-medium);
  line-height: 2;
  letter-spacing: var(--tracking-ui);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-out);
}
.nav__menu-link:hover { color: var(--gl-sage); }
/* On a Kids page the hover takes the star rather than the sage, so the menu
   belongs to the palette the rest of the page is using. The panel is
   --gl-black in BOTH appearances, so one value serves both: the star reads
   15.9:1 on it, where the sub-brand's grass — the obvious "accent" answer —
   would have been 3.3:1 and unreadable at 20px regular. */
.theme-kids .nav__menu-link:hover { color: var(--kid-sun-300); }

/* --- appearance control -------------------------------------------------- */
/* Three real states, not two. "System" is the default and is a distinct choice
   from light or dark — it means "no stored preference", so the page keeps
   following the OS as it changes. A two-way toggle cannot express it, and once
   a visitor flips such a toggle they can never get back to following the OS. */
.theme-control {
  border: 0;
  margin: 0;
  padding: 0;
  min-width: 0;
}
.theme-control__group {
  position: relative;
  display: inline-flex;
  padding: 3px;
  border-radius: var(--radius-pill);
  background-color: rgb(250 250 249 / 0.08);
}
/* A single thumb that slides between the three positions, rather than the
   selected cell simply lighting up. One moving object reads as one control;
   three independently highlighting cells read as three. Neutral, not sage —
   sage is reserved for primary actions, and this is a preference, not a CTA. */
.theme-control__group::before {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 2rem;
  height: 2rem;
  border-radius: var(--radius-pill);
  background-color: rgb(250 250 249 / 0.16);
  transition: translate var(--dur-toggle) var(--ease-toggle);
}
.theme-control__group:has(.theme-control__option:nth-child(2) input:checked)::before {
  translate: 2rem 0;
}
.theme-control__group:has(.theme-control__option:nth-child(3) input:checked)::before {
  translate: 4rem 0;
}
/* Without :has() the thumb would sit under "System" permanently and mislead,
   so it is simply not drawn and the checked cell carries its own fill. */
@supports not selector(:has(*)) {
  .theme-control__group::before { display: none; }
  .theme-control__option input:checked + span { background-color: rgb(250 250 249 / 0.16); }
}
.theme-control__option { position: relative; }
.theme-control__option input {
  position: absolute;
  opacity: 0;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  cursor: pointer;
}
.theme-control__option span {
  display: grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  border-radius: var(--radius-pill);
  color: color-mix(in srgb, var(--text-on-inverse) 68%, transparent);
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.theme-control__option svg { width: 1rem; height: 1rem; }
.theme-control__option:hover span { color: var(--text-on-inverse); }
.theme-control__option span { position: relative; }   /* sit above the thumb */
.theme-control__option input:checked + span { color: var(--gl-stone-50); }
/* Selected state fills the glyph in. Lucide is a stroke set, so "on" is the
   same drawing with its interior painted rather than a different icon — the
   shape never changes, only its weight, which is what makes the control read
   as one object changing state instead of two glyphs swapping. The open paths
   (the sun's rays, the monitor's stand) have no interior and stay linework, so
   only the meaningful mass fills: the sun's disc, the moon, the screen.

   fill-opacity, not fill. Animating fill up from `transparent` interpolates
   from transparent BLACK and drags every midtone through grey on the way to
   white — the same trap the gradients hit. fill-opacity is a plain number with
   no colour to get wrong.

   The 60ms delay is the whole micro-animation: the thumb leaves first and the
   fill lands just after it arrives, so the sequence reads as cause and effect
   rather than two things starting at once. */
.theme-control__option svg {
  fill: currentColor;
  fill-opacity: 0;
  transition: fill-opacity var(--dur-toggle) var(--ease-toggle) 60ms,
              transform var(--dur-toggle) var(--ease-toggle);
}
.theme-control__option input:checked + span svg {
  fill-opacity: 1;
  transform: scale(1.08);
}
/* Press feedback beats the fill: no delay, and it wins while held. */
.theme-control__option:active span svg {
  transform: scale(0.9);
  transition-delay: 0ms;
}

@media (prefers-reduced-motion: reduce) {
  .theme-control__group::before,
  .theme-control__option svg { transition: none; }
}
.theme-control__option input:focus-visible + span {
  outline: var(--border-width-thick) solid var(--focus-ring);
  outline-offset: 2px;
}

/* --- mobile drawer ------------------------------------------------------ */
/* ---------------------------------------------------------------------------
   MENU TOGGLE
   Not a hamburger. A rounded capsule containing a vertical rule whose visible
   length animates: a short stub when closed, a full-height divider when open —
   a panel-toggle metaphor rather than three stacked lines.

   The line is one <path> with pathLength="22.6667", which normalises its length
   so stroke-dasharray values map directly to the icon's own coordinate system.
   The stub is a dash of 8 pushed into place by a negative dashoffset; opening
   grows the dash to the full 22.6667 and slides the offset to 0 while the whole
   path translates back to centre. Geometry and timing taken from the live site.
   --------------------------------------------------------------------------- */
.nav-toggle {
  display: inline-grid;
  place-items: center;
  width: 2.75rem; height: 2.75rem;
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-primary);
  cursor: pointer;
  line-height: 0;
}
@media (min-width: 64rem) { .nav-toggle { display: none; } }
.nav-toggle svg { width: 2rem; height: 2rem; display: block; }

.toggle-icon__line {
  transform: translateX(-4.6667px);
  stroke-dasharray: 8 1000;
  stroke-dashoffset: -7.3333;
  transition: transform var(--dur-toggle) var(--ease-toggle),
              stroke-dasharray var(--dur-toggle) var(--ease-toggle),
              stroke-dashoffset var(--dur-toggle) var(--ease-toggle);
}
[aria-expanded="true"] .toggle-icon__line,
.nav-toggle--open .toggle-icon__line {
  transform: none;
  stroke-dasharray: 22.6667 1000;
  stroke-dashoffset: 0;
}

/* The header floats ABOVE the open menu, so the toggle is one persistent
   button that animates stub -> divider in place. Two separate open/close
   buttons would each render in a fixed state and the animation would never
   actually play — it would just cut. The brand rides along, which is also what
   the live site does. */
.header[data-drawer-open="true"] {
  z-index: calc(var(--z-modal) + 1);
  --text-primary: var(--gl-stone-50);
  color: var(--gl-stone-50);
}
.header[data-drawer-open="true"] .header__blur,
.header[data-drawer-open="true"] .header__scrim { opacity: 0; }

/* ---------------------------------------------------------------------------
   MOBILE MENU
   A dark full-screen panel in BOTH themes — it is a mode, not a surface, so it
   does not follow the page ground. Tokens are re-pointed at its root rather
   than colours being hardcoded on each child.

   Structurally it is a NAVIGATION STACK, not an accordion. The root list is one
   pane; a parent pushes its own pane in over it from the right while the root
   slides a quarter of the way out to the left, and a back control reverses it.
   The reason is not decoration: an accordion pushes everything below it down
   the page, so opening Ministries moves Locations under the reader's thumb
   mid-reach. A pane leaves the rows it replaced exactly where they were.
   --------------------------------------------------------------------------- */
.drawer {
  position: fixed;
  inset: 0;
  z-index: var(--z-modal);
  display: grid;
  grid-template-rows: 1fr;
  /* Grid items default to min-width:auto and refuse to shrink below their
     content, so the track sized to the padded containers' max-content and the
     panel grew wider than the viewport. Pinning the column to minmax(0,1fr)
     lets it shrink; min-width:0 on the children stops the same thing recurring
     one level down. */
  grid-template-columns: minmax(0, 1fr);
  background-color: var(--gl-stone-950);
  color: var(--gl-stone-50);

  --text-primary:   var(--gl-stone-50);
  --text-secondary: var(--gl-stone-300);
  --text-muted:     var(--gl-stone-400);
  /* Sage is a light-value green: on this ground it is the accent that reads,
     and --text-accent's light-theme value (ink) would vanish into it. */
  --text-accent:    var(--gl-sage-400);
  --border-subtle:  rgb(250 250 249 / 0.12);
  --border-control: rgb(250 250 249 / 0.42);
  --surface-hover:  rgb(250 250 249 / 0.12);
  --focus-ring:     var(--gl-sage-400);

  /* Clears the floating header so the menu's own content starts below it. */
  padding-top: var(--header-h);

  /* The base state is CLOSED, and `data-state` drives both directions, because
     the menu has to animate OUT as well as in — which an @keyframes entrance
     cannot do: hiding the node mid-animation just cuts. site.js unhides, forces
     one layout, then sets data-state="open"; on the way out it sets "closing"
     and only hides once the fade has actually finished. */
  opacity: 0;
  transition: opacity var(--dur-menu-out) var(--ease-in-out);
}
/* The ground arrives on --dur-normal rather than the entrance duration on
   purpose: the header floats ABOVE the menu and interpolates its own tone
   tokens over exactly that, so the two cross-fade in lockstep. A slower ground
   would leave the brand and toggle already paper-white over a page that is
   still light. The 420ms belongs to the rows settling, not to the ground. */
.drawer[data-state="open"] {
  opacity: 1;
  transition: opacity var(--dur-normal) var(--ease-out);
}
.drawer[hidden] { display: none; }
.drawer > * { min-width: 0; }

/* The ground fades; the CONTENT carries the movement — it drops in from under
   the header, which is where the toggle that summoned it lives. Moving the
   whole panel instead would drag its edges across the viewport. */
.drawer__viewport {
  position: relative;
  overflow: hidden;
  transform: translateY(-12px);
  transition: transform var(--dur-menu-in) var(--ease-out);
}
.drawer[data-state="open"] .drawer__viewport { transform: none; }
.drawer[data-state="closing"] .drawer__viewport {
  transition: transform var(--dur-menu-out) var(--ease-in-out);
}

/* --- the pane stack ----------------------------------------------------- */
/* Panes are stacked in the same box and slide past each other. Each is opaque,
   so the incoming one covers the outgoing one instead of the two ghosting
   through each other, and each scrolls independently. */
.drawer__pane {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  background-color: var(--gl-stone-950);
  transition: transform var(--dur-pane) var(--ease-nav),
              opacity   var(--dur-pane) var(--ease-nav);
}
/* Parallax, not replacement: the root travels a QUARTER of the distance the
   sub-pane travels, and dims. Two layers moving at different rates is what
   makes one read as being behind the other. */
.drawer[data-pane="sub"] .drawer__pane--root {
  transform: translateX(-25%);
  opacity: 0.4;
}
.drawer__pane--sub {
  z-index: 1;
  transform: translateX(100%);
  /* visibility keeps a parked pane out of hit-testing and off the accessibility
     tree, but it is delayed by the full duration so the pane is still painted
     while it slides away — flipping it at the start would make the exit a cut. */
  visibility: hidden;
  transition: transform var(--dur-pane) var(--ease-nav),
              visibility 0s linear var(--dur-pane);
}
.drawer__pane--sub[data-active="true"] {
  transform: none;
  visibility: visible;
  transition: transform var(--dur-pane) var(--ease-nav), visibility 0s;
}

/* --- entrance stagger ---------------------------------------------------- */
/* Rows resolve in sequence on the way IN. `--i` is set in the markup, where the
   order is known, so nothing has to be counted out at runtime. */
.drawer [data-drawer-stagger] {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity var(--dur-menu-in) var(--ease-out),
              transform var(--dur-menu-in) var(--ease-out);
  transition-delay: calc(60ms + var(--i, 0) * var(--stagger-menu));
}
.drawer[data-state="open"] [data-drawer-stagger] { opacity: 1; transform: none; }
/* The exit is ONE movement, not eleven. The rows hold the state they arrived
   in and the panel takes the whole group with it; playing the stagger backwards
   would make dismissing the menu the slowest thing in the interface. */
.drawer[data-state="closing"] [data-drawer-stagger] {
  opacity: 1;
  transform: none;
  transition: none;
}

.drawer__body { padding-block: var(--space-lg) var(--space-2xl); }
.drawer__list { list-style: none; margin: 0; padding: 0; }
.drawer__link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  padding: var(--space-sm) 0;
  border: 0;
  border-bottom: var(--border-width) solid var(--border-subtle);
  background: none;
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-weight: var(--weight-bold);
  font-size: var(--display-sm);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-display);
  text-decoration: none;
  text-align: left;
  -webkit-text-stroke: var(--stroke-display) currentColor;
  cursor: pointer;
}
/* The rule under a row separates it from the NEXT row. The last row has no next
   row, so its rule was closing the list off against whatever followed — and on
   a sub-pane, against nothing at all. */
.drawer__list > li:last-child > .drawer__link { border-bottom: 0; }
/* The chevron leans into the direction of travel on press. It is the only
   pre-emptive movement in the menu, and it lasts 140ms. */
.drawer__chevron {
  width: 0.6em; height: 0.6em;
  flex: none;
  color: var(--text-muted);
  transition: transform var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.drawer__link--parent:hover .drawer__chevron,
.drawer__link--parent:focus-visible .drawer__chevron {
  transform: translateX(4px);
  color: var(--text-primary);
}

/* --- sub-pane ------------------------------------------------------------ */
/* One chrome row above the rows: the control that undoes the push, and a label
   saying where the push landed. The label is deliberately NOT display type —
   the rows below are, and two competing bold lines 2px apart in size read as a
   list with a stray first item rather than as a heading over a list. Condensed
   uppercase at label scale is a different KIND of thing, which is what makes
   the hierarchy legible without making anything bigger. */
.drawer__pane-head {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  min-height: 2.75rem;
  margin-bottom: var(--space-sm);
}
/* Pulled left by its own padding so the ARROW, not the button box, lines up
   with the text column below it. */
.drawer__back {
  display: inline-grid;
  place-items: center;
  flex: none;
  width: 2.75rem; height: 2.75rem;   /* full target, whatever the glyph does */
  margin-left: -0.6875rem;
  border: 0;
  border-radius: var(--radius-pill);
  background: rgb(250 250 249 / 0.08);
  color: var(--text-primary);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out);
}
.drawer__back:hover { background: rgb(250 250 249 / 0.16); }
.drawer__back-icon {
  width: 1.25rem; height: 1.25rem;
  transition: transform var(--dur-fast) var(--ease-out);
}
.drawer__back:hover .drawer__back-icon,
.drawer__back:focus-visible .drawer__back-icon { transform: translateX(-2px); }

.drawer__pane-title {
  margin: 0;
  font-family: var(--font-condensed);
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
}

/* Watch and Give are one decision at the same weight, so they sit on one row.
   Stacked, the second read as a consequence of the first. */
/* Watch and Directions share the top row; Give takes the whole of the next.
   The two neutrals are the same kind of thing — somewhere else to go — so they
   sit at equal weight, and the one action the site spends its accent on is not
   asked to share a row with something it is not comparable to. */
.drawer__footer {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-2xs);
  margin-top: var(--space-lg);
}

.drawer__give { grid-column: 1 / -1; }

/* Directions is the third button, not a separate region — same gap as the two
   above it, and no rule between them. A hairline plus a 3rem gap announced a
   division that isn't there: all three are the same kind of thing. */
.drawer__meta {
  margin-top: var(--space-2xs);
}
/* The pin is the site's own hand-drawn mark, not a Lucide glyph, and it is
   drawn heavier than a stroked icon — so it runs slightly smaller than the
   label beside it rather than matching its cap height. */
.drawer__directions svg { width: 1.05rem; height: 1.05rem; flex: none; }
.drawer__copyright {
  margin-top: var(--space-lg);
  text-align: center;
  font-size: var(--text-xs);
  color: var(--text-muted);
}

/* Reduced motion keeps the STATE CHANGES — a pane still replaces a pane, the
   panel still appears and goes — and drops only the travel. Removing the
   transitions altogether would leave a reader unable to tell whether pressing
   a parent row did anything at all. */
@media (prefers-reduced-motion: reduce) {
  .drawer__viewport { transform: none; }
  .drawer [data-drawer-stagger] { transform: none; transition-delay: 0s; }
  .drawer[data-pane="sub"] .drawer__pane--root { transform: none; opacity: 0; }
  .drawer__pane--sub {
    transform: none;
    opacity: 0;
    transition: opacity var(--dur-fast) linear,
                visibility 0s linear var(--dur-fast);
  }
  .drawer__pane--sub[data-active="true"] {
    transform: none;
    opacity: 1;
    transition: opacity var(--dur-fast) linear, visibility 0s;
  }
  .drawer__chevron, .drawer__back-icon { transition: none; }
}

/* ==========================================================================
   HERO
   The signature element: a single large-radius ink card holding media, with
   content anchored to its lower edge. Not a full-viewport background image.
   ========================================================================== */
/* The card sits flush under the header on the live site — no gap. */
.hero { padding-top: 0; }

.hero__card {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  min-height: var(--hero-h);
  padding: var(--space-lg);
  border: var(--border-width) solid var(--surface-media-border);
  border-radius: var(--radius-xl);
  background-color: var(--surface-media);
  color: var(--text-on-media);
  overflow: hidden;
  isolation: isolate;
}
@media (min-width: 48rem) {
  .hero__card { min-height: var(--hero-h-md); padding: var(--space-xl); }
}
@media (min-width: 64rem) {
  .hero__card { min-height: var(--hero-h-lg); }
}

.hero__media {
  position: absolute;
  inset: 0;
  z-index: -2;
  width: 100%; height: 100%;
  object-fit: cover;
}

/* Background video layer. Sits above the poster and below the gradient, so the
   poster carries first paint (and stays the fallback) while the video fades in
   on top of it. The poster is the video's own first frame, so the swap is
   invisible rather than a visible cut.

   Cover-fitting a fixed 16:9 embed inside an arbitrary box needs container
   query units: the iframe is grown along whichever axis is short. `inset: 0`
   gives this wrapper a definite size, so `container-type: size` is safe here —
   it would break the card itself, whose height is intrinsic. */
.hero__video {
  position: absolute;
  inset: 0;
  z-index: -2;
  overflow: hidden;
  container-type: size;
  pointer-events: none;
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease-out);
}
.hero__video[data-ready="true"] { opacity: 1; }
/* The layer has to exist BEFORE the fade starts, not be created by it.

   Safari promotes whatever overlaps a compositing animation when the animation
   begins — and it got the hero's text wrong while doing so: for the ~500ms of
   this transition the title, eyebrow and meta line painted dim grey on the dark
   card, while getComputedStyle reported rgb(250 250 249) at opacity 1 the whole
   time. Style was never the problem; only the pixels were. Caught by scanning a
   screen recording frame by frame — the strip holding those three lines had no
   pixel brighter than 56/255 for 380ms — and pinned down by logging
   document.getAnimations() through load: this transition is the only thing
   running in that window.

   Declaring will-change here means the layer is already there when the fade
   starts, so nothing is promoted mid-animation and the text is never dragged
   through the re-composite. This element is one viewport-sized box wrapping an
   iframe that is composited anyway — not the multi-thousand-pixel case the reel
   warns about. */
.hero__video { will-change: opacity; }
.hero__video iframe {
  position: absolute;
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  width: max(100cqw, 100cqh * 16 / 9);
  height: max(100cqh, 100cqw * 9 / 16);
  border: 0;
}
/* No autoplaying motion for anyone who has asked not to have it — the poster
   is a complete experience on its own. site.js also declines to inject. */
@media (prefers-reduced-motion: reduce) {
  .hero__video { display: none; }
}

/* Gradient is a legibility tool anchored to the text edge — not decoration.
   It is weighted for the *video*, not the poster: across a 26s loop the footage
   under the headline changes every frame, so contrast cannot be verified once
   and trusted. The scrim is held high through the whole text band and only
   releases in the top quarter, where nothing is set. */
.hero__card::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(to top,
    rgb(12 10 9 / 0.88) 0%,
    rgb(12 10 9 / 0.72) 22%,
    rgb(12 10 9 / 0.42) 48%,
    rgb(12 10 9 / 0.05) 78%);
}

/* Mobile stacks the text over full-width actions; from tablet up the text sits
   left and the actions right, both anchored to the card's lower edge. This is
   the live site's arrangement at each breakpoint. */
.hero__content {
  /* Painted above the media and the scrim regardless — both sit at negative
     z-index — but stated rather than left implied, so the text has a paint
     order of its own instead of being an in-flow box the compositor is free to
     bundle in with the layer fading in behind it. Belt and braces with the
     will-change above; costs nothing and changes nothing at rest. */
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: var(--space-md);
}
@media (min-width: 48rem) {
  .hero__content {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: flex-end;
    justify-content: space-between;
  }
}

/* The title set on one line below the tablet break and stacked above it (the
   job .brk used to do). The masked halves therefore sit side by side at small
   sizes and stack at large, so the responsive behaviour is unchanged and only
   the entrance is new. */
.hero__title .rise__line { display: inline-block; vertical-align: bottom; }
.hero__title .rise__line + .rise__line { margin-left: 0.28em; }
@media (min-width: 48rem) {
  .hero__title .rise__line { display: block; }
  .hero__title .rise__line + .rise__line { margin-left: 0; }
}

.hero__title {
  font-size: var(--text-lg);        /* 18px on mobile, per the live site */
  font-weight: var(--weight-bold);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-ui);
  color: var(--gl-stone-50);
  margin: 0;
}
@media (min-width: 48rem) {
  .hero__title {
    font-size: var(--display-lg);
    line-height: var(--leading-tight);
    letter-spacing: var(--tracking-display);
  }
}

/* One voice: a single line in a single treatment with one separator. The
   previous version ran two type styles, a hairline and two separator systems
   at once, which made a short factual line feel busy. Nothing is emphasised
   over anything else here, because at this size nothing needs to be. */
.hero__meta {
  /* The title sets tight — 1.04 leading with negative tracking — so its last
     line sits hard against its own baseline. Without an explicit gap the meta
     reads as a third line of the heading rather than as separate information.
     The gap grows with the type it separates, or it closes up again at display
     scale. */
  margin-top: var(--space-xs);
  font-size: var(--text-lg);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-ui);
  line-height: var(--leading-snug);
  color: rgb(250 250 249 / 0.82);
  text-wrap: pretty;
}
@media (min-width: 48rem) {
  .hero__meta { margin-top: var(--space-md); }
}
/* The only separator in the line. Dimmed so it divides without competing. */
.hero__meta .sep { margin-inline: 0.5em; opacity: 0.45; }

/* Full-width stacked actions on mobile; intrinsic pills from tablet up. */
.hero__actions { display: flex; flex-direction: column; gap: var(--space-2xs); }
.hero__actions > .btn { width: 100%; }
@media (min-width: 48rem) {
  .hero__actions { flex-direction: row; flex-wrap: wrap; }
  .hero__actions > .btn { width: auto; }
}

/* Compact hero for interior pages — same language, no media.

   The title takes the condensed face in uppercase. That is the second
   sanctioned use of Roboto Condensed after message titles, and it is how the
   live v2 interior pages set their H1 — the condensed face earns a heading
   here because the page name is short and structural, which is exactly what a
   condensed uppercase line is for. Anything longer than about four words
   should go back to the sans. */
.page-hero { padding-block: var(--space-2xl) var(--space-xl); }
.page-hero__title {
  font-family: var(--font-condensed);
  font-weight: var(--weight-bold);
  font-size: clamp(2rem, 1.2rem + 3.6vw, 3.5rem);
  line-height: 1.02;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  text-wrap: balance;
}
/* The hand-drawn GL, as a watermark. A mask rather than a background image, so
   it takes a theme token instead of the flat #fff the source file carries —
   white on near-white paper is invisible in dark mode for the opposite reason
   it is subtle in light. */
/* No overflow clip: the texture deliberately runs UP past this box, behind
   the header. */
.page-hero--mark { position: relative; }
.page-hero--mark::before {
  content: "";
  position: absolute;
  /* Climbs to the top of the HEADER and stops. The header is sticky and holds
     its own place in flow, so the hero begins below it and the pseudo-element
     has to reach back over that distance for the header to overlap the texture
     rather than butt against it — but it stops at the announcement band, which
     is its own surface and should not have a watermark bleeding through it.
     Both heights are measured rather than assumed: the bar can be switched off
     in the build and dismissed by a visitor at runtime. */
  top: calc(-1 * (var(--chrome-h, var(--header-h)) - var(--bar-h, 0px)));
  right: 0;
  bottom: 0;
  left: 0;
  background-color: var(--text-primary);
  opacity: 0.05;
  /* Anchored to the hero's top-right corner. The hero's top edge IS the
     header's bottom, because the header is sticky and holds its own place in
     flow — so `top` starts the texture immediately beneath the chrome rather
     than tucked behind it. Anchored to the HERO rather than to --header-h on
     purpose: with the announcement bar on, the real chrome is 129px, not 85,
     and a header-relative offset would be wrong whenever that bar toggles. */
  /* Sized as a fraction of the hero, so it shrinks with the viewport twice
     over — once because the box narrows and again because the fraction is of
     that narrower box. Raised at the smaller widths to hold its presence. */
  -webkit-mask: url("../img/gl-drawn.svg") right top / 78% no-repeat;
          mask: url("../img/gl-drawn.svg") right top / 78% no-repeat;
  pointer-events: none;
  z-index: 0;
}
@media (min-width: 48rem) {
  .page-hero--mark::before {
    -webkit-mask-size: 60%;
            mask-size: 60%;
  }
}
@media (min-width: 64rem) {
  .page-hero--mark::before {
    -webkit-mask-size: 40%;
            mask-size: 40%;
  }
}
.page-hero--mark > .container { position: relative; z-index: 1; }

.page-hero--center { text-align: center; }
.page-hero--center .container { display: grid; justify-items: center; }
.page-hero__lead { margin-top: var(--space-md); max-width: 62ch; }
.page-hero__actions { margin-top: var(--space-lg); }
.page-hero--center .page-hero__actions { justify-content: center; }

/* ==========================================================================
   CARD
   Flat by default with a 1px border. Hover darkens the border and lifts the
   accent rule — no shadow bloom, no scale.
   ========================================================================== */
.card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  padding: var(--space-md);
  background-color: var(--surface-raised);
  border: var(--border-width) solid var(--border-subtle);
  border-radius: var(--radius-md);
  transition: border-color var(--dur-fast) var(--ease-out),
              background-color var(--dur-fast) var(--ease-out);
}
.card:has(.card__link:hover) { border-color: var(--border-strong); }

/* Stretched-link pattern: the whole card is clickable, the anchor stays the
   only tab stop and keeps its accessible name. */
.card__link { text-decoration: none; color: inherit; }
.card__link::after { content: ""; position: absolute; inset: 0; border-radius: inherit; }
.card__title { font-size: var(--text-xl); font-weight: var(--weight-bold); font-family: var(--font-sans); text-transform: none; letter-spacing: var(--tracking-normal); line-height: var(--leading-snug); }
.card__body { color: var(--text-secondary); font-size: var(--text-sm); line-height: var(--leading-relaxed); }
/* Feature cards that act as a section heading opt back into the display face. */
.card__title--display {
  font-size: var(--display-sm);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-display);
  -webkit-text-stroke: var(--stroke-display) currentColor;
}
.card__foot { margin-top: auto; padding-top: var(--space-xs); }

.card__thumb {
  aspect-ratio: 16 / 10;
  width: 100%;
  object-fit: cover;
  border-radius: var(--radius-sm);
  background-color: var(--surface-sunken);
}

/* Media-led card: image fills, content sits on the ink ground beneath. */
.card--media {
  padding: 0;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background-color: var(--surface-media);
  border-color: var(--surface-media-border);
  color: var(--text-on-media);
}
/* Same treatment on the media cards: fully rounded, so the card shows through
   beneath the thumbnail's lower curve. */
.card--media .card__thumb {
  /* Fixed 16:9 — the ratio is set by the card, never by the image. Whatever is
     supplied gets cropped to fit by object-fit, so a mixed batch of source
     images still produces a row of identical cards. */
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-lg);
}
.card--media .card__content { display: grid; gap: var(--space-2xs);
  justify-items: start; padding: var(--space-md); }
.card--media .card__title { color: var(--gl-stone-50); }
.card--media .card__body  { color: rgb(250 250 249 / 0.72); }

/* ==========================================================================
   NEXT-STEP LIST
   Replaces the usual icon-card triptych. Numbered, ruled, full-width rows —
   it reads as a sequence, which is what the content actually is.
   ========================================================================== */
.steps { border-top: var(--border-width) solid var(--border-subtle); }
.step {
  display: grid;
  gap: var(--space-2xs) var(--space-md);
  grid-template-columns: auto 1fr;
  align-items: start;
  padding-block: var(--space-md);
  border-bottom: var(--border-width) solid var(--border-subtle);
}
@media (min-width: 48rem) {
  .step {
    grid-template-columns: 3.5rem minmax(0, 18rem) minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-lg);
  }
}
/* aria-hidden, decorative, and large — a mark rather than reading text, so it
   takes the brand colour directly. */
.step__num {
  font-family: var(--font-condensed);   /* numeral in a tight column */
  font-size: var(--display-sm);
  line-height: 1;
  color: var(--accent-mark);
}
.step__title { font-size: var(--text-xl); font-weight: var(--weight-bold); font-family: var(--font-sans); text-transform: none; letter-spacing: var(--tracking-normal); line-height: var(--leading-snug); }
.step__body { color: var(--text-secondary); font-size: var(--text-sm); grid-column: 2 / -1; }
@media (min-width: 48rem) { .step__body { grid-column: auto; } }
.step__action { grid-column: 2 / -1; }
@media (min-width: 48rem) { .step__action { grid-column: auto; justify-self: end; } }

/* ==========================================================================
   VALUE — the About page's core values
   ========================================================================== */
.value {
  display: grid;
  gap: var(--space-sm);
  padding-block: var(--space-lg);
  border-top: var(--border-width) solid var(--border-subtle);
}
@media (min-width: 56rem) {
  .value { grid-template-columns: minmax(0, 4fr) minmax(0, 8fr); gap: var(--space-2xl); }
}
.value__title { font-size: var(--display-sm); position: sticky; top: calc(var(--header-h, 72px) + var(--space-md)); align-self: start; }
.value__body { color: var(--text-secondary); line-height: var(--leading-relaxed); }

/* ==========================================================================
   LOCATION CARD
   ========================================================================== */
.location {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: var(--border-width) solid var(--border-subtle);
  border-radius: var(--radius-lg);
  background-color: var(--surface-raised);
}
.location__map {
  aspect-ratio: 3 / 2;
  width: 100%;
  object-fit: cover;
  background-color: var(--surface-sunken);
  border-bottom: var(--border-width) solid var(--border-subtle);
}
.location__body { display: grid; gap: var(--space-xs); padding: var(--space-md); }
.location__name { font-size: var(--display-sm); }
.location__meta { display: grid; gap: var(--space-3xs); font-size: var(--text-sm); color: var(--text-secondary); }
.location__meta dt { font-weight: var(--weight-bold); color: var(--text-muted); font-size: var(--text-2xs); letter-spacing: var(--tracking-label); text-transform: uppercase; }
.location__meta dd { margin: 0 0 var(--space-2xs); }
.location__actions { display: flex; flex-wrap: wrap; gap: var(--space-2xs); margin-top: var(--space-2xs); }

/* ==========================================================================
   ACCORDION
   ========================================================================== */
.accordion { border-top: var(--border-width) solid var(--border-subtle); }
.accordion__item { border-bottom: var(--border-width) solid var(--border-subtle); }
.accordion__trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  width: 100%;
  padding: var(--space-md) 0;
  border: 0;
  background: none;
  color: var(--text-primary);
  font-family: var(--font-sans);
  font-size: var(--text-lg);
  font-weight: var(--weight-bold);
  text-align: left;
  cursor: pointer;
}
.accordion__trigger:hover { color: var(--text-accent); }
.accordion__icon {
  flex: none;
  width: 1.25rem; height: 1.25rem;
  transition: transform var(--dur-normal) var(--ease-out);
}
.accordion__trigger[aria-expanded="true"] .accordion__icon { transform: rotate(45deg); }
.accordion__panel {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--dur-normal) var(--ease-out);
}
.accordion__panel[data-open="true"] { grid-template-rows: 1fr; }
.accordion__panel > div { overflow: hidden; }
.accordion__panel p {
  padding-bottom: var(--space-md);
  max-width: 62ch;
  color: var(--text-secondary);
  line-height: var(--leading-relaxed);
}

/* ==========================================================================
   NOTICE — the dated announcement strip
   ========================================================================== */
.notice {
  display: grid;
  gap: var(--space-sm);
  padding: var(--space-md);
  border-radius: var(--radius-lg);
  background-color: var(--surface-cream);
  color: var(--text-primary);
}
@media (min-width: 48rem) {
  .notice {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    gap: var(--space-lg);
    padding: var(--space-md) var(--space-lg);
  }
}
.notice__title { font-size: var(--text-xl); font-weight: var(--weight-bold); }
.notice__body { color: color-mix(in srgb, var(--text-primary) 72%, transparent); font-size: var(--text-sm); line-height: var(--leading-relaxed); }
/* Grid children stretch by default; the action must stay its intrinsic width. */
.notice > .btn { justify-self: start; }

/* ==========================================================================
   FORM
   ========================================================================== */
.field { display: grid; gap: var(--space-3xs); }
.field__label {
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  color: var(--text-primary);
}
.field__hint { font-size: var(--text-xs); color: var(--text-muted); }
.field__error { font-size: var(--text-xs); color: var(--text-danger); }

.input, .select, .textarea {
  width: 100%;
  padding: 0.7rem 0.875rem;
  min-height: 2.75rem;
  background-color: var(--surface-raised);
  border: var(--border-width) solid var(--border-default);
  border-radius: var(--radius-sm);
  color: var(--text-primary);
  font-size: var(--text-base);
  transition: border-color var(--dur-fast) var(--ease-out);
}
.input::placeholder, .textarea::placeholder { color: var(--text-muted); }
.input:hover, .select:hover, .textarea:hover { border-color: var(--border-strong); }
.textarea { min-height: 7rem; resize: vertical; }
.select {
  appearance: none;
  padding-right: 2.5rem;
  background-image: var(--icon-chevron);
  background-repeat: no-repeat;
  background-position: right 0.875rem center;
  background-size: 1rem;
}
:root { --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2378716C' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); }
[data-theme="dark"] { --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23A8A29E' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); }

.field[data-invalid="true"] .input,
.field[data-invalid="true"] .select,
.field[data-invalid="true"] .textarea { border-color: var(--text-danger); }

.checkbox { display: flex; align-items: flex-start; gap: var(--space-2xs); font-size: var(--text-sm); color: var(--text-secondary); }
.checkbox input {
  flex: none;
  width: 1.15rem; height: 1.15rem;
  margin-top: 0.15rem;
  accent-color: var(--surface-accent);
}

/* ==========================================================================
   FOOTER
   Mirrors the live layout: a wordmark/social row, then four equal columns —
   two of links, two of campus details — then a baseline bar.

   Deliberately NO column headings. The live footer has none, and the link
   groups are short and self-evident enough that labels would add a row of
   chrome without adding meaning.
   ========================================================================== */
/* The footer is a fixed dark surface, NOT --surface-inverse. Inverse flips with
   the theme, so in dark mode the footer inverted to near-white — a bright slab
   at the end of a dark page. It is a constant here, like the sage button
   ground, and takes the darkest tone in the ramp in both themes.

   Because the ground no longer flips, its foreground tokens cannot either:
   they are restated locally so every descendant resolves correctly without
   knowing it is inside the footer. */
.footer {
  background-color: var(--surface-deep);
  color: var(--gl-stone-50);
  padding-block: var(--space-2xl) var(--space-lg);
  margin-top: var(--section-y);

  --text-on-inverse: var(--gl-stone-50);
  --text-primary:    var(--gl-stone-50);
  --text-secondary:  var(--gl-stone-300);
  --text-muted:      var(--gl-stone-400);
  --border-inverse:  rgb(250 250 249 / 0.14);
  --focus-ring:      var(--gl-sage-400);
}
/* The closing section put down its own ground — colour or photograph — so the
   footer's top margin below it is not air between two things, it is a stripe of
   page ground between two solid surfaces, and it reads as a seam. Zeroed here
   so the two grounds meet on a hard line; the section's own bottom padding is
   already the air that margin was providing. Set at build (see
   closes_on_ground()) rather than with :has(), so the answer is visible in the
   markup and does not depend on how a closing block happens to nest. */
.footer--flush { margin-top: 0; }

/* No border needed in dark mode: --surface-deep is already a step below the
   page ground, so the tone difference draws the edge on its own. */

.footer__masthead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  flex-wrap: wrap;
  padding-bottom: var(--space-2xl);
}
.footer__brand svg { width: min(100%, 8.5rem); height: auto; color: var(--text-on-inverse); }

.footer__social { display: flex; gap: 1.25rem; }
.footer__social a {
  display: grid;
  place-items: center;
  width: 2.25rem; height: 2.25rem;
  border-radius: var(--radius-pill);
  color: var(--text-on-inverse);
  background-color: rgb(250 250 249 / 0.08);
  transition: background-color var(--dur-fast) var(--ease-out);
}
.footer__social a:hover { background-color: rgb(250 250 249 / 0.16); }
.footer__social svg { width: 1.125rem; height: 1.125rem; }

/* Four equal columns at desktop, two everywhere below that — including mobile.
   The four children pair off naturally at two columns: the two link lists sit
   side by side on the first row, the two campuses on the second. Stacking them
   one-up on mobile only made the footer a long scroll of half-empty rows. */
.footer__cols {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-xl) var(--space-sm);
}
@media (min-width: 64rem) { .footer__cols { grid-template-columns: repeat(4, 1fr); } }

.footer__list {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  list-style: none;
  margin: 0;
  padding: 0;
}
.footer__link {
  position: relative;
  align-self: start;
  font-size: 1.25rem;              /* 20px, per the live footer */
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-ui);
  color: var(--text-on-inverse);
  text-decoration: none;
}
/* Same underline wipe as the nav and the announcement CTA. */
.footer__link::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  bottom: -0.18em;
  height: 2px;
  background-color: currentColor;
  border-radius: 2px;
  transform: scaleX(0);
  transform-origin: right center;
  transition: transform var(--dur-normal) var(--ease-out);
}
.footer__link:hover::after,
.footer__link:focus-visible::after { transform: scaleX(1); transform-origin: left center; }
/* At two columns on a phone the column is ~148px, and 20px type wraps the
   longest label ("Connect Groups") onto a second line — which then knocks the
   rest of that list out of step with the column beside it. One step down keeps
   every label on one line and the two lists reading as matched rows. */
@media (max-width: 39.999em) { .footer__link { font-size: 1.125rem; } }

/* Link + trailing arrow, as in the announcement bar. The wipe moves onto the
   label span: on an inline-flex link it would span the arrow too and read as a
   strikethrough through it. */
/* Label, sliding rule, arrow — the footer's "Directions" treatment, lifted out
   of the footer so anything can use it. The rule is on the LABEL SPAN, not the
   link: on an inline-flex link it would span the arrow too and read as a
   strikethrough through it. */
.arrowlink,
.footer__link--arrow {
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  font-size: var(--text-sm);
  text-decoration: none;
}
/* link-arrow draws its arrow from a masked ::after; this one uses a real SVG,
   so that pseudo-element must not also fire. */
.arrowlink::after,
.footer__link--arrow::after { display: none; }
.arrowlink > span,
.footer__link--arrow > span { position: relative; }
.arrowlink svg,
.footer__link--arrow svg {
  width: 1.05em; height: 1.05em; flex: none;
  transition: transform var(--dur-fast) var(--ease-out);
}
.arrowlink:hover svg,
.footer__link--arrow:hover svg { transform: translateX(3px); }

/* `> svg` is deliberate: as a descendant selector this also matched the arrow
   inside the nested "Directions" link and blew it up to the location mark's
   size. The child combinator keeps it to the mark itself.

   The mark stacks above the name, as it does on the live site, which gives the
   hand-drawn artwork room to read as artwork rather than as a list bullet.

   align-items is set explicitly because the default `stretch` would make the
   SVG fill the column's full width and scale the artwork with it — the icon
   needs its own size, not the container's. */
.footer__place {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.625rem;
}
.footer__place > svg {
  flex: none;
  width: 2.25rem;
  height: 2.25rem;
}
.footer__place h2 {
  font-family: var(--font-sans);
  font-size: var(--text-lg);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-ui);
  line-height: var(--leading-snug);
  color: var(--text-on-inverse);
  margin-bottom: 0.35rem;
}
.footer__place p {
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  color: color-mix(in srgb, var(--text-on-inverse) 72%, transparent);
}

.footer__bottom {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  justify-content: space-between;
  align-items: center;
  margin-top: var(--space-2xl);
  padding-top: var(--space-md);
  border-top: var(--border-width) solid var(--border-inverse);
  font-size: var(--text-sm);
  color: color-mix(in srgb, var(--text-on-inverse) 62%, transparent);
}


/* ==========================================================================
   BACK TO TOP
   Mobile only, and only when it has been asked for. Two conditions gate it:

   1. DISTANCE. Below one and a half screens, "back to top" is a nudge the
      thumb can do faster than the eye can find a button. The control does not
      exist until the return trip is long enough to be worth automating.

   2. INTENT. Scrolling UP is the gesture that means "I want to go back", so
      that is what summons it; scrolling down again dismisses it. A button
      that simply parks itself after N pixels and stays there for the rest of
      the session is the thing this is deliberately not — it would sit over
      the content on every page, permanently, to serve a rare action.

   The hysteresis that separates the two directions lives in site.js.

   Frosted rather than solid because it floats over live content: a solid
   puck reads as part of the page and has to be dodged, while glass reads as
   a layer above it and lets the reader keep their place underneath.
   ========================================================================== */
.totop {
  position: fixed;
  z-index: var(--z-sticky);
  right: var(--space-sm);
  /* The inset is what keeps it clear of the home indicator; without it the
     control lands under the gesture bar on every notched phone. */
  bottom: calc(var(--space-sm) + env(safe-area-inset-bottom, 0px));

  display: grid;
  place-items: center;
  width: 2.75rem;   /* 44px — the whole control is the target */
  height: 2.75rem;
  padding: 0;
  /* No border. The frost already separates the puck from whatever is scrolling
     beneath it, and a hairline on top of that draws a second edge that fights
     the blurred one — the same call .btn--on-media makes for the same reason.
     A soft drop shadow does the separating instead: it reads as height rather
     than as a drawn outline, which is what a floating control actually is. */
  border: 0;
  border-radius: var(--radius-pill);
  /* Partly transparent on purpose. At full opacity the backdrop-filter has
     nothing to show through and the glass collapses into a plain circle. */
  background: color-mix(in srgb, var(--surface-raised) 68%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(1.6);
          backdrop-filter: blur(14px) saturate(1.6);
  color: var(--text-primary);
  box-shadow: var(--shadow-float);
  cursor: pointer;
  /* A press on a control is a press, not a text selection. */
  -webkit-user-select: none; user-select: none;
  /* Taps land instantly; the 300ms wait is for double-tap zoom this has none
     of, and the grey flash is Android's, not ours. */
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

/* Desktop has the keyboard, a scrollbar and a trackpad flick. The control is
   removed rather than hidden — no markup cost it cannot pay back. */
@media (min-width: 48rem) { .totop { display: none; } }

/* Without the frost there is nothing to see through, so the glass becomes a
   raised surface instead of a translucent one. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .totop { background: var(--surface-raised); }
}

/* Resting state. `visibility` is what takes it out of the tab order while it
   is invisible — opacity alone would leave a focusable ghost — and its
   transition is stepped to the END of the exit so the fade is not cut short.
   The exit is faster than the entrance: arriving should feel considered,
   leaving should feel like it got out of the way. */
.totop {
  opacity: 0;
  visibility: hidden;
  transform: translateY(0.75rem) scale(0.88);
  transition: opacity var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out),
              visibility 0s linear var(--dur-fast);
}
.totop[data-visible] {
  opacity: 1;
  visibility: visible;
  transform: none;
  /* easeOutQuint: most of the travel happens immediately and the last few
     pixels settle. It is what makes the arrival read as arriving rather than
     as appearing. */
  transition: opacity var(--dur-normal) var(--ease-out),
              transform var(--dur-normal) var(--ease-out),
              visibility 0s;
}

/* The arrow lands a beat after the puck it sits in — 60ms, enough to read as
   one movement with a follow-through rather than as two separate ones. */
.totop svg {
  width: 1.125rem;
  height: 1.125rem;
  transform: translateY(0.375rem);
  opacity: 0;
  transition: transform var(--dur-fast) var(--ease-out),
              opacity var(--dur-fast) var(--ease-out);
}
.totop[data-visible] svg {
  transform: none;
  opacity: 1;
  transition: transform var(--dur-normal) var(--ease-out) 60ms,
              opacity var(--dur-fast) var(--ease-out) 60ms;
}

/* Press: the puck takes the pressure, the arrow starts the journey. */
.totop:active {
  transform: scale(0.94);
  transition-duration: var(--dur-fast);
}
.totop:active svg { transform: translateY(-0.125rem); transition-duration: var(--dur-fast); }

/* Nothing else on screen should have to compete with an open menu. */
:root:has([data-header][data-drawer-open]) .totop {
  opacity: 0;
  visibility: hidden;
  transform: translateY(0.75rem) scale(0.88);
}

@media (prefers-reduced-motion: reduce) {
  /* The control still comes and goes — that is state, not decoration — but it
     does so without travel. */
  .totop, .totop[data-visible],
  .totop svg, .totop[data-visible] svg {
    transform: none;
    transition-duration: 1ms;
    transition-delay: 0s;
  }
  .totop:active { transform: none; }
}


/* ==========================================================================
   SECTION NAV
   For a page whose sections ARE its navigation. Tablet and desktop get a
   sticky rail, which is the page's own business; below 48rem they get this —
   a floating pill naming the section you are in, opening a sheet from the
   bottom of the screen. It sits in the thumb's reach rather than under the
   header, and does double duty: a menu when you press it and a position
   indicator when you do not.
   ========================================================================== */

/* Anchors land clear of the floating header AND the announcement bar, so
   --chrome-h rather than base.css's --header-h: the bar is dismissible, and a
   section hiding behind it until someone closes it is a section you cannot
   link to. On a page whose primary navigation IS its anchors this has to be
   exact, and base.css offsets twice — once as scroll-padding on the scroller
   and again as scroll-margin on :target — which stacks to double the header on
   every jump. The padding is retuned and the target margin zeroed, so one rule
   owns the distance. Both are gated on the sheet being in the document, so a
   page without an index keeps the site-wide defaults. */
:root:has([data-secnav-sheet]) {
  scroll-padding-top: calc(var(--chrome-h, 85px) + var(--space-md));
}
:root:has([data-secnav-sheet]) :target { scroll-margin-top: 0; }

/* A page with an index already carries a bottom-anchored control, and the two
   would sit on top of each other. The sheet's first entry goes to the top of
   the document, so nothing is lost by standing the generic one down. */
:root:has([data-secnav-sheet]) .totop { display: none; }

/* ---- the index ---------------------------------------------------------
   A list of names and nothing else. Every device a row could carry — a rule
   down the side, a ground behind the current one, a weight change — is one
   more mark competing with the six words that are the actual content, so the
   default is a plain row and the rail asks for its own furniture. */
.secindex { list-style: none; margin: 0; padding: 0; }
.secindex__group + .secindex__group { margin-top: var(--space-lg); }
.secindex__title {
  display: block;
  font-family: var(--font-condensed);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
  padding-inline: var(--space-2xs);
  margin-bottom: var(--space-3xs);
}
.secindex__list { list-style: none; margin: 0; padding: 0; display: grid; gap: 1px; }

.secindex__link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  min-height: 2.75rem;   /* 44px — the whole row is the target */
  padding: 0.45rem var(--space-2xs);
  border-radius: var(--radius-sm);
  font-size: var(--text-base);
  line-height: var(--leading-snug);
  text-decoration: none;
  color: var(--text-secondary);
  transition: color var(--dur-fast) var(--ease-out);
}
.secindex__link:hover { color: var(--text-primary); }
/* Nothing is highlighted until it is pressed, and then only while it is. */
.secindex__link:active { background: var(--surface-hover); }
/* Weight is NOT part of the current state: Helvetica has no 600, 700 is a
   visible jump, and either way the row would change width as you scroll past
   it. Full-strength ink and one sage dot say it instead — a dot is where you
   are, where a ground would read as what you have chosen. */
.secindex__link[aria-current] { color: var(--text-primary); }
.secindex__link[aria-current]::after {
  content: "";
  flex: none;
  width: 0.375rem;
  height: 0.375rem;
  border-radius: 50%;
  background: var(--accent-mark);
}

/* ---- the pill ----------------------------------------------------------
   Full-bleed between the gutters rather than shrink-to-fit: the label reads
   from the same left edge as the page's own text, and the chevron parks at the
   right where the thumb already is.

   Frosted and borderless, which is the language .totop already speaks — this
   site has exactly one kind of thing that floats over live content and they
   should not be two. A hairline on top of a blurred edge draws a second edge
   fighting the first, and --shadow-menu is built for a menu-sized surface;
   --shadow-float is the throw a control this size can carry without reading
   as a smudge underneath it. */
.secjump {
  position: fixed;
  z-index: var(--z-sticky);
  left: var(--gutter);
  right: var(--gutter);
  /* The inset is what keeps it clear of the home indicator; without it the
     control lands under the gesture bar on every notched phone. */
  bottom: calc(var(--space-sm) + env(safe-area-inset-bottom, 0px));
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: 0.5rem 0.75rem 0.5rem 1rem;
  border: 0;
  border-radius: var(--radius-pill);
  /* Partly transparent on purpose. At full opacity the backdrop-filter has
     nothing to show through and the glass collapses into a plain slab —
     denser than the puck's 68% because this one has type on it. */
  background: color-mix(in srgb, var(--surface-raised) 86%, transparent);
  -webkit-backdrop-filter: blur(14px) saturate(1.6);
          backdrop-filter: blur(14px) saturate(1.6);
  color: var(--text-primary);
  box-shadow: var(--shadow-float);
  cursor: pointer;
  /* A press-and-hold on a control is a press, not a text selection — without
     this the label highlights blue under the thumb. */
  -webkit-user-select: none; user-select: none;
  /* Taps land instantly; the 300ms wait is for double-tap zoom this has none
     of, and the grey flash is Android's, not ours. */
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: opacity var(--dur-fast) var(--ease-out),
              transform var(--dur-normal) var(--ease-nav);
}
/* Without the frost there is nothing to see through, so the glass becomes a
   raised surface instead of a translucent one. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .secjump { background: var(--surface-raised); }
}

/* Desktop reads a page top to bottom with a scrollbar to hand, and any page
   carrying this has a rail up there anyway. */
@media (min-width: 48rem) { .secjump, .secsheet { display: none; } }

/* A press is a press. The whole pill takes the dip, like every other control
   on the site that can be pushed. */
.secjump:active { transform: scale(0.985); transition-duration: var(--dur-fast); }

/* Out of the way while the sheet it opened is on screen. */
.secjump[data-state="open"] { opacity: 0; transform: translateY(120%); }
.secjump__text {
  display: grid;
  justify-items: start;
  flex: 1 1 auto;
  min-width: 0;
  text-align: left;
}
/* The eyebrow is a caption on the line below it, not a second label. It gets
   the smallest size in the scale and the quietest ink, so what the control
   actually says is the section name. */
.secjump__eyebrow {
  font-family: var(--font-condensed);
  font-size: var(--text-2xs);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
  line-height: 1.1;
}
.secjump__now {
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.secjump__chev { width: 1.125rem; height: 1.125rem; flex: none; color: var(--text-muted); }

/* Nothing else on screen should have to compete with an open menu. */
:root:has([data-header][data-drawer-open]) .secjump {
  opacity: 0;
  transform: translateY(120%);
}

/* ---- the sheet ---------------------------------------------------------- */
.secsheet[hidden] { display: none; }
.secsheet { position: fixed; inset: 0; z-index: var(--z-modal); }
/* Same wash as the directions dialog — explicit ink at low alpha rather than
   shading toward black, so the page keeps its colour underneath. */
.secsheet__scrim {
  position: absolute;
  inset: 0;
  background: rgb(12 10 9 / 0.55);
  -webkit-backdrop-filter: blur(3px);
          backdrop-filter: blur(3px);
  opacity: 0;
  transition: opacity var(--dur-menu-out) var(--ease-out);
}
.secsheet__panel {
  position: absolute;
  inset: auto 0 0 0;
  display: flex;
  flex-direction: column;
  /* Short of the full height on purpose: the strip of page left showing above
     it is what says this is a layer over the document rather than a new
     screen. */
  max-height: min(78svh, 40rem);
  background: var(--surface-raised);
  border-radius: var(--radius-xl) var(--radius-xl) 0 0;
  box-shadow: var(--shadow-modal);
  transform: translateY(100%);
  transition: transform var(--dur-menu-out) var(--ease-nav);
}
.secsheet[data-state="open"] .secsheet__scrim { opacity: 1; }
.secsheet[data-state="open"] .secsheet__panel {
  transform: none;
  transition-duration: var(--dur-menu-in);
}
/* No rule under the head. The gap between the title and the first row is
   already the separation, and a hairline drawn across a sheet this short is a
   line for its own sake. */
.secsheet__head {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-sm);
  padding: var(--space-md) var(--space-md) var(--space-2xs);
}
.secsheet__grab {
  position: absolute;
  top: var(--space-2xs);
  left: 50%;
  transform: translateX(-50%);
  width: 2.25rem;
  height: 0.25rem;
  border-radius: var(--radius-pill);
  background: var(--border-subtle);
}
/* The list is the sheet. Its title says what pressing a row will do and then
   gets out of the way, so it is set at the size of the rows rather than above
   them. */
.secsheet__title {
  font-family: var(--font-sans);
  font-size: var(--text-base);
  color: var(--text-muted);
  margin: 0;
}
/* Ringless. The × is already a closed shape and a circle drawn round it is a
   second one saying nothing the first did not — and the 44px target is the
   control's size, not something that has to be visible to exist. */
.secsheet__close {
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  margin: -0.5rem -0.75rem -0.5rem 0;   /* target grows inward, not outward */
  flex: none;
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
  transition: color var(--dur-fast) var(--ease-out);
}
.secsheet__close:hover { color: var(--text-primary); }
.secsheet__close svg { width: 1.125rem; height: 1.125rem; }
/* The rows carry their own inset so a press has something to land on, so the
   body gives back exactly that much — row text and the title above it start
   on the same line. */
.secsheet__body {
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: var(--space-2xs) calc(var(--space-md) - var(--space-2xs))
           calc(var(--space-lg) + env(safe-area-inset-bottom, 0px));
}

@media (prefers-reduced-motion: reduce) {
  .secjump, .secsheet__scrim, .secsheet__panel { transition-duration: 1ms; }
}

/* ==========================================================================
   ROW LIST
   A ruled list of linked rows. Used wherever a set of destinations would
   otherwise become a row of identical cards — it gives the set a reading
   order and lets each item carry a different amount of copy.
   ========================================================================== */
.rowlist { border-top: var(--border-width) solid var(--border-subtle); }

.rowitem {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-md);
  padding-block: var(--space-md);
  border-bottom: var(--border-width) solid var(--border-subtle);
  transition: border-color var(--dur-fast) var(--ease-out);
}
.rowitem:hover { border-bottom-color: var(--border-strong); }
.rowitem__title { font-size: var(--text-xl); font-weight: var(--weight-bold); font-family: var(--font-sans); text-transform: none; letter-spacing: var(--tracking-normal); line-height: var(--leading-snug); }
.rowitem:hover .rowitem__title { color: var(--text-accent); }
.rowitem__body {
  margin-top: var(--space-3xs);
  max-width: 54ch;
  color: var(--text-secondary);
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
}
.rowitem__link { text-decoration: none; color: inherit; }
.rowitem__link::after { content: ""; position: absolute; inset: 0; }
.rowitem__chevron {
  width: 1.25rem; height: 1.25rem;
  color: var(--text-muted);
  transition: transform var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.rowitem:hover .rowitem__chevron { transform: translateX(4px); color: var(--accent-mark); }

/* ==========================================================================
   EVENT ROW
   The date block is real information, so it is given real typographic weight
   rather than being reduced to an icon.
   ========================================================================== */
.event {
  position: relative;
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: var(--space-md);
  align-items: center;
  padding-block: var(--space-sm);
  border-bottom: var(--border-width) solid var(--border-subtle);
}
@media (min-width: 48rem) {
  .event { grid-template-columns: auto minmax(0, 1fr) auto; gap: var(--space-lg); }
}
.event:hover { border-bottom-color: var(--border-strong); }

.event__date {
  display: grid;
  place-items: center;
  width: 3.75rem;
  padding: var(--space-2xs) 0;
  border: var(--border-width) solid var(--border-subtle);
  border-radius: var(--radius-sm);
  background-color: var(--surface-sunken);
  line-height: 1;
}
.event__month {
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-accent);
}
.event__day {
  font-family: var(--font-condensed);   /* numeral in a narrow chip */
  font-size: 1.75rem;
  color: var(--text-primary);
  margin-top: 0.15rem;
}
.event__title { font-size: var(--text-lg); font-weight: var(--weight-bold); font-family: var(--font-sans); text-transform: none; letter-spacing: var(--tracking-normal); line-height: var(--leading-snug); }
.event__meta {
  display: flex; flex-wrap: wrap; gap: var(--space-2xs) var(--space-sm);
  margin-top: 0.15rem;
  font-size: var(--text-sm);
  color: var(--text-muted);
}
.event__link { text-decoration: none; color: inherit; }
.event__link::after { content: ""; position: absolute; inset: 0; }
.event:hover .event__title { color: var(--text-accent); }
.event__action { display: none; }
@media (min-width: 48rem) { .event__action { display: block; } }

/* ==========================================================================
   FEATURE PANEL — oversized statement block (mission, invitations)
   ========================================================================== */
.panel {
  padding: var(--space-lg);
  border-radius: var(--radius-xl);
  background-color: var(--surface-cream);
  color: var(--text-primary);
}
@media (min-width: 48rem) { .panel { padding: var(--space-2xl); } }
.panel--accent { background-color: var(--surface-accent); color: var(--text-on-accent); }

/* --------------------------------------------------------------------------
   ON A SAGE GROUND
   Sage is the one surface that does not change between themes — --surface-accent
   is the same brand green in both — so nothing sitting on it may change either.
   Every token that would otherwise follow the theme is pinned to ink here.

   The bug that forced this: --surface-hover is stone-100 in light and stone-800
   in DARK, so an outline button on sage took a near-black fill in dark mode and
   kept its ink label, which made the text disappear on hover. Any theme-reactive
   token is wrong on a fixed ground; pinning one of them would only have moved
   the problem to the next one.
   -------------------------------------------------------------------------- */
.panel--accent,
.mediacard--panel {
  --text-primary:    var(--text-on-accent);
  --text-secondary:  color-mix(in srgb, var(--text-on-accent) 80%, transparent);
  --text-muted:      color-mix(in srgb, var(--text-on-accent) 66%, transparent);
  --border-subtle:   color-mix(in srgb, var(--text-on-accent) 22%, transparent);
  --border-default:  color-mix(in srgb, var(--text-on-accent) 34%, transparent);
  --border-control:  color-mix(in srgb, var(--text-on-accent) 48%, transparent);
  --surface-hover:   color-mix(in srgb, var(--text-on-accent) 12%, transparent);
  --focus-ring:      var(--gl-ink);
}
.panel--accent .btn--secondary,
.mediacard--panel .btn--secondary {
  --btn-bg: var(--gl-ink);
  --btn-fg: var(--gl-stone-50);
  --btn-bg-hover: color-mix(in srgb, var(--gl-ink) 84%, var(--gl-stone-50));
}
/* Ink label on a translucent ink wash — legible on sage in either theme,
   which the inherited --surface-hover was not. */
.panel--accent .btn--outline,
.mediacard--panel .btn--outline {
  --btn-fg: var(--gl-ink);
  --btn-border: color-mix(in srgb, var(--gl-ink) 48%, transparent);
  --btn-bg-hover: color-mix(in srgb, var(--gl-ink) 12%, transparent);
}
/* On sage, the default stone-300 border is barely there — 1.6:1 against the
   panel. The outline takes the same ink the panel's text uses (7.17:1), so it
   reads as a deliberate second action rather than a control that failed to
   render. */
.panel--accent .btn--outline {
  --btn-border: var(--text-on-accent);
  --btn-fg: var(--text-on-accent);
  --btn-bg-hover: color-mix(in srgb, var(--gl-ink) 12%, transparent);
}
.panel__title { font-size: var(--display-md); max-width: 18ch; }
.panel__body { margin-top: var(--space-sm); max-width: 52ch; line-height: var(--leading-relaxed); }

/* ==========================================================================
   SHOWCASE — sticky media, scrolling panels
   Mirrors the live "New Here / Events / Connect Groups / Serve" section: a
   media panel pins beside the text and swaps image as each panel passes it.

   The sticky element pins at --header-h, matching the live site exactly, so
   the image sits directly beneath the header rather than at an arbitrary
   offset. Its height is bounded so it never exceeds the viewport minus the
   header — a sticky element taller than its scrollport silently stops
   sticking, which is the usual reason these break.
   ========================================================================== */
/* Centred, and capped below the page container. The container is fluid to
   2200px, which is right for full-bleed cards but lets this section's two
   columns drift so far apart that the eye has to travel between an image and
   the text it belongs to. 1440px keeps the pair reading as one unit while
   still giving the media real presence. */
/* The full-screen tint this section fades in has no element of its own. It used
   to be a `position: fixed` backdrop at z-index -1, which covered the viewport
   at rest and failed the moment iOS Safari collapsed its toolbar: a fixed box
   cannot paint the strip the browser reveals in that same frame, so the ground
   appeared to lift off the bottom of the screen. The tint is now mixed into the
   root background — see the `html` rule in base.css — where it reaches the
   canvas and cannot be lifted off anything. initShowcase() sets --tint-o. */

/* --- sticky intro ---------------------------------------------------------
   The heading holds while the section's ground tints in, then lifts away as the
   panels arrive and the image below takes over the pinning. Two elements, the
   same as the sticky media: the outer box supplies the scroll range, the inner
   one sticks.

   The pinned box is the HEADING'S OWN height, not a full frame. A frame-tall
   pinned box was the obvious first move — it centres the heading and mirrors
   .showcase__sticky — but it unpins when its BOTTOM edge reaches the bottom of
   the runway, which is one whole frame before the runway actually ends. That
   left ~730px where the heading had gone and the image had not yet pinned, and
   the gap is fixed: it is (frame − header) regardless of how long the runway
   is, so no amount of extra height closes it. Sizing the box to the heading
   drops the gap to (top offset + heading height − header), about 210px, which
   reads as one element handing off to the next.

   Consequence of the same maths: the heading sits in the upper third rather
   than centred. That is the trade for continuity, and it is the right one here
   — the space below it is where the panels rise from. */
/* Mobile gets no scroll effect at all. A pinned heading needs a runway of
   empty screen to hold over, and on a phone that is most of the page — the
   reader scrolls a blank tinted field waiting for content. Ordinary flow with
   generous space below says the same thing in a third of the distance. */
.showcase__intro { padding-block: var(--space-2xl) var(--space-3xl); }
.showcase__intro-pin { position: static; }
.showcase__intro .section-head { margin-bottom: 0; }

@media (min-width: 48rem) {
  /* Shorter than a frame, deliberately. At ~108svh the runway was almost exactly
     one viewport tall, which put the panels just below the fold for the whole
     hold — so a large desktop showed a heading at the top of an otherwise empty
     tinted screen, and had to scroll ~800px of it. Sizing the runway well under
     a frame means the grid is already rising into view when the heading pins:
     the screen is never empty, and the hold drops to ~340px. The rem cap stops
     the whole thing growing again on a tall window. */
  .showcase__intro {
    padding-block: 0;
    min-height: min(76svh, 38rem);
  }
  .showcase__intro-pin {
    position: sticky;
    top: calc(var(--header-h) + var(--space-2xl));
  }
}

/* With motion turned down the runway is dead space and a pinned heading is a
   scroll effect nobody asked for. Both revert to ordinary flow. */
@media (prefers-reduced-motion: reduce) {
  .showcase__intro { min-height: 0; padding-block: var(--space-3xl); }
  .showcase__intro-pin { position: static; }
}

.showcase__grid {
  display: grid;
  gap: var(--space-xl);
  max-width: 90rem;          /* 1440px */
  margin-inline: auto;
}
@media (min-width: 64rem) {
  .showcase__grid {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-3xl);
    align-items: stretch;
  }
}

/* --- sticky media (desktop only) -----------------------------------------
   TWO elements, not one. A sticky element travels only within its containing
   block, and a grid item's box is just its own height — so making the media
   itself sticky gave it no range at all and it scrolled away with the page.
   The outer column stretches to the full row height and provides the range;
   the inner box does the sticking. */
.showcase__media { display: none; }
@media (min-width: 64rem) {
  .showcase__media {
    display: block;
    position: relative;
    align-self: stretch;      /* the whole point: gives the sticky its range */
    height: 100%;
  }
  /* The sticky element spans the viewport below the header and CENTRES the
     media inside it, rather than pinning the media itself near the top. Two
     reasons: the image holds the optical centre of the screen while you read,
     and — the practical one — a top-pinned image releases before the last
     panel can reach it, so the final item never became active. Anchoring at
     the middle gives every panel, including the last, a turn. */
  .showcase__sticky {
    position: sticky;
    top: var(--header-h);
    height: calc(100svh - var(--header-h));
    display: grid;
    place-items: center;
  }
  .showcase__frame {
    width: 100%;
    /* Square, matching the reference implementation this pattern comes from. */
    aspect-ratio: 1 / 1;
    max-height: min(72svh, 44rem);
    border-radius: var(--radius-xl);
    overflow: hidden;
    background-color: var(--surface-media);
    isolation: isolate;
    position: relative;
  }
}
.showcase__frame img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  /* A slow, small scale under the crossfade. Without it the swap reads as a
     slideshow; with it the panel feels like one continuous surface. */
  transform: scale(1.04);
  transition: opacity var(--dur-slow) var(--ease-out),
              transform 900ms var(--ease-out);
}
.showcase__frame img[data-active="true"] { opacity: 1; transform: scale(1); }

/* --- panels --------------------------------------------------------------- */
/* Mobile: one column, content full width.
   Tablet: two columns — the sticky media is hidden below 64rem, so the panels
   have the whole width to themselves and a single column would leave a long
   thin ribbon of text down the middle.
   Desktop: back to one column, because it sits beside the pinned image. */
.showcase__panels { display: grid; gap: var(--space-xl); }
/* Rules between the panels in the stacked layouts.

   The colour is a wash of the text colour rather than --border-subtle, which
   is stone-200 — effectively the tint itself (1.19:1) and so invisible the
   moment the section locks. Mixing from --text-primary also means it inverts
   with the theme for free.

   Gap goes to zero and the spacing moves into the panels' own padding, so each
   rule sits centred in the space between two panels rather than hugging the
   one above it. */
.showcase__panels { --rule: color-mix(in srgb, var(--text-primary) 20%, transparent); }

/* Mobile: a divider between each item, none after the last. The thumbnail sits
   to the LEFT of the copy rather than above it — spanning every row of a
   two-column grid, so the title, body and action stack beside it and the panel
   reads as one list row instead of an image with a caption under it. */
@media (max-width: 47.999rem) {
  .showcase__panels { gap: 0; }
  .showcase__panel {
    /* The thumbnail is taken OUT of the grid and positioned instead.

       As a grid item spanning every row it forced the row tracks to total at
       least its own 72px height, and the browser hands that surplus to the gaps
       between rows — which is what pushed the title 63px off its description.
       Row sizing has to answer to the copy alone, so the image sits absolutely
       and the column is simply padded to clear it. */
    position: relative;
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--space-2xs);
    align-items: start;
    padding-block: var(--space-lg);
    padding-left: calc(4.5rem + var(--space-sm));
    border-bottom: var(--border-width) solid var(--rule);
  }
  .showcase__panel > .showcase__thumb {
    position: absolute;
    inset-inline-start: 0;
    top: var(--space-lg);
  }
  .showcase__panel:first-child > .showcase__thumb { top: 0; }
  .showcase__panel > .btn { margin-top: var(--space-2xs); }
  .showcase__panel:first-child { padding-top: 0; }
  .showcase__panel:last-child { padding-bottom: 0; border-bottom: 0; }
}

/* Tablet: two columns crossed by a vertical and a horizontal rule.
   The nth-child selectors assume a 2-column grid — :nth-child(odd) is the left
   column and :nth-child(-n+2) the first row. Outer edges take no padding so
   the block still starts and ends flush with the section. */
@media (min-width: 48rem) and (max-width: 63.999rem) {
  .showcase__panels {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 0;
  }
  .showcase__panel { padding: var(--space-xl); }
  .showcase__panel:nth-child(odd)  { padding-left: 0;  border-right: var(--border-width) solid var(--rule); }
  .showcase__panel:nth-child(even) { padding-right: 0; }
  .showcase__panel:nth-child(-n+2) { padding-top: 0;   border-bottom: var(--border-width) solid var(--rule); }
  .showcase__panel:nth-child(n+3)  { padding-bottom: 0; }
}
@media (min-width: 64rem) {
  /* Spacing sets the scroll distance, and therefore how long each panel gets
     beside the image. The live section runs ~1900px for four panels; these
     values land in the same range. */
  /* Padded in viewport units at both ends, and generously.

     The anchor is the centre of the pinned image, and a panel only counts as
     active when it is nearest to that point. At 20svh the first panel raced
     past the anchor BEFORE the image had pinned — measurably so: panel 0 was
     only ever active at negative scroll offsets, never once while pinned, so
     "New Here?" never held the rule while its image was up.

     ~42svh puts the first panel below the anchor at the moment pinning begins,
     so it scrolls up into it afterwards. The trailing value does the same for
     the last panel before the sticky releases. The empty space this creates is
     not padding for its own sake — it is the scroll distance the first and
     last items need in order to get a turn. */
  .showcase__panels { gap: 9rem; padding-block: 42svh 46svh; }
}

.showcase__panel {
  display: grid;
  gap: var(--space-sm);
  justify-items: start;

  /* Applies at EVERY width, not just desktop. The tint fades in beneath these
     panels on all screen sizes, so their controls sit on two different grounds
     at different scroll positions and the border has to hold on both. The
     default border is stone-300, which IS the tint — 1.00:1 against it, so the
     outline disappears completely once the section locks. stone-500 is the
     lowest step clearing 3:1 on the page AND on the tint, in both themes
     (light 4.59 / 3.22, dark 4.12 / 3.16). Scoping this to the desktop
     breakpoint left the same bug in place on tablet and mobile. */
  --border-default: var(--gl-stone-500);
}
/* Stacked, the image stops being a banner and becomes a mark: a small circle
   set left, above the heading. A full-width 4:3 photo per panel turned the
   column into four large images with captions, which buries the copy — the
   panels are the content here and the media is the accent. */
.showcase__thumb {
  width: 4.5rem;
  aspect-ratio: 1;
  object-fit: cover;
  border-radius: 50%;
  justify-self: start;
  background-color: var(--surface-sunken);
}
@media (min-width: 64rem) { .showcase__thumb { display: none; } }

.showcase__title { font-size: var(--display-sm); }
.showcase__body {
  max-width: 46ch;
  color: var(--text-secondary);
  line-height: var(--leading-relaxed);
}

/* Panels rise as they enter. Transform + opacity only, so this stays on the
   compositor and never reflows the column while scrolling. */
@media (min-width: 64rem) {
  .showcase__panel {
    position: relative;
    padding-left: var(--space-md);
    opacity: 0.34;
    transform: translateY(14px);
    transition: opacity var(--dur-slow) var(--ease-out),
                transform var(--dur-slow) var(--ease-out);
  }
  .showcase__panel[data-shown="true"] { opacity: 1; transform: none; }

  /* A slight rule marking the panel the image currently belongs to. It draws
     downward from the top rather than fading, so the eye reads it as the
     section being entered. The gutter is reserved on every panel, so nothing
     shifts sideways as the marker moves between them. */
  .showcase__panel::before {
    content: "";
    position: absolute;
    left: 0;
    top: 0.15em;
    bottom: 0.15em;
    width: 2px;
    border-radius: 2px;
    /* The brand sage itself, not --text-accent. This is a mark, not text, so it
       is not bound by the contrast rule that pushes accented TEXT to sage-700 /
       sage-400 — and like every other sage surface it stays constant across
       themes rather than shifting with them. */
    background-color: var(--gl-sage);
    transform: scaleY(0);
    transform-origin: top center;
    transition: transform var(--dur-slow) var(--ease-out);
  }
  .showcase__panel[data-active="true"]::before { transform: scaleY(1); }
}
@media (prefers-reduced-motion: reduce) {
  .showcase__panel::before { transition: none; }
}
/* Dimming unread panels is an effect, not information — if motion is not
   wanted, everything is simply legible from the start. */
@media (prefers-reduced-motion: reduce) {
  .showcase__panel { opacity: 1 !important; transform: none !important; }
  .showcase__frame img { transition: none; }
}

/* ==========================================================================
   WATCH BADGE
   The site's own artwork: watch-ring.svg (the lettering, as outlines) layered
   over watch-play.svg. Both share a 300.57 x 300.66 viewBox, so they overlay
   exactly at the same size with no positioning needed.

   Loaded as <img> rather than inlined: the ring is ~25KB of outlined letter
   paths, so inlining it once per instance would cost more than the images
   themselves. As <img> each is fetched once and cached, and it removes the
   duplicate-id hazard that inline SVG <textPath> carries.
   ========================================================================== */
.watch-badge {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  border: 0;
  background: none;
  cursor: pointer;
}
.watch-badge__disc {
  position: relative;
  display: grid;
  /* One explicit cell filling the box. `place-items: center` would size the
     track to the item while the item sizes to the track — circular, and it
     collapsed the artwork to zero. A 1fr track breaks the loop. */
  grid-template: 1fr / 1fr;
  width: clamp(6rem, 4.5rem + 6vw, 9rem);
  aspect-ratio: 1;
  border-radius: var(--radius-pill);
  background: rgb(250 250 249 / 0.14);
  -webkit-backdrop-filter: blur(12px) saturate(1.4);
          backdrop-filter: blur(12px) saturate(1.4);
  transition: background-color var(--dur-normal) var(--ease-out),
              transform var(--dur-normal) var(--ease-out);
}
.watch-badge:hover .watch-badge__disc { background: rgb(250 250 249 / 0.24); transform: scale(1.04); }
/* In the flow rather than over its container, for the watch hero — which
   gives the badge a stage row of its own so it cannot overlap the title. An
   absolutely positioned badge paints above the card's static content. */
.watch-badge--inline { position: static; }
.watch-badge:active .watch-badge__disc { transform: scale(0.97); }

/* Both layers share one grid cell and are inset by their OWN size, not by
   padding on the disc. Percentage padding resolves against the containing
   block's inline size — here the media panel, not the disc — so `padding: 11%`
   became 81px and swallowed the whole box. A percentage width on a grid item
   resolves against the grid area instead, which is the disc, so this scales
   correctly with the badge at every breakpoint.

   `inset` was not an option either: on a replaced element like <img> it
   repositions without resizing, which just shifts the artwork off-centre.

   The selector is qualified to out-specify `.msg__media img`, which otherwise
   forces object-fit: cover and crops the lettering. */
.watch-badge .watch-badge__ring,
.watch-badge .watch-badge__play {
  grid-area: 1 / 1;
  place-self: center;
  width: 78%;
  height: 78%;
  object-fit: contain;
}
/* Only the lettering turns. The play mark holds still so the eye has a fixed
   target to aim at — a control that rotates is harder to hit and to read. */
.watch-badge__ring { animation: watch-spin 18s linear infinite; }
@keyframes watch-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) { .watch-badge__ring { animation: none; } }

/* ==========================================================================
   MESSAGE VARIANTS (see message-options.html)
   ========================================================================== */
.msg { background-color: var(--surface-deep); color: var(--gl-stone-50); border-radius: var(--radius-xl); }
.msg__inner { padding: clamp(1.5rem, 1rem + 3vw, 3.5rem); }
.msg__eyebrow {
  display: block;
  font-family: var(--font-condensed);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: rgb(250 250 249 / 0.62);
  /* Tight to the title: it now labels the text block rather than the section,
     so it should read as part of that block, not float above it. */
  margin-bottom: var(--space-3xs);
}
/* Condensed, uppercase — the one place the secondary face carries a heading,
   matching how the live site sets message titles. */
.msg__title {
  font-family: var(--font-condensed);
  font-weight: var(--weight-bold);
  font-size: clamp(1.75rem, 1.1rem + 3vw, 3.5rem);
  line-height: 1.02;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  color: var(--gl-stone-50);
  text-wrap: balance;
}
.msg__media {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background-color: var(--gl-black);
}
.msg__media img { width: 100%; height: 100%; object-fit: cover; }

/* B — split */
/* G — meta bar beneath the artwork. The title drops to a supporting size on
   purpose: the graphic already states it, so repeating it at display scale
   says the same thing twice. */
.msg--bar .msg__title { font-size: clamp(1.25rem, 1.05rem + 0.9vw, 1.75rem); }
.msg__bar {
  display: grid;
  gap: var(--space-sm);
  margin-top: var(--space-lg);
  padding-top: var(--space-md);
  border-top: var(--border-width) solid rgb(250 250 249 / 0.18);
}
@media (min-width: 48rem) {
  .msg__bar { grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: var(--space-lg); }
}
.msg__barmeta {
  display: flex; flex-wrap: wrap; gap: var(--space-2xs) var(--space-sm);
  font-size: var(--text-sm); color: rgb(250 250 249 / 0.72);
}


/* ==========================================================================
   REEL — auto-advancing 4:5 card track
   Built on native scroll-snap rather than a transform carousel: touch,
   trackpad and keyboard scrolling all work for free, and the auto-advance is
   just a programmatic scroll on top of the same mechanism. If the script never
   runs, it is still a perfectly good swipeable row.
   ========================================================================== */
.reel { position: relative; }

/* The row is moved with a transform, not by scrolling.

   scrollLeft is rounded to whole pixels, so the slow ends of an eased glide
   (well under 1px per frame) stutter between moving and not moving — that is
   the jumpiness. A transform is sub-pixel and runs on the compositor, which is
   why the reference implementation translates its track too. */
.reel__viewport {
  overflow: hidden;
  /* Tells the browser nothing inside can paint outside this box, so the very
     wide track cannot influence the rest of the page's paint or layout. */
  contain: layout paint;
  touch-action: pan-y;      /* horizontal drag is ours; vertical stays the page's */
  cursor: grab;
}
.reel__viewport:active { cursor: grabbing; }

.reel__track {
  display: flex;
  /* Without this the default `stretch` sizes every card to the container's
     height and silently overrides aspect-ratio — so the cards change shape as
     the row is resized. Aligning to the start lets each card derive its height
     from its own width, which is what keeps 4:5 true at every viewport. */
  align-items: flex-start;
  /* Sized to its content rather than inheriting the parent's width. Without
     this the flex box reports the viewport width while its children overflow
     it, which makes every offset measurement read against the wrong box. */
  width: max-content;
  gap: var(--space-sm);
  padding-block: var(--space-2xs);
  /* NO will-change here. The track is several thousand pixels wide, and
     promoting it permanently creates a GPU layer of roughly its full size at
     device pixel ratio — large enough to exhaust the layer budget and leave
     parts of the page unpainted. The transform below promotes it only while it
     is actually moving, which is all that is needed. */
  transform: translate3d(0, 0, 0);
}

.reel__card {
  position: relative;
  flex: 0 0 clamp(16rem, 11rem + 22vw, 25rem);
  aspect-ratio: 4 / 5;
  /* Perspective lives on the card, not the rotating element — a transform is
     rendered in its PARENT's perspective, so putting it on the wrapper itself
     would flatten the rotation into a 2D squash. No overflow:hidden here
     either; clipping on the card would flatten the 3D space. The radius and
     clipping move to the faces. */
  perspective: 1200px;
  -webkit-user-select: none;
          user-select: none;
}
.reel__card img { pointer-events: none; }

.reel__link { display: block; width: 100%; height: 100%; text-decoration: none; color: inherit; }

.reel__flip {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transition: transform 600ms var(--ease-out);
}

.reel__face {
  position: absolute;
  inset: 0;
  border-radius: var(--radius-lg);
  overflow: hidden;
  /* Both faces must hide their back, or the front shows through mirrored. */
  -webkit-backface-visibility: hidden;
          backface-visibility: hidden;
  background-color: var(--surface-media);
}
.reel__face--back {
  transform: rotateY(180deg);
  display: grid;
  align-content: center;
  justify-items: center;
  gap: var(--space-xs);
  padding: var(--space-lg);
  text-align: center;
  /* The deep surface, not the raw primitive. Hard-coded to stone-950 this was
     byte-identical to the dark-mode page ground, so a flipped card dissolved
     into the background and only its text and CTA stayed visible. The semantic
     resolves to the same stone-950 in light mode and steps down to true black
     in dark, which is the same move the message card and the footer make. */
  background-color: var(--surface-deep);
  color: var(--gl-stone-50);
}
.reel__face--back .reel__kicker { color: rgb(250 250 249 / 0.6); }
.reel__face--back .reel__name { font-size: var(--display-sm); }
/* The same glass as "Watch Message": a light film over the card, no outline.
   It was solid sage, which put the brand's loudest colour on a card that is
   already asking for attention with a flip — and sage is reserved (see the
   button rule). A film reads as an affordance on the photograph rather than a
   second thing competing with it, and it carries its own contrast without an
   edge: an outline on a blurred film is a second boundary saying what the fill
   already says. */
.reel__cta {
  margin-top: var(--space-2xs);
  display: inline-flex;
  align-items: center;
  gap: 0.4em;
  padding: 0.7rem 1.4rem;
  border-radius: var(--radius-pill);
  background-color: rgb(250 250 249 / 0.18);
  color: var(--gl-stone-50);
  -webkit-backdrop-filter: blur(10px) saturate(1.4);
          backdrop-filter: blur(10px) saturate(1.4);
  font-size: var(--text-sm);
  /* Same pill, same type as .btn — it is one only because it lives inside
     the card's link rather than beside it. */
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-ui);
}
/* Without backdrop-filter the film has nothing to frost and an 18% white on a
   dark card is barely there. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .reel__cta { background-color: rgb(250 250 249 / 0.3); }
}
.reel__card:hover .reel__cta { background-color: rgb(250 250 249 / 0.28); }
.reel__cta svg { width: 1em; height: 1em; }

/* Mobile: no flip. The back face stacks UNDER the image as ordinary content,
   the way the promo cards read — a flip needs a hover to invite it, and on
   touch the card would either never turn or turn and stay stuck.

   Both faces come out of the 3D context entirely: static position, no
   transform, backface visible. Left in place they would still occupy the same
   cell and the back would render mirrored behind the front. */
@media (max-width: 47.999rem) {
  .reel__card { aspect-ratio: auto; }
  .reel__flip { display: grid; transform: none; }
  .reel__face {
    position: static;
    transform: none;
    -webkit-backface-visibility: visible;
            backface-visibility: visible;
  }
  .reel__face--front { aspect-ratio: 4 / 5; }
  /* The caption moves below the image, so the overlay copy would be a duplicate. */
  .reel__face--front figcaption { display: none; }
  /* The card carries the ground again. The flip restructure moved the surface
     onto the faces, so with the back face transparent the near-white copy was
     sitting on the light page — present, measurable, and unreadable. */
  .reel__card {
    background-color: var(--surface-media);
    border-radius: var(--radius-lg);
    overflow: hidden;
  }
  /* The content column fills whatever the image leaves, so the action can be
     pushed to the card's bottom edge rather than trailing the text. */
  .reel__flip { grid-template-rows: auto 1fr; }
  .reel__face--back {
    background-color: transparent;   /* the card's ground now shows through */
    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
    gap: var(--space-3xs);
    padding: var(--space-md);
  }
  .reel__face--back .reel__name { font-size: var(--text-lg); }
  /* `margin-top: auto` in a flex column absorbs all remaining space above the
     button, which pins it to the bottom no matter how many lines land above —
     the reason the column needed a definite height in the first place. */
  .reel__cta {
    display: inline-flex;
    margin-top: auto;
    padding: 0.55rem 1.1rem;
    font-size: var(--text-xs);
  }
  /* A second focusable control inside a card that is already a link would be a
     duplicate tab stop to the same place, so it stays a styled span. */

  /* Every card the same height regardless of how long its words are. Clamping
     alone is not enough — a one-line name still leaves a short card — so the
     name also RESERVES two lines. Both halves are needed. */
  .reel__kicker {
    display: -webkit-box;
    -webkit-line-clamp: 1;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
  .reel__name {
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    min-height: calc(2 * 1.2em);
    line-height: 1.2;
  }
}

/* Hover only where hover actually exists. A width breakpoint would flip on a
   narrow desktop window and, worse, leave touch devices stuck mid-flip after a
   tap registers as a lingering hover. On touch the card is simply a link. */
/* More air between cards where the row is wide enough to afford it. */
@media (min-width: 64rem) {
  .reel__track { gap: var(--space-xl); }
}

@media (hover: hover) and (pointer: fine) {
  .reel__card:hover .reel__flip,
  .reel__link:focus-visible .reel__flip { transform: rotateY(180deg); }
}
@media (prefers-reduced-motion: reduce) {
  .reel__flip { transition: none; }
}
.reel__card img { width: 100%; height: 100%; object-fit: cover; }
.reel__face--front figcaption {
  position: absolute;
  inset: auto 0 0 0;
  padding: var(--space-md) var(--space-sm) var(--space-sm);
  background: linear-gradient(to top, rgb(12 10 9 / 0.86), rgb(12 10 9 / 0));
  color: var(--gl-stone-50);
}
.reel__kicker {
  display: block;
  font-family: var(--font-condensed);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: rgb(250 250 249 / 0.72);
}
.reel__name { margin-top: 0.15rem; font-size: var(--text-xl); font-weight: var(--weight-bold); }

/* Auto-advancing content must be pausable — this is a requirement, not a
   nicety, for anything that moves for more than five seconds. */
/* Deliberately quiet, and out of the way. It sat in the heading cluster next to
   View All, which gave a utility control the same weight as the section's
   actual call to action. Below the track and right-aligned, it is where you
   would look for it only once you wanted it.

   Icon only: the label was the loudest part, and it had to change wording with
   the state anyway. The accessible name carries that instead. */
.reel__controls {
  display: flex;
  justify-content: flex-end;
  margin-top: var(--space-sm);
}
.reel__pause {
  display: inline-grid;
  place-items: center;
  width: 2rem;
  height: 2rem;
  padding: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  opacity: 0.55;
  transition: opacity var(--dur-fast) var(--ease-out),
              background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.reel__pause:hover,
.reel__pause:focus-visible {
  opacity: 1;
  color: var(--text-primary);
  background-color: var(--surface-hover);
}
.reel__pause svg { width: 0.9rem; height: 0.9rem; }
/* Both glyphs share one grid cell so the swap is a crossfade rather than a
   pop. display:none gave no frames to animate; each icon now scales through
   its own centre while the other leaves, so the control reads as one object
   changing state instead of two icons trading places. Short and eased-out —
   this is feedback on a click, not an event. */
.reel__pause .is-pause,
.reel__pause .is-play {
  grid-area: 1 / 1;
  transition: opacity 180ms var(--ease-out), transform 180ms var(--ease-out);
}
.reel__pause .is-play { opacity: 0; transform: scale(0.6); }
[data-reel][data-paused="true"] .reel__pause .is-play { opacity: 1; transform: none; }
[data-reel][data-paused="true"] .reel__pause .is-pause { opacity: 0; transform: scale(0.6); }

@media (prefers-reduced-motion: reduce) {
  .reel__pause .is-pause,
  .reel__pause .is-play { transition: none; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .reel__track { scroll-behavior: auto; }
}

/* ==========================================================================
   FAMILY — one row, one age range each
   ========================================================================== */
.family__grid { display: grid; gap: var(--space-lg); }
@media (min-width: 48rem) { .family__grid { grid-template-columns: repeat(3, 1fr); } }
.family__item {
  position: relative;
  display: grid;
  gap: var(--space-2xs);
  padding-top: var(--space-md);
  border-top: var(--border-width-thick) solid var(--gl-sage);
}
.family__age {
  font-family: var(--font-condensed);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
}
.family__name { font-size: var(--text-xl); font-weight: var(--weight-bold); }
.family__item:hover .family__name { color: var(--text-accent); }
.family__body { color: var(--text-secondary); font-size: var(--text-sm); line-height: var(--leading-relaxed); }
.family__link { text-decoration: none; color: inherit; }
.family__link::after { content: ""; position: absolute; inset: 0; }

/* ==========================================================================
   FEATURED EVENT
   Artwork beside the copy on a tinted surface — one step off the page ground
   rather than a dark slab, so it sits inside the page instead of interrupting
   it. The action is the secondary (ink) button, not sage: sage is the primary
   call to action and an event notice should not outrank Plan A Visit.
   ========================================================================== */
.fev {
  border-radius: var(--radius-xl);
  overflow: hidden;
  background-color: var(--surface-tint);
  color: var(--text-primary);
  transition: background-color var(--dur-tint) var(--ease-out),
              color var(--dur-tint) var(--ease-out);
}
.fev__grid { display: grid; }
@media (min-width: 52rem) {
  .fev__grid { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: stretch; }
}
.fev__media { position: relative; overflow: hidden; background-color: var(--surface-media); }

/* Featured badge. Sits above the title in the content area rather than on the
   artwork, so it labels the item rather than decorating the image.

   Deliberately quiet. A solid ink slab at bold weight competed with the title
   directly beneath it — a label should be read second, not first. It is now a
   faint wash of the text colour rather than a filled chip, at medium weight
   with tracking pulled back from the 0.09em used for standalone eyebrows,
   which at this size was doing more to widen the badge than to aid reading.
   The colour is still --text-primary, so quietening it costs no contrast. */
.fev__badge {
  /* A plain label, not a chip. The container was the loudest thing about it:
     even a faint wash draws a box around a word that only needs to be read
     first and quietly. Without it the badge sits in the same visual register as
     the eyebrows used everywhere else on the site. */
  display: inline-flex;
  align-items: center;          /* the icon centres on the label, not the baseline */
  gap: 0.4em;
  color: var(--text-muted);
  font-family: var(--font-condensed);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  line-height: 1;
}
/* `display: block` matters: an inline SVG sits on the text baseline and leaves
   a descender gap beneath it, which reads as the icon riding high. */
.fev__badge svg {
  display: block;
  flex: none;
  width: 0.8em;
  height: 0.8em;
  opacity: 0.7;                 /* the mark supports the word, it is not the point */
}
.fev__media img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* The media is rounded on ALL FOUR corners, not just the outer pair — it reads
   as its own rounded block sitting on the card, with the card's surface visible
   in the notches where the image curves away. This also makes it layout
   independent: the same rule holds whether the media sits beside the copy or
   above it, so nothing has to flip at the breakpoint. */
.fev--split .fev__media {
  /* Stacked: width comes from the column, height from the ratio. */
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-xl);
}
@media (min-width: 52rem) {
  .fev__inner {
    grid-template-columns: minmax(0, min(100%, 60ch));
    justify-content: center;
  }
  .fev--split .fev__media {
    /* Side by side, the ratio has to go.

       aspect-ratio resolves whichever dimension is not otherwise determined —
       so with `height: 100%` filling a tall row, it derived the WIDTH from that
       height. A title that wrapped to two lines made the row taller, which made
       the image wider, which pushed it straight over the text beside it. The
       image was sized by the copy instead of by its own column.

       Letting width come from the track and height from the row removes the
       feedback loop; the img's object-fit: cover handles the crop. */
    aspect-ratio: auto;
    height: 100%;
    min-width: 0;
  }
}

.fev__inner {
  display: grid;
  /* The text block is centred in the content area on BOTH axes, while the text
     inside it stays left-aligned.

     `justify-content` centres the column track; `justify-items: start` keeps
     every line ranged left within that track. Centring the items instead would
     centre each one independently and leave the title, body and button on three
     different left edges. The column is capped at a readable measure so the
     block has a defined width to be centred within — without a cap it would
     fill the area and there would be nothing to centre.

     The column is the ONLY measure. The body previously carried its own
     max-width as well, and two competing caps mean the smaller silently wins,
     which is what made the block look arbitrarily narrow. One cap, set here. */
  /* Stacked, the content fills the card: there is only one column, so capping
     the measure would leave the copy floating in a narrow strip while the
     image above it runs edge to edge. The cap and the centring only apply once
     the content sits in its own half beside the image. */
  grid-template-columns: minmax(0, 1fr);
  align-content: center;
  justify-items: start;
  gap: var(--space-sm);
  padding: clamp(1.25rem, 1rem + 2vw, 2.5rem);
}
.fev__title {
  font-size: var(--text-xl);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-ui);
  line-height: var(--leading-snug);
}
.fev__body {
  font-size: var(--text-sm);
  line-height: var(--leading-relaxed);
  color: var(--text-secondary);
}


/* ==========================================================================
   PRAYER HOUSE
   The gathering's own sub-brand, pinned in both appearances for exactly the
   reason Rooted is: the lockup, the grain and the palette belong to the event,
   and a brand that changes colour with the reader's OS setting is not a brand.

   Values sampled from the live page (post 7523), not invented: cream #FDEAD3,
   sand #E2D2BF, bark #533B1E, brown #4E3F2C, over a grainy sepia ground. Every
   block below remaps the semantic text roles onto those, so the components
   inside behave normally without a single hardcoded colour at a call site.

   The texture path is written from the STYLESHEET, not the document — url()
   inside a stylesheet resolves against the stylesheet, which is the trap
   documented at the ministry band in build.py.
   ========================================================================== */
.phouse {
  --ph-cream: #FDEAD3;
  --ph-sand:  #E2D2BF;
  --ph-bark:  #533B1E;
  --ph-brown: #4E3F2C;
}

/* ---- the ground: lockup beside a cream card ---------------------------- */
.phouse__band {
  /* Climbs back over the header so the grain runs behind the chrome rather
     than starting below it. The header is sticky and holds its own place in
     flow, so a band that wants to sit UNDER it has to reclaim that height and
     then pad it back — the same two lines the ministry band uses. The page is
     marked data-tone="dark" for the header's contrast switcher to match. */
  margin-top: calc(-1 * var(--chrome-h, var(--header-h)));
  padding-top: var(--chrome-h, var(--header-h));
  /* A colour under the grain, so the band is never a white flash while the
     texture loads and never bare paper if it 404s. */
  background-color: #1A1310;
  background-image: url("../img/prayer-house/texture.webp");
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  /* The head now sits on the grain rather than inside the cream card, so the
     band carries the light half of the palette and the card overrides it back. */
  color: var(--ph-cream);
  --text-primary: var(--ph-cream);
  --text-secondary: var(--ph-sand);
  --text-muted: var(--ph-sand);
}
.phouse__band > .container { padding-block: var(--space-3xl); }
/* The band holds one thing: the lockup, centred. Everything that was beside it
   now lives in the card down in the brown section. */
.phouse__band { display: grid; place-items: center; }
.phouse__lockup {
  display: block;
  /* Live runs this at 800px in a 1312px content width. */
  width: min(100%, 26rem);
  height: auto;
  margin-inline: auto;
}

.phouse__card {
  background-color: var(--ph-cream);
  border-radius: var(--radius-xl);
  padding: clamp(1.75rem, 1.25rem + 3vw, 3rem);
  color: var(--ph-bark);
  /* Pinned, like the ministry band's inner: this copy sits on the event's own
     surface, so it must not follow the page into a theme flip. */
  --text-primary: var(--ph-bark);
  --text-secondary: color-mix(in srgb, var(--ph-bark) 88%, transparent);
  --text-muted: var(--ph-bark);
  --border-subtle: color-mix(in srgb, var(--ph-bark) 30%, transparent);
}
/* The Psalm. A rule and a citation rather than quote marks at display size —
   the copy already carries its own curly quotes, and a decorative glyph on top
   of them reads as two sets. */
/* On the brown band the card leads, and the times/photo pair follows it. */
.phouse__times .phouse__card { margin-bottom: var(--space-xl); }

/* Two columns inside the card: the statement, then the Psalm set against it.
   Stacked, the card ran the prose down one side and left the other half empty,
   which is a lot of cream doing nothing. Splitting it solves both problems at
   once — the quote fills the space, and the column caps the lede's measure
   without a max-width doing it by hand. */
@media (min-width: 52rem) {
  .phouse__card {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 0.82fr);
    gap: var(--space-2xl);
    /* Centred against the taller column, so a short quote sits with the
       paragraph rather than hanging off the top of it. */
    align-items: center;
  }
}
.phouse__card-body > .lead { max-width: 62ch; }

/* Stacked it sits under the prose with a rule above it; beside the prose the
   rule turns vertical, which is what actually separates two columns. */
.phouse__quote {
  margin: var(--space-lg) 0 0;
  padding-top: var(--space-md);
  border-top: 1px solid var(--border-subtle);
}
@media (min-width: 52rem) {
  .phouse__quote {
    margin: 0;
    padding: var(--space-2xs) 0 var(--space-2xs) var(--space-xl);
    border-top: 0;
    border-left: 1px solid var(--border-subtle);
  }
}
/* A pull quote now rather than a footnote, so it is set above body size — but
   under the title, which is still the loudest thing on the card. */
.phouse__quote p {
  font-style: italic;
  font-size: clamp(1.0625rem, 0.95rem + 0.6vw, 1.5rem);
  line-height: var(--leading-snug);
  text-wrap: pretty;
  color: var(--text-secondary);
}
.phouse__cite {
  display: block;
  margin-top: var(--space-sm);
  font-family: var(--font-condensed);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  font-style: normal;
}

/* ---- the gathering times: bordered panel beside a photograph ----------- */
.phouse__times {
  background-color: var(--ph-brown);
  color: var(--ph-cream);
  --text-primary: var(--ph-cream);
  --text-secondary: var(--ph-sand);
  --text-muted: var(--ph-sand);
}
.phouse__split {
  display: grid;
  gap: var(--space-lg);
  align-items: stretch;
}
@media (min-width: 52rem) {
  /* 40/60, as live sets it. */
  .phouse__split { grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); }
}
.phouse__panel {
  border: 1px solid color-mix(in srgb, var(--ph-sand) 45%, transparent);
  border-radius: var(--radius-xl);
  padding: clamp(1.5rem, 1.25rem + 2vw, 2.5rem);
  display: grid;
  align-content: center;
}
/* The cadence stated once, then the dates grouped by month.

   Ten rows of "Wed, <date> at 12 PM" is the same two facts retyped ten times,
   and the reader has to diff them to find the one thing that varies. The rule
   line carries the weekday and the hour; the rows carry only what changes. */
.phouse__cadence {
  margin: var(--space-2xs) 0 var(--space-lg);
  color: var(--ph-sand);
}
.phouse__dates {
  margin: 0;
  display: grid;
  gap: var(--space-2xs);
}
.phouse__row {
  display: grid;
  /* Wide enough for a month plus the year marker the new year carries. */
  grid-template-columns: 4.75rem 1fr;
  align-items: baseline;
  gap: var(--space-sm);
  padding-block: var(--space-3xs);
  border-top: 1px solid color-mix(in srgb, var(--ph-sand) 22%, transparent);
}
.phouse__row:first-child { border-top: 0; }
/* Condensed, which is exactly what the minority face is for here: a compressed
   label sitting beside numerals. */
.phouse__mon {
  font-family: var(--font-condensed);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--ph-sand);
}
.phouse__days {
  font-family: var(--font-condensed);
  font-size: var(--text-lg);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-ui);
  color: var(--ph-cream);
  font-variant-numeric: tabular-nums;
}
/* The year, where it changes. Quieter than the month it follows — it is a
   boundary marker, not a second label. */
.phouse__yr {
  margin-left: 0.4em;
  color: color-mix(in srgb, var(--ph-sand) 65%, transparent);
}

.phouse__days .sep { color: color-mix(in srgb, var(--ph-sand) 55%, transparent); }

/* Where, and how to get there. The dates above all silently assume it. */
.phouse__where {
  margin-top: var(--space-lg);
  padding-top: var(--space-md);
  border-top: 1px solid color-mix(in srgb, var(--ph-sand) 30%, transparent);
}
.phouse__campus {
  font-weight: var(--weight-bold);
  color: var(--ph-cream);
}
.phouse__addr {
  margin-top: var(--space-3xs);
  font-size: var(--text-sm);
  color: var(--ph-sand);
}
.phouse__where .arrowlink { margin-top: var(--space-sm); color: var(--ph-cream); }
/* ---- Ken Burns gallery -------------------------------------------------
   Three photographs, each drifting slowly while it holds, crossfading into the
   next, with a hairline ring counting down to the handover.

   Driven entirely by CSS, and that is the point rather than a preference: the
   ring and the fades sit on ONE timeline, so they cannot drift apart the way a
   JS interval and a CSS transition eventually do. Everything below derives from
   two numbers — the slot and the fade — so retiming the whole thing is two
   custom properties.

   The layering is the fiddly part and is worth stating. A plain crossfade of
   two half-opaque images does NOT composite to full coverage: at the midpoint
   you get 0.5·top + 0.25·bottom + 0.25·ground, and the ground shows through as
   a dip. So the outgoing slide holds at full opacity underneath and is cut only
   once the incoming one has finished arriving on top of it — which needs the
   incoming to be the topmost layer for the length of its fade. z-index is
   animated in discrete steps to do exactly that: 3 while arriving, 2 while
   holding, 1 while waiting. Only ever one slide is arriving, so the top of the
   stack is never contested.
   ------------------------------------------------------------------------ */
.kbgal {
  --kb-slot: 8s;                     /* how long each photograph holds   */
  --kb-fade: 1.5s;                   /* the crossfade                    */
  --kb-loop: calc(var(--kb-slot) * 3);
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border-radius: var(--radius-xl);
  min-height: 16rem;
  height: 100%;
  background-color: var(--ph-brown);
}
.kbgal__slide {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  will-change: opacity, transform;
  animation:
    kb-fade var(--kb-loop) linear infinite,
    kb-layer var(--kb-loop) step-end infinite,
    kb-drift var(--kb-loop) linear infinite;
  /* Slide 0 is already holding at load (its fade is behind it); the other two
     arrive on schedule. One expression covers both cases. */
  animation-delay: calc(var(--i) * var(--kb-slot) - var(--kb-fade));
}
/* Each photograph drifts from its own corner. Same motion on all three reads
   as a slideshow effect; three different origins read as three photographs. */
.kbgal__slide:nth-child(1) { transform-origin: 50% 45%; }
.kbgal__slide:nth-child(2) { transform-origin: 28% 70%; }
.kbgal__slide:nth-child(3) { transform-origin: 72% 35%; }

/* 6.25% = the 1.5s fade over the 24s loop; 39.58% = that fade plus one 8s
   slot. The drop at 39.59% is invisible: the next slide is fully opaque and
   on top of it by then. */
@keyframes kb-fade {
  0%              { opacity: 0; }
  6.25%, 39.58%   { opacity: 1; }
  39.59%, 100%    { opacity: 0; }
}
@keyframes kb-layer {
  0%      { z-index: 3; }   /* arriving — must be above the one it replaces */
  6.25%   { z-index: 2; }   /* holding                                      */
  39.59%  { z-index: 1; }   /* waiting                                      */
}
/* Barely a zoom. 4% over eight seconds is the difference between a photograph
   that breathes and one that is being panned across. */
@keyframes kb-drift {
  0%              { transform: scale(1); }
  39.58%          { transform: scale(1.04); }
  39.59%, 100%    { transform: scale(1); }
}

/* The ring. One slot long, so it empties exactly as the handover completes.
   22px, a 1.5px hairline, and a shadow rather than a plate behind it — a chip
   would announce itself, and this only has to be findable once. */
.kbgal__ring {
  position: absolute;
  right: clamp(0.75rem, 2%, 1.25rem);
  bottom: clamp(0.75rem, 2%, 1.25rem);
  z-index: 4;
  width: 22px;
  height: 22px;
  /* Starts the sweep at twelve o'clock instead of three. */
  rotate: -90deg;
  filter: drop-shadow(0 1px 2px rgb(0 0 0 / 0.45));
  pointer-events: none;
}
.kbgal__track {
  fill: none;
  stroke: rgb(255 255 255 / 0.28);
  stroke-width: 1.5;
}
.kbgal__sweep {
  fill: none;
  stroke: rgb(255 255 255 / 0.85);
  stroke-width: 1.5;
  stroke-linecap: round;
  /* r=9.25 → circumference 58.12. Declared rather than measured at runtime, so
     the ring needs no script to initialise. */
  stroke-dasharray: 58.12;
  animation: kb-sweep var(--kb-slot) linear infinite;
}
@keyframes kb-sweep {
  from { stroke-dashoffset: 58.12; }
  to   { stroke-dashoffset: 0; }
}

/* Motion turned down: the first photograph, and nothing else. The countdown is
   removed rather than frozen — a progress ring that never advances is a
   broken control, not a still one. */
@media (prefers-reduced-motion: reduce) {
  .kbgal__slide { animation: none; opacity: 0; }
  .kbgal__slide:first-child { opacity: 1; }
  .kbgal__ring { display: none; }
}


/* ==========================================================================
   REVEAL — entrance micro-animation
   One primitive, applied deliberately. The temptation is to reveal everything;
   the result of that is a page that assembles itself in front of the reader
   and never settles. It is used on section-level content only — headings,
   groups of cards, the footer columns — never on individual words, icons or
   list items.

   Three rules keep it from reading as a template:
     1. The distance is small. 10px, not 40. It should register as the content
        settling, not travelling.
     2. It happens ONCE. Re-animating every time an element scrolls back into
        view is the single clearest tell of a stock scroll library.
     3. Transform and opacity only, so it never triggers layout while scrolling.
   ========================================================================== */
[data-reveal] {
  opacity: 0;
  transform: translate3d(0, 10px, 0);
  transition: opacity var(--dur-reveal) var(--ease-reveal),
              transform var(--dur-reveal) var(--ease-reveal);
}
[data-reveal][data-revealed="true"] {
  opacity: 1;
  transform: none;
}

/* Large blocks travel further. 10px is right for a heading, but on a card
   several hundred pixels tall the same distance is proportionally invisible —
   the element fades without ever appearing to move. The travel has to scale
   with the thing travelling, and the duration with it, or the longer distance
   just moves faster and undoes the calm.

   18px over 620ms, down from 26 over 900. The old pair was tuned on a desktop
   grid of three, where a card is a small tile in a field of them. In one
   column on a phone a card is 420px tall and there are twenty-one of them —
   the About team list — so the same entrance runs for over half a second on
   every one of them, and a flick leaves four cards in different states of
   arriving at once. Shortening the beat is what stops the column shimmering;
   the shorter travel keeps the curve's shape at the shorter beat. */
[data-reveal="card"] {
  transform: translate3d(0, 18px, 0);
  transition-duration: 620ms;
}
[data-reveal="card"][data-revealed="true"] { transform: none; }

/* Media settles from a fractional scale rather than sliding — an image that
   travels reads as a slide deck, one that settles reads as focus. */
[data-reveal="media"] { transform: scale(1.02); }
[data-reveal="media"][data-revealed="true"] { transform: none; }

/* Grouped children arrive in sequence. The delay is set per child by script so
   the count is not baked into a stylesheet. */
[data-reveal-group] > * {
  opacity: 0;
  transform: translate3d(0, 10px, 0);
  transition: opacity var(--dur-reveal) var(--ease-reveal),
              transform var(--dur-reveal) var(--ease-reveal);
}
[data-reveal-group][data-revealed="true"] > * {
  opacity: 1;
  transform: none;
}

/* Already read by the time the observer fired. A reader who flicks three
   screens at once outruns the callback, and replaying an entrance on a block
   that is level with their eyes — or already above it — is the animation
   arriving to describe something that has finished happening. site.js marks
   those and they simply appear.

   Setting the flag and the revealed state in the same task is what makes this
   silent: a transition is read off the AFTER-change style, so a zero duration
   there means no transition is started at all. */
[data-reveal][data-reveal-instant],
[data-reveal-group][data-reveal-instant] > *,
/* Two attributes so it outranks `[data-rise="fade"] .rise__inner` below —
   the rise carries its own duration and per-line delay, and a heading that
   appears silently must not still be dealing its lines out one at a time. */
[data-rise][data-reveal-instant] .rise__inner {
  transition-duration: 0s;
  transition-delay: 0s;
}

/* Nothing moves, and nothing is hidden waiting to. Reveals are decoration; the
   content is the point. */
@media (prefers-reduced-motion: reduce) {
  [data-reveal],
  [data-reveal-group] > * {
    opacity: 1 !important;
    transform: none !important;
    transition: none !important;
  }
}

/* --- interaction polish ---------------------------------------------------
   Presses and hovers, kept to the smallest change that still registers. */
.btn { transition: background-color var(--dur-fast) var(--ease-out),
                   border-color var(--dur-fast) var(--ease-out),
                   color var(--dur-fast) var(--ease-out),
                   transform 120ms var(--ease-out); }
.btn:active { transform: scale(0.975); }

/* The card's own edge responds; nothing lifts or scales. A shadow bloom on
   hover is the most over-used gesture on the web and says nothing that a
   border darkening does not. */
.card { transition: border-color var(--dur-normal) var(--ease-out),
                    background-color var(--dur-normal) var(--ease-out); }

.reel__card,
.showcase__thumb,
.footer__place svg { transition: transform var(--dur-normal) var(--ease-out); }

/* ==========================================================================
   RISE — line-by-line heading entrance
   Each line sits in its own clipping box so it can travel up from behind its
   own baseline. The lines are separate elements, not <br> breaks: a text node
   cannot be transformed on its own.

   The padding/negative-margin pair matters. `overflow: hidden` cuts at the box
   edge, which sits ON the baseline — so descenders (g, y, p) get sheared off.
   The box is extended below the baseline and pulled back by the same amount,
   which restores the descender without changing the layout.
   ========================================================================== */
.rise { display: block; }
.rise__line {
  display: block;
  overflow: hidden;
  padding-bottom: 0.14em;
  margin-bottom: -0.14em;
}
.rise__inner {
  display: block;
  /* Step and duration are variables so line-rise and word-rise are the same
     mechanism at two scales. A word is a smaller unit than a line, so it takes
     a tighter stagger — 64ms between words reads as a stutter. */
  transition-delay: calc(var(--line, 0) * var(--rise-step, 64ms));
}

/* Each line travels up from behind its own baseline while fading in. The fade
   softens the start so the movement never reads as mechanical; the travel is
   what makes the line feel like it is arriving rather than simply appearing. */
[data-rise="fade"] .rise__inner {
  transform: translateY(110%);
  opacity: 0;
  transition-property: transform, opacity;
  transition-duration: var(--rise-dur, 620ms);
  transition-timing-function: var(--ease-reveal);
}

/* --- word rise (interior page heroes) ------------------------------------
   The masking box is per word rather than per line. vertical-align: bottom is
   load-bearing: an inline-block with overflow:hidden aligns its bottom MARGIN
   edge to the baseline by default, and with the descender fix pulling that
   margin up by 0.1em every word would sit a tenth of an em high. */
/* Tighter than the line rise on both counts, and it has to be: a heading of
   ten words on three rows accumulates nine steps before the last one starts,
   so the stagger compounds in a way a three-line heading never does. At 60ms
   over 780ms the last word of the About statement landed 1.3s after the first
   — long enough that the reader finishes the sentence before the sentence
   finishes arriving. 44/540 puts the whole heading down in under 950ms. */
.rise--words { --rise-step: 44ms; --rise-dur: 540ms; }
/* Authored line breaks. A block row per line keeps the break at every width,
   rather than letting the measure decide where a three-line statement turns. */
.rise__row { display: block; }
/* The brand rule under a word, not the word in the brand colour — sage as
   letterforms is 1.97:1 on paper. Thickness in em so it tracks the type
   through the hero's clamp instead of thinning out as the heading grows. */
.rise__mark {
  /* Bold AND the rule. The sentence around it is medium, so weight is what
     picks these words out at a glance and the sage rule is what colours them —
     sage as letterforms is 1.97:1 on paper, so it cannot do the job alone. */
  font-weight: var(--weight-bold);
  text-decoration: underline;
  text-decoration-color: var(--accent-mark);
  text-decoration-thickness: 0.07em;
  text-underline-offset: 0.1em;
}
.rise__word {
  display: inline-block;
  overflow: hidden;
  padding-bottom: 0.1em;
  margin-bottom: -0.1em;
  vertical-align: bottom;
}

/* Everything around the title only fades. One element moves per screen — the
   heading — and the supporting copy arrives after it rather than alongside it,
   so the eye is never asked to track two things at once. */
.page-hero .eyebrow[data-reveal],
.page-hero__lead[data-reveal],
.page-hero__actions[data-reveal] {
  transform: none;
  transition-property: opacity;
}
.page-hero .eyebrow[data-reveal] { transition-delay: 0ms; }
.page-hero__lead[data-reveal]    { transition-delay: 190ms; }
.page-hero__actions[data-reveal] { transition-delay: 270ms; }
[data-rise="fade"][data-revealed="true"] .rise__inner {
  transform: none;
  opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
  .rise__inner {
    transform: none !important;
    opacity: 1 !important;
    clip-path: none !important;
    transition: none !important;
  }
  .rise__word { overflow: visible; }
}

/* ==========================================================================
   VIDEO BAND
   A 16:9 film sitting on the deep surface, the way the live Plan A Visit page
   opens. Distinct from the hero video: that one is a background that must
   cover an arbitrary box, this one IS the content and keeps its own ratio.
   ========================================================================== */
.videoband { background-color: var(--surface-deep); }
.videoband__frame {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background-color: var(--gl-black);
}
.videoband__poster,
.videoband__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.videoband__poster { object-fit: cover; }
/* The poster is a complete experience on its own — it is the film's own first
   frame — so the iframe fades over it only once it has actually loaded. With
   reduced motion or save-data the script never loads it and the poster simply
   stays, which is why it must be a real image and not a placeholder. */
.videoband__video {
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease-out);
}
.videoband__video[data-ready="true"] { opacity: 1; }
.videoband__video iframe {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  border: 0;
}

/* ==========================================================================
   LAYER CARD
   Three layers: the photograph blurred as the card's ground, a scrim, and a
   sharp thumbnail of the same picture sitting on top at full strength. Copy
   sits on the blur beneath it.

   The blur does the job a flat tint would, but carries the image's own palette
   into the card — which is what makes the thumbnail read as belonging to the
   card rather than pasted onto it. Every card is coloured by its own photograph
   instead of by a colour the system had to invent.

   --thumb-ratio is the only thing instances vary: 4/5 for the visit steps,
   1/1 for the team. Everything else is shared, so a fix to the blur or the
   scrim lands in both.
   ========================================================================== */
.layercard {
  position: relative;
  isolation: isolate;
  /* A grid with one row, so the inner box inherits the card's full height.
     As a plain block container the card stretched to its row while the inner
     box stayed at content height — 402px against 381px — which left 21px of
     dead space at the foot and gave `margin-top: auto` nothing to push
     against, so the link stopped short in exactly the cards whose copy ran
     short. The height has to be handed down before anything can be pinned to
     the bottom of it. */
  display: grid;
  grid-template-rows: minmax(0, 1fr);
  border-radius: var(--radius-xl);
  overflow: hidden;
  background-color: var(--surface-media);
  /* clip-path as well as overflow, and it is not redundant. The reveal animates
     transform and opacity, which promotes the card to its own compositing
     layer — and a rounded OVERFLOW clip is not reliably applied to composited
     descendants, so the card faded in with hard square corners and only
     snapped round once the animation settled. The blur inside carries its own
     filter layer, which makes it likelier still.

     clip-path is a geometric clip rather than a paint-time one: it survives
     compositing, so the corners are round on the first frame. overflow stays
     for anything that escapes horizontally. */
  clip-path: inset(0 round var(--radius-xl));
}
/* A plain box whose only job is to clip. The blur lives inside it rather than
   being clipped by the card, because the card would be clipping a FILTERED
   layer through its own border-radius — which WebKit does not do reliably, and
   which showed as the blur's square corners cutting the card's rounded ones. */
.layercard__bg {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  overflow: hidden;
  /* Matches the card exactly (inset: 0), so the same shape. Belt and braces:
     the filtered image inside is the layer most likely to lose an ancestor's
     rounded clip. */
  clip-path: inset(0 round var(--radius-xl));
  z-index: 0;
}
/* Overscan has to clear the BLUR RADIUS, not just the box. filter: blur()
   samples beyond the pixels it is given and finds nothing past the image's own
   edge, so the result fades toward transparent there. A Gaussian needs roughly
   3x its radius to fall off invisibly; at 8% it was one radius, and the falloff
   read as a hard rectangle set in from the card's edge. */
.layercard__blur {
  position: absolute;
  inset: -24%;
  width: 148%;
  height: 148%;
  max-width: none;             /* the reset's img{max-width:100%} caps this */
  object-fit: cover;
  filter: blur(24px) saturate(1.15);
}
/* A blurred photograph is still a photograph: how bright it lands depends
   entirely on which one. The scrim guarantees the copy's contrast, and sits
   between the blur and the content — the thumbnail stays at full strength. */
.layercard__bg::after {
  content: "";
  position: absolute;
  inset: 0;
  background-color: rgb(28 25 23 / 0.62);
}
.layercard__inner {
  position: relative;
  z-index: 1;
  display: grid;
  grid-template-rows: auto 1fr;   /* content takes the slack, so feet line up */
  gap: var(--space-sm);
  padding: var(--space-sm);
}
/* All four corners, matching the card's own geometry rather than squaring off
   against an edge it never touches. */
.layercard__thumb {
  display: block;
  width: 100%;
  aspect-ratio: var(--thumb-ratio, 4 / 5);
  object-fit: cover;
  border-radius: var(--radius-lg);
}
/* Re-points the text tokens rather than colouring each child. Anything dropped
   in here — a link-arrow, a tag, a button — resolves light without knowing it
   is on a photograph. */
.layercard__content {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  padding: 0 var(--space-2xs) var(--space-2xs);
  color: var(--gl-stone-50);
  --text-primary: var(--gl-stone-50);
  --text-secondary: rgb(250 250 249 / 0.82);
  --text-muted: rgb(250 250 249 / 0.66);
  --text-accent: var(--gl-stone-50);
  --accent-mark: var(--gl-sage-400);
  --border-control: rgb(250 250 249 / 0.55);
}

/* --- the visit steps ----------------------------------------------------- */
.stepcards {
  display: grid;
  gap: var(--space-md);
  list-style: none;
  margin: 0;
  padding: 0;
}
@media (min-width: 40rem) { .stepcards { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-xl); } }
@media (min-width: 64rem) { .stepcards { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.stepcard { --thumb-ratio: 4 / 5; }
.stepcard__title {
  font-family: var(--font-sans);
  font-size: var(--text-xl);
  font-weight: var(--weight-bold);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-normal);
  text-transform: none;
  color: inherit;
}
.stepcard__body {
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: var(--text-secondary);
}
/* Pushed to the floor so the buttons align across the row however long the copy
   above them runs. Full width, because a pill floating in the middle of a
   card's foot reads as unfinished. */
.layercard__content .btn {
  width: 100%;
  margin-top: auto;
  padding-top: var(--space-sm);
  padding-bottom: var(--space-sm);
}


/* ==========================================================================
   FAQ SPLIT
   Questions carry the column; the three cards beside them are the answers
   people reach for once the questions run out. 1.85:1, matching the live page.
   ========================================================================== */
.faq-split { display: grid; gap: var(--space-xl); }
@media (min-width: 64rem) {
  .faq-split {
    grid-template-columns: minmax(0, 1.85fr) minmax(0, 1fr);
    gap: var(--space-2xl);
    align-items: start;
  }
}
/* Centred while stacked, because a lone column reads as its own thing. Once
   the aside column appears the heading is labelling the LEFT column rather than
   the section, and a centred label over an off-centre column looks like a
   mistake. */
@media (min-width: 64rem) {
  .faq-split > :first-child .section-head--center {
    justify-items: start;
    text-align: left;
  }
  .faq-split > :first-child .section-head--center > * { max-width: none; }
}

.sidecards { display: grid; gap: var(--space-md); }
.sidecard {
  padding: var(--space-md);
  border-radius: var(--radius-xl);
  background-color: var(--surface-raised);
  display: grid;
  gap: var(--space-2xs);
  justify-items: start;
}
.sidecard__title {
  font-family: var(--font-sans);
  font-size: var(--text-xl);
  font-weight: var(--weight-bold);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-normal);
  text-transform: none;
}
.sidecard__body { font-size: var(--text-sm); color: var(--text-secondary); }
.sidecard .btn { margin-top: var(--space-2xs); }

/* ==========================================================================
   STICKY SIDE
   A section's thesis holds in the left column while its evidence scrolls past
   on the right. Used five times down About, which is what makes it a component
   rather than a layout: the page has one idea about how a section explains
   itself, and every section states it the same way.

   Two parts, and the second is easy to lose: the outer column supplies the
   scroll range and the inner box does the sticking. A sticky element only
   travels inside its containing block, so making the aside itself sticky
   without a full-height column gives it nowhere to go. Here the grid cell IS
   the range — the body beside it is always taller — so no min-height has to be
   invented, unlike the homepage showcase intro.
   ========================================================================== */
.stickyside { display: grid; gap: var(--space-lg); }
@media (min-width: 64rem) {
  .stickyside {
    /* 465 / 863 with a 48px gutter, matching the live page. */
    grid-template-columns: minmax(0, 465fr) minmax(0, 863fr);
    gap: var(--space-xl);
    align-items: start;
  }
  .stickyside__aside {
    position: sticky;
    top: calc(var(--header-h) + var(--space-lg));
    align-self: start;        /* without it the cell stretches and never sticks */
  }
}
.stickyside__aside {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  padding-block: var(--space-md);
}

/* The whole thesis as one sentence at display size — not an eyebrow, a headline
   and a paragraph. Medium weight rather than bold, with slightly open tracking:
   this is a statement to be read, not a label to be scanned, and bold at 34px
   over four lines reads as shouting. */
.stickyside__lede {
  font-family: var(--font-sans);
  font-size: var(--stickyside-lede, 2.125rem);
  font-weight: var(--weight-medium);
  /* The bold sits INSIDE the sentence, not on it. Each lede opens with the
     phrase that names the section — "Our values", "Grumlaw Seven" — and that
     phrase carries the weight while the rest of the statement stays medium.
     It is the whole reason the lede is a sentence rather than a label: the
     heading and the explanation are the same run of words, and the bold is
     what lets you read it either way. Declared explicitly because the parent
     is 500, and the browser default would otherwise inherit rather than jump. */
  line-height: 1.2;
  letter-spacing: 0.015em;
  text-transform: none;
  color: var(--text-primary);
  margin: 0;
}
.stickyside__lede strong { font-weight: var(--weight-bold); }
/* Centred in a section head it is a heading, not a sticky aside: it needs a
   measure so it breaks on its own terms, and room under it. */
.section-head--center .stickyside__lede {
  max-width: 24ch;
  margin-inline: auto;
  text-wrap: balance;
}
@media (max-width: 47.999rem) {
  .stickyside__lede { --stickyside-lede: 1.625rem; }
}
/* Full-bleed 3px rule under the lede. Heavier than --border-width on purpose —
   it closes the statement rather than separating two things, so it reads as
   punctuation, and a hairline would look like a stray divider. */
.stickyside__rule {
  border: 0;
  height: 3px;
  margin: 0;
  /* --text-primary, so it follows the section's ground: ink on paper and on
     the tint, paper on the ink section. The live page hard-codes #000 and
     #FAFAF9 as two separate rules; one token covers both and dark mode too. */
  background-color: var(--text-primary);
}

/* ==========================================================================
   DEFINITION ITEMS
   Title, body, and optionally a citation or a link. Carries the values, the
   strategy and the beliefs — three lists that differ in content and column
   count, not in kind, so they share one component rather than three.
   ========================================================================== */
.deflist { display: grid; gap: var(--space-lg); list-style: none; margin: 0; padding: 0; }
/* Not cards. On a tinted ground four raised panels read as a wall of objects
   rather than as the four parts of one evening — the section already has a
   surface, and these are its content, not things placed on it. The padding
   goes with the panel; only the rhythm between items separates them now. */
.deflist--plain .defitem {
  padding: 0;
  border-radius: 0;
  background-color: transparent;
}

/* Bold: dark cards, as live sets them. Each of these is a door — a form, an
   inbox, a reading list — not a description of one, and a door should look
   like an object you can go through. --surface-deep rather than live's
   stone-800 for the same reason the pathway uses it: on a tinted section a
   stone-800 card is 1.03:1 against its own ground in dark mode, which is
   invisible. Deep is stone-950 on paper and true black in the dark, so the
   card reads as a slab in both.

   Live runs 32px padding and a 32px radius, which are --space-lg and
   --radius-xl here, so the geometry matches without inventing a value. */
.deflist--bold .defitem {
  padding: var(--space-lg);
  border-radius: var(--radius-xl);
  background-color: var(--surface-deep);
  color: var(--gl-stone-50);
  --text-primary: var(--gl-stone-50);
  --text-secondary: rgb(250 250 249 / 0.78);
  --text-muted: rgb(250 250 249 / 0.62);
  --accent-mark: var(--gl-sage);
  --uline-color: var(--gl-sage);
}
/* Held to the same measure as the trips list. Both are a short run of items
   that a reader scans down a column, and at the full container width the two
   sections would sit at different widths on adjacent pages for no reason a
   reader could name. */
.deflist--bold { max-width: var(--measure-list); margin-inline: auto; }
@media (min-width: 48rem) {
  .deflist--bold { gap: var(--space-lg); }
}
@media (min-width: 48rem) {
  .deflist--plain { gap: var(--space-xl) var(--space-2xl); }
}
@media (min-width: 48rem) {
  .deflist--two { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-md); }
}
.defitem {
  display: flex;
  flex-direction: column;
  gap: var(--space-2xs);
  padding: var(--space-md);
  border-radius: var(--radius-xl);
  background-color: var(--surface-raised);
}
/* The single-column variant is reading matter, not a card grid — it drops the
   fill and takes a rule instead, so seven beliefs do not read as seven boxes.

   gap: 0 is the point. The list's 32px grid gap stacks ON TOP of each item's
   own padding, which put 56px between a verse and the rule below it and 24px
   between that rule and the next title — the hairline sat twice as far from
   what it followed as from what it introduced. With no gap the padding alone
   sets the rhythm and the rule sits centred in it. Cards need a gap because
   nothing else separates them; here the rule is the separator. */
.deflist--flow { gap: 0; }
.deflist--flow .defitem {
  background-color: transparent;
  padding: var(--space-md) 0;
  border-radius: 0;
  border-top: var(--border-width) solid var(--border-subtle);
}
.deflist--flow .defitem:first-child { border-top: 0; padding-top: 0; }
/* Sized by height, because the set is not on a common grid — the viewBoxes run
   from 303x300 to 373x261, so a fixed square box would letterbox some and crop
   others. Height plus auto width lets each one keep its own proportions, which
   is the whole point of a hand-drawn set. */
/* Ink, not sage. The live set fills #252525 — the rule lived in each icon's
   <defs>, which the extraction stripped, so they came through inheriting
   currentColor and picked up the accent. --text-primary matches at 40,40,39 and
   adapts to dark mode, which a hard-coded hex would not.

   Square box, also matching live: the viewBoxes run 303x300 to 373x261, and a
   uniform footprint keeps fourteen icons aligned down two columns. The artwork
   centres inside it on its own (preserveAspectRatio defaults to meet).

   align-self is load-bearing. .defitem is a column flex container, so the
   default `align-items: stretch` pulled every icon to the full card width.
   Same trap as the reel cards, the footer icon and the badge artwork. */
.defitem__icon {
  align-self: start;
  width: 3.25rem;
  height: 3.25rem;
  color: var(--text-primary);
  margin-bottom: var(--space-2xs);
}

/* Medium, matching the live page. The only bold on this screen is the phrase
   opening each lede — if the item titles are bold too, that contrast is spent
   and the ledes stop reading as the thing they name. */
.defitem__title {
  font-family: var(--font-sans);
  font-size: var(--text-xl);
  font-weight: var(--weight-medium);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-normal);
  text-transform: none;
}
.defitem__body { font-size: var(--text-sm); color: var(--text-secondary); }
/* Scripture references: the condensed face doing exactly what it is for —
   a dense structural string of numerals that must not read as prose. */
/* --text-secondary, not --text-muted. At 12px these are reading text — someone
   looks up these references — and muted measured 3.22:1 on the tinted ground.
   Muted is for labels you skim, not strings you transcribe. */
.defitem__cite {
  font-family: var(--font-condensed);
  font-size: var(--text-xs);
  color: var(--text-secondary);
  font-style: normal;
}
/* Hug the label. As a flex item of a column container it stretched the whole
   card width, which put the hover underline and the hit target across empty
   space to the right of the arrow. */
.defitem .arrowlink,
.defitem .link-arrow { margin-top: var(--space-2xs); align-self: start; }
/* Auto margin so the seven values' CTAs align across the grid however long the
   copy above them runs. */
.defitem__body + .arrowlink,
.defitem__body + .link-arrow { margin-top: auto; padding-top: var(--space-2xs); }

/* ==========================================================================
   MEDIA CARD
   4:5 image with the title and its action sitting ON the picture. These are
   series artwork — each one already carries the word as lettering — so putting
   the label underneath repeated what the image was saying and pushed the
   action a card's height away from the thing it acts on.
   ========================================================================== */
.mediacards { display: grid; gap: var(--space-md); list-style: none; margin: 0; padding: 0; }
@media (min-width: 40rem) { .mediacards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.mediacard {
  position: relative;
  isolation: isolate;
  display: grid;
  align-content: end;          /* content sits on the floor of the picture */
  aspect-ratio: 4 / 5;
  border-radius: var(--radius-xl);
  overflow: hidden;
  background-color: var(--surface-media);
}
.mediacard__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  z-index: 0;
}
/* Every stop is the same ink with only alpha moving. Fading to `transparent`
   would fade to transparent BLACK regardless of the colour above it, dragging
   a grey cast through the midtones of the picture — the trap the header
   gradients hit. Four stops rather than two so the ramp clears the copy quickly
   and then lets go, instead of veiling the whole lower half. */
.mediacard::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background: linear-gradient(to top,
    rgb(12 10 9 / 0.9) 0%,
    rgb(12 10 9 / 0.74) 20%,
    rgb(12 10 9 / 0.3) 46%,
    rgb(12 10 9 / 0) 70%);
}
.mediacard__body {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  justify-items: start;
  gap: var(--space-sm);
  padding: var(--space-md);
}
/* Pinned light: this text sits on a photograph, not on the section, so it must
   not follow the section's tokens into a theme flip. */
.mediacard__title {
  font-family: var(--font-sans);
  font-size: var(--text-xl);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-normal);
  text-transform: none;
  line-height: var(--leading-snug);
  color: var(--gl-stone-50);
}
/* --- panel variant -------------------------------------------------------
   No photograph, so nothing to scrim and nothing to sit on: the card becomes a
   flat sage surface and its content sits in normal flow rather than overlaid.
   Sage as a GROUND, which is the one place the accent is allowed to be large —
   and on it the primary action goes ink, per the button rule. */
.mediacard--panel {
  aspect-ratio: auto;
  /* The photo cards use `align-content: end` to sit their copy on the scrim.
     With no photo there is nothing to sit on, so the body has to FILL the card
     and let its own flex layout place things — otherwise the block stays pinned
     to the floor and the heading starts 256px down an empty card.

     An explicit 1fr row, not `align-content: stretch`: the row here is sized by
     its content, and stretch alone left the body at content height with the
     actions floating 256px above the floor. Same lesson as the layer card —
     height has to be handed down before anything can be pinned to the bottom
     of it. */
  grid-template-rows: minmax(0, 1fr);
  align-content: stretch;
  background-color: var(--surface-accent);
  color: var(--text-on-accent);
}
.mediacard--panel::after { content: none; }   /* no scrim without a photo */
.mediacard--panel .mediacard__body {
  justify-content: flex-start;
  gap: var(--space-xs);
  /* Roomier at the sides and top than the photo cards, because this is a block
     of reading text rather than a caption — but the BOTTOM matches theirs
     exactly (--space-md), so all three cards' buttons land on the same line. */
  padding: var(--space-lg) var(--space-lg) var(--space-md);
}
/* Actions on the floor, matching the two photo cards beside it — their buttons
   sit at the bottom because the scrim puts them there, and a row where two
   cards' actions line up and the third's floats mid-card reads as a mistake. */
.mediacard--panel .cluster { margin-top: auto; padding-top: var(--space-md); }
.mediacard--panel .mediacard__title,
.mediacard--panel .mediacard__meta { color: var(--text-on-accent); }
.mediacard--panel .mediacard__meta { font-size: var(--text-sm); }
.mediacard--panel .mediacard__meta--times {
  font-family: var(--font-condensed);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  font-size: var(--text-2xs);
}

/* Address and times, between the title and the action. Pinned light for the
   same reason the title is: this sits on a photograph, not on the section. */
.mediacard__meta {
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
  color: rgb(250 250 249 / 0.86);
}
/* Two large cards rather than a grid of small ones — the picture IS the
   content here, since someone is using it to recognise a building they have
   not been to. Wider cards, and the gap opens up to match. */
@media (min-width: 40rem) {
  .mediacards--wide { gap: var(--space-lg); }
}
@media (min-width: 64rem) {
  .mediacards--wide { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.mediacard .btn--glass {
  --btn-bg: rgb(250 250 249 / 0.18);
  --btn-fg: var(--gl-stone-50);
  --btn-border: rgb(250 250 249 / 0.5);
  --btn-bg-hover: rgb(250 250 249 / 0.3);
}

/* ==========================================================================
   PEOPLE — the layer card at 1:1.
   ========================================================================== */
.people { display: grid; gap: var(--space-md); list-style: none; margin: 0; padding: 0; }
@media (min-width: 40rem) { .people { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 90rem) { .people { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.person { --thumb-ratio: 1 / 1; }
/* No photograph on file. A grey rectangle reads as a broken image, so the
   placeholder states initials instead — and it fills the same 1:1 box, so a
   row with a gap in it still lines up. */
.person__photo--none {
  display: grid;
  place-items: center;
  width: 100%;
  aspect-ratio: var(--thumb-ratio, 1 / 1);
  border-radius: var(--radius-lg);
  font-family: var(--font-condensed);
  font-size: var(--display-sm);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  color: var(--gl-stone-50);
  background-color: rgb(250 250 249 / 0.12);
}
.person__name {
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-normal);
  text-transform: none;
  line-height: var(--leading-snug);
  color: var(--text-primary);
}
.person__role { font-size: var(--text-sm); color: var(--text-secondary); }
/* Pinned to the floor of the card and held on one line.

   margin-top: auto works because .layercard__inner gives the content box the
   row's slack (grid-template-rows: auto 1fr) — so the link drops to the bottom
   of whatever height the row settles at, and the links align across a row
   however many lines the role above them runs to. Same mechanism the step
   cards' buttons use.

   nowrap because "Email Shea →" breaking between the name and the arrow reads
   as two separate things; it is one label. It is short by construction — a
   first name — so it has no business wrapping. */
.person__mail {
  font-size: var(--text-sm);
  margin-top: auto;
  padding-top: var(--space-2xs);
  white-space: nowrap;
}

/* ==========================================================================
   STATEMENT HERO — a long claim on the left, its consequence on the right.
   ========================================================================== */
.statement { display: grid; gap: var(--space-lg); }
@media (min-width: 64rem) {
  .statement { grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); gap: var(--space-2xl);
               align-items: end; }
}
/* Medium, with the bold reserved for the two words that carry the claim. The
   whole statement was 700, which meant nothing inside it could be emphasised —
   and a four-line paragraph at bold display size reads as shouting rather than
   as a mission.

   The ceiling came down from 5rem to 3.75rem because 80px did not fit: the
   authored three lines were wrapping to five, so the line breaks that ARE the
   rhythm of the sentence were being overridden by the measure. A statement
   whose breaks are authored has to be sized to hold them. */
.statement__claim {
  font-family: var(--font-sans);
  font-weight: var(--weight-medium);
  font-size: clamp(2rem, 1.1rem + 3.4vw, 3.75rem);
  line-height: 1.12;
  letter-spacing: var(--tracking-display);
  /* text-wrap: balance would re-break the lines it is given; the breaks here
     are authored, so there is nothing to balance. */
  text-wrap: normal;
}
.statement__claim em { font-style: normal; color: var(--text-accent); }
.statement__aside { font-size: var(--text-xl); color: var(--text-secondary); }


/* Every permanently dark ground, in one place.

   btn--glass takes --btn-fg from --text-primary and tints from
   --surface-inverse, both of which are PAGE tokens. On a card that is dark
   regardless of theme, --text-primary is ink in light mode, so the label
   vanished (1.35:1) — and --surface-inverse being ink in light meant the fill
   was ink-on-black, so the button had no visible shape in EITHER theme
   (1.01:1). It looked fine in dark only because the label happened to land
   light for an unrelated reason.

   Listing the grounds together rather than fixing them one at a time: this is
   the third surface to hit it, and the next dark card would have hit it too. */
.surface-ink .btn--glass,
.msg .btn--glass,
.layercard__content .btn--glass {
  /* Matched to .reel__cta — same film, same blur, no border. One treatment for
     "an action on a dark card", whether that card is the flip side of a reel
     tile, the message card, or a layer card.

     The 18% film reads as roughly 1.5:1 against near-black, which is a shape
     you sense rather than one you measure. That is the flip card's established
     look and it is deliberate here; if these ever need to carry weight on their
     own, the border comes back for all of them at once rather than one card
     drifting from the rest. */
  --btn-bg: rgb(250 250 249 / 0.18);
  --btn-fg: var(--gl-stone-50);
  --btn-border: transparent;
  --btn-bg-hover: rgb(250 250 249 / 0.28);
  -webkit-backdrop-filter: blur(10px) saturate(1.4);
          backdrop-filter: blur(10px) saturate(1.4);
}

/* ==========================================================================
   DIRECTIONS MODAL
   Native <dialog>. Focus trapping, Escape, inerting the page behind it and
   returning focus to the trigger are platform behaviour — reimplementing any
   of it by hand is how modals become inaccessible.
   ========================================================================== */
.dirs {
  /* Centred explicitly. A modal <dialog> is centred by the UA sheet with
     `inset: 0; margin: auto` — and this build's reset opens with
     `* { margin: 0 }`, which beats it on specificity and pins the dialog to
     the top-left corner. Any reset with a universal margin rule breaks native
     dialog centring, silently and only when the dialog is finally opened. */
  position: fixed;
  inset: 0;
  margin: auto;
  width: min(34rem, calc(100vw - 2 * var(--gutter)));
  max-height: min(40rem, calc(100svh - 4rem));
  padding: var(--space-lg);
  border: 0;
  border-radius: var(--radius-xl);
  background-color: var(--surface-raised);
  color: var(--text-primary);
  overflow: auto;
}
.dirs::backdrop {
  /* Explicit ink at low alpha, not `transparent` shading toward black: the
     dialog sits over photographs and a neutral wash keeps their colour. */
  background-color: rgb(12 10 9 / 0.55);
  -webkit-backdrop-filter: blur(3px);
          backdrop-filter: blur(3px);
}
.dirs__head {
  display: flex;
  align-items: start;
  justify-content: space-between;
  gap: var(--space-md);
  margin-bottom: var(--space-md);
}
.dirs__title { font-size: var(--text-xl); font-weight: var(--weight-medium); margin: 0; }
.dirs__close {
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  flex: none;
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.dirs__close:hover { background-color: var(--surface-hover); color: var(--text-primary); }
.dirs__close svg { width: 1.1rem; height: 1.1rem; }

.dirs__list { display: grid; gap: var(--space-md); list-style: none; margin: 0; padding: 0; }
.dirs__item {
  display: grid;
  grid-template-columns: 5rem minmax(0, 1fr);
  gap: var(--space-md);
  align-items: start;
  padding-top: var(--space-md);
  border-top: var(--border-width) solid var(--border-subtle);
}
.dirs__item:first-child { border-top: 0; padding-top: 0; }
/* Locations has a photograph of every building; a ministry does not — Young
   Adults meets in a church the church does not own. Without an <img> the
   detail auto-placed into the 5rem THUMBNAIL column and every line inside it
   wrapped at about 80px. One column here instead, and the rule sits directly
   after the one it overrides: both are single-class, so source order is the
   only thing deciding which wins. */
.dirs__item--plain { grid-template-columns: minmax(0, 1fr); }
.dirs__venue { margin: 0; color: var(--text-secondary); }
.dirs__when {
  margin: var(--space-2xs) 0 0;
  font-family: var(--font-condensed);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
}
/* Square, so the two buildings read as a matched pair rather than two crops. */
.dirs__thumb {
  width: 100%;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: var(--radius-md);
  background-color: var(--surface-media);
}
/* On a phone the dialog is already the full width of the screen minus the
   gutter, and an 80px crop of a building is decoration competing with the
   three things the visitor came for — when, where, and which map app. It goes,
   and the column with it. */
@media (max-width: 47.999rem) {
  .dirs__item { grid-template-columns: minmax(0, 1fr); }
  .dirs__thumb { display: none; }
}
.dirs__detail { display: grid; gap: var(--space-2xs); justify-items: start; }
.dirs__name {
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  letter-spacing: var(--tracking-normal);
  text-transform: none;
  margin: 0;
}
/* The address is the control. A button, not a <p> with a copy icon beside it —
   the whole line is the target, which is easier to hit and says plainly that
   the text itself does something. */
.dirs__address {
  display: flex;
  align-items: baseline;
  gap: var(--space-2xs);
  flex-wrap: wrap;
  padding: 0;
  border: 0;
  background: transparent;
  text-align: left;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  cursor: pointer;
}
.dirs__address:hover { color: var(--text-primary); }
.dirs__copyhint {
  font-family: var(--font-condensed);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
}
.dirs__address:hover .dirs__copyhint,
.dirs__address:focus-visible .dirs__copyhint { color: var(--accent-mark); }
.dirs__address[data-copied="true"] .dirs__copyhint { color: var(--accent-mark); }
.dirs__links { display: flex; flex-wrap: wrap; gap: var(--space-2xs); margin-top: var(--space-3xs); }
/* Announced, not displayed. The button's own Copy -> Copied swap is the visible
   feedback and it sits exactly where the eye already is; a second confirmation
   restating the address underneath was noise for anyone who could see the
   first. Screen readers still need it, so the element stays in the accessibility
   tree via .visually-hidden rather than being removed. */
.dirs__status {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ==========================================================================
   SEGMENTED CONTROL
   One control, several mutually exclusive views.
   ========================================================================== */
.segmented {
  display: inline-flex;
  padding: 3px;
  border-radius: var(--radius-pill);
  background-color: var(--surface-sunken);
  border: var(--border-width) solid var(--border-subtle);
}
.segmented__option {
  border: 0;
  border-radius: var(--radius-pill);
  padding: 0.5rem 1rem;
  background: transparent;
  color: var(--text-secondary);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  cursor: pointer;
  white-space: nowrap;
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.segmented__option:hover { color: var(--text-primary); }
.segmented__option[aria-pressed="true"] {
  background-color: var(--surface-raised);
  color: var(--text-primary);
}
.segmented__option:focus-visible {
  outline: var(--border-width-thick) solid var(--focus-ring);
  outline-offset: 2px;
}

/* ==========================================================================
   TILES
   16:9 artwork with its title beneath. The picture is a promo graphic that
   already carries the event's name as lettering, so the title is NOT set over
   it — it would land on top of type that is already there.
   ========================================================================== */
.tilebar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  margin-bottom: var(--space-lg);
}
/* Announced, not shown. The grid itself is the visible answer to a filter —
   the count only restated what the reader can already see. But a screen reader
   user gets no feedback at all from items silently leaving the DOM, so the live
   region stays and only its pixels go. */
.tilebar__count {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.tiles {
  display: grid;
  gap: var(--space-md);
  list-style: none;
  margin: 0;
  padding: 0;
}
@media (min-width: 40rem) { .tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 64rem) { .tiles { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
/* Filtered-out tiles leave the flow: a grid with holes in it reads as a
   loading state rather than a filtered one. */
.tile[hidden] { display: none; }

.tile__link {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  height: 100%;
  text-decoration: none;
  color: inherit;
}
.tile__img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  border-radius: var(--radius-lg);
  background-color: var(--surface-media);
  transition: transform var(--dur-normal) var(--ease-out);
}
.tile__title {
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-normal);
  text-transform: none;
  color: var(--text-primary);
  margin: 0;
}
/* The rule is on the inner span so it tracks the text rather than the block,
   and slides the way every other link on the site does. */
.tile__title > span {
  position: relative;
  --uline-bottom: -0.16em;
}
.tile__title > span::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  bottom: var(--uline-bottom);
  height: 2px;
  border-radius: 2px;
  background-color: var(--accent-mark);
  transform: scaleX(0);
  transform-origin: right center;
  transition: transform var(--dur-normal) var(--ease-out);
}
.tile__link:hover .tile__title > span::after,
.tile__link:focus-visible .tile__title > span::after {
  transform: scaleX(1);
  transform-origin: left center;
}
/* A restrained lift on the artwork — 1.5%, which reads as a response rather
   than a zoom. Pointer devices only: on touch there is no hover to leave. */
@media (hover: hover) and (pointer: fine) {
  .tile__link:hover .tile__img { transform: scale(1.015); }
}
@media (prefers-reduced-motion: reduce) {
  .tile__img { transition: none; }
  .tile__link:hover .tile__img { transform: none; }
}

/* ==========================================================================
   HUB CARDS
   A springboard: artwork, name, one line of what it is. Used for the six
   ministries. Distinct from the tile (which is promo artwork with a caption)
   and from the layer card (which is a photograph) — this one leads with a
   MARK, so the art sits in a panel of its own rather than bleeding to the
   card's edge.
   ========================================================================== */
.hubcards {
  display: grid;
  gap: var(--space-md);
  list-style: none;
  margin: 0;
  padding: 0;
}
@media (min-width: 40rem) { .hubcards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 64rem) { .hubcards { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

.hubcard { display: grid; }
.hubcard__link {
  display: grid;
  grid-template-rows: auto 1fr;   /* body takes the slack, so cards match */
  gap: var(--space-md);
  height: 100%;
  padding: var(--space-md);
  border-radius: var(--radius-xl);
  background-color: var(--surface-raised);
  text-decoration: none;
  color: inherit;
  transition: transform var(--dur-normal) var(--ease-out);
}
/* The panel behind the mark. Ink rather than the page ground, because these
   marks were drawn to sit on dark and half of them are single-colour. */
.hubcard__art {
  display: grid;
  place-items: center;
  aspect-ratio: 16 / 10;
  border-radius: var(--radius-lg);
  background-color: var(--surface-media);
  overflow: hidden;
  /* One colour for the whole row. As images these arrived in three different
     colours on one line — Students, Young Adults and Missions are drawn in
     near-white, Kids in a dark green that all but vanished on this panel. The
     panel is ink in both appearances, so the mark is pinned light rather than
     themed: six ministries reading as one set matters more here than any of
     them keeping its own palette, and each brand gets its own page to be right
     on. */
  color: var(--gl-stone-50);
}
/* Two kinds of artwork, one optical size and now one colour. Capped on BOTH
   axes: these lockups run from 1.33 to 3.18 in ratio, so a width alone would
   make the squarest of them twice the height of the widest and the row would
   read as six sizes rather than six marks. */
.hubcard__mark {
  /* Sized by HEIGHT, not width. These lockups run from 1.33 to 3.18 in ratio,
     so a shared width made Kids 133px tall against Missions at 77 — six sizes
     rather than six marks. A shared height is how a row of logos reads as a
     set; the widths differ, which is simply what the artwork is. max-width
     stops the widest of them filling its tile edge to edge. */
  height: 30%;
  width: auto;
  max-width: 68%;
}
.hubcard__icon { height: 44%; width: auto; max-width: 40%; }
.hubcard__body { display: flex; flex-direction: column; gap: var(--space-2xs); }
.hubcard__name {
  font-family: var(--font-sans);
  font-size: var(--text-xl);
  font-weight: var(--weight-medium);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-normal);
  color: var(--text-primary);
}
/* Rule on the inner span, so it tracks the words rather than the block. */
.hubcard__name > span { position: relative; }
.hubcard__name > span::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -0.14em;
  height: 2px;
  border-radius: 2px;
  background-color: var(--accent-mark);
  transform: scaleX(0);
  transform-origin: right center;
  transition: transform var(--dur-normal) var(--ease-out);
}
.hubcard__link:hover .hubcard__name > span::after,
.hubcard__link:focus-visible .hubcard__name > span::after {
  transform: scaleX(1);
  transform-origin: left center;
}
.hubcard__text { font-size: var(--text-sm); color: var(--text-secondary); }
@media (hover: hover) and (pointer: fine) {
  .hubcard__link:hover { transform: translateY(-2px); }
}
@media (prefers-reduced-motion: reduce) {
  .hubcard__link { transition: none; }
  .hubcard__link:hover { transform: none; }
}
/* Label and disclosure sit as one unit, so the underline and the hover target
   still read as a single nav item even though they are two controls. */
.nav__parent { display: inline-flex; align-items: center; gap: 0.15rem; }
.nav__disclose {
  display: inline-grid;
  place-items: center;
  padding: 0.25rem;
  border: 0;
  background: transparent;
  color: inherit;
  cursor: pointer;
  border-radius: var(--radius-xs);
}
.nav__disclose:focus-visible {
  outline: var(--border-width-thick) solid var(--focus-ring);
  outline-offset: 2px;
}
/* The chevron lives in the button now, so the rotation hook moves with it. */
.nav__item[data-open="true"] .nav__disclose .nav__chevron { transform: rotate(180deg); }


/* ==========================================================================
   QUICK INFO
   The three questions every ministry gets asked, answered before the prose.
   A <dl>, because that is what it is: a term and its value. A table would
   imply rows relating across columns; paragraphs would be prose someone has
   to read to extract a meeting time from.
   ========================================================================== */
.quickinfo {
  display: grid;
  gap: var(--space-md);
  margin: 0;
  padding: var(--space-lg);
  border-radius: var(--radius-xl);
  background-color: var(--surface-sunken);
}
/* auto-fit, not a fixed three. Ministries carry three pairs or four depending
   on what they actually need answered, and a hard 3-column grid dropped the
   fourth onto a row of its own. */
@media (min-width: 40rem) {
  .quickinfo {
    grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
    gap: var(--space-lg);
  }
}
.quickinfo__item { display: grid; gap: var(--space-3xs); justify-items: start; }
/* The mark leads, then the label, then the answer. Sized to the label rather
   than the value: it is signage for the row, not an illustration of it. */
/* Ink, not sage — matching the live site, which draws these at #252525, and
   matching .defitem__icon, which is the same hand-drawn set used the same way
   on About. Sage is a light-value green: at 1.97:1 on paper these read as a
   watermark rather than as signage. They are decorative (every one sits beside
   its own label), so nothing was failing — they just were not the mark the
   rest of the site draws. */
.quickinfo__icon {
  height: 2rem;
  width: auto;
  color: var(--text-primary);
  margin-bottom: var(--space-3xs);
}
/* --text-secondary, not muted. At 11px on the sunken surface muted lands at
   4.4:1 — under the line, and these three words are the fastest thing on the
   page to read, not decoration beside the values. */
.quickinfo__label {
  font-family: var(--font-condensed);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-secondary);
}
.quickinfo__value {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: var(--weight-medium);
  line-height: var(--leading-snug);
  color: var(--text-primary);
}

/* --- ministry hero mark -------------------------------------------------- */



/* The ministry titles are sentences, not page names — "Real growth happens in
   circles, not rows." is prose, and condensed uppercase would turn it into a
   sign. The sans face at display size keeps it readable as a claim. */
.page-hero__title--sans {
  font-family: var(--font-sans);
  text-transform: none;
  letter-spacing: var(--tracking-display);
  font-size: clamp(1.875rem, 1.1rem + 3vw, 3rem);
  line-height: 1.14;
}

/* ==========================================================================
   MINISTRY BAND
   The mark, at size, on a photograph — and nothing else. Matching the live
   ministry pages, where the hero carries no words at all: the logo IS the
   headline, and the sentence explaining it starts the section below.

   That split is the point. A logo competing with a headline in the same view
   makes neither the title of the page; giving the mark a band of its own lets
   it be the title and lets the sentence be a sentence.
   ========================================================================== */
.ministryband {
  /* Reaches up behind the header the way the About watermark does — the header
     is sticky and holds its own place in flow, so the band has to climb back
     over it to sit under the chrome rather than begin below it. */
  margin-top: calc(-1 * var(--chrome-h, var(--header-h)));
  padding-top: var(--chrome-h, var(--header-h));
  min-height: min(42rem, 82svh);
  display: grid;
  align-items: center;
  background-color: var(--surface-media);
  background-size: cover;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  color: var(--gl-stone-50);
}
/* The scrim is a pseudo-element, not a second background layer, because the
   image arrives as an inline style and an inline background-image replaces the
   whole property — gradient included. One ink at one alpha: a gradient toward
   `transparent` would fade toward transparent BLACK and drag grey through the
   artwork's midtones.

   Light, because the Kids artwork is a pale illustration and the mark carries
   its own colour. A photograph would want more. */
.ministryband { position: relative; isolation: isolate; }
.ministryband--photo::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  /* Heavy enough to carry copy. When the band held only the mark, 0.12 was
     plenty — the logo brought its own colour. With the title, lead and buttons
     on it the scrim has to guarantee light text against whatever pixel it
     lands on, including a pale sky. That is the trade: the artwork becomes a
     backdrop rather than the subject, which is what it has to be once there
     are words in front of it. */
  background-color: rgb(12 10 9 / 0.62);
  pointer-events: none;
}
.ministryband__inner {
  position: relative;
  z-index: 1;
  display: grid;
  justify-items: center;
  text-align: center;
  padding-block: var(--space-3xl);
  /* Pinned light: this copy sits on a photograph, not on the page, so it must
     not follow the section's tokens into a theme flip. */
  color: var(--gl-stone-50);
  --text-primary: var(--gl-stone-50);
  --text-secondary: rgb(250 250 249 / 0.84);
  --border-control: rgb(250 250 249 / 0.5);
}
/* The mark leads, then the sentence. Smaller than when it stood alone in the
   band — it is the first thing in a stack now, not the whole of it. */
/* Grid rather than block: the wide lockups are width-governed and land
   narrower than this box (240px of a 300px span on a phone), and a block would
   leave them flush left while the title above and lead below stay centred.
   `justify-items` centres whatever width the mark resolves to. */
.ministryband__mark {
  display: grid;
  justify-items: center;
  margin-bottom: var(--space-lg);
  line-height: 0;
}
.ministry__logo {
  width: auto;
  height: clamp(5.5rem, 4rem + 6vw, 9rem);
  max-width: 100%;
}
.ministry__logo--drawn {
  height: clamp(4.5rem, 3.5rem + 4vw, 7rem);
  color: var(--gl-stone-50);
}

/* The same treatment a section head gets — bold, tight, negative tracking,
   at `--display-md`. Sans rather than the condensed uppercase the other page
   heroes use, because these titles are claims ("Real growth happens in
   circles, not rows.") and caps would make them signage. Medium at 34px was
   the odd one out: the band is the largest statement on the page and it was
   set lighter and smaller than "Where we are going next" further down it. */
.ministry__title {
  font-family: var(--font-sans);
  /* Its own clamp rather than `--display-md` straight, landing on the same
     3rem at desktop. The token's 1.875rem floor is too big for these on a
     phone: "A fun, safe place for kids" needs 375px and gets 311, so the
     authored break — at the grammar, not at the middle — turns into three
     ragged lines. The floor is the size these titles were already set at. */
  font-size: clamp(1.625rem, 1.01rem + 2.5vw, 3rem);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-display);
  -webkit-text-stroke: var(--stroke-display) currentColor;
  text-transform: none;
  text-wrap: balance;
}
.ministry__lead { margin-top: var(--space-md); max-width: 62ch; color: var(--text-secondary); }
.ministry__actions { margin-top: var(--space-lg); justify-content: center; }

/* ---- stacked variant ----------------------------------------------------
   A vertical list with the mark beside its pair rather than above it. The row
   layout is right for a full-bleed strip; in a narrow column beside a picture
   it would set four columns of about 110px each and break every value onto
   three lines. */
.quickinfo--stack {
  padding: 0;
  background-color: transparent;
  border-radius: 0;
  gap: 0;
}
@media (min-width: 40rem) {
  .quickinfo--stack { grid-template-columns: 1fr; gap: 0; }
}
.quickinfo--stack .quickinfo__item {
  grid-template-columns: 2.75rem 1fr;
  column-gap: var(--space-md);
  align-items: center;
  padding-block: var(--space-md);
  border-top: 1px solid var(--border-subtle);
}
.quickinfo--stack .quickinfo__item:first-child { border-top: 0; padding-top: 0; }
/* The mark spans both rows of its item so the label and value stack beside it
   rather than under it. */
.quickinfo--stack .quickinfo__icon {
  grid-row: 1 / span 2;
  align-self: center;
  margin-bottom: 0;
  height: 2.75rem;
}
.quickinfo--stack .quickinfo__label,
.quickinfo--stack .quickinfo__value { grid-column: 2; }

/* ---- facts beside a photograph -----------------------------------------
   Stacked by default and split only past 66rem. That break is set from the
   device rather than the design: it clears a 1024px tablet in landscape, which
   a 60rem break would have split into a 380px column of facts beside a
   photograph — narrow enough that half the values wrap. */
.infosplit { display: grid; gap: var(--space-xl); }
@media (min-width: 66rem) {
  /* Live runs 484 / 796 with a 72px gutter — near enough 5fr / 8fr. Stretch,
     not centre: the two are meant to finish level with each other. */
  .infosplit {
    grid-template-columns: 5fr 8fr;
    gap: var(--space-2xl);
    align-items: stretch;
  }
}
/* The left side is two boxes stacked; the picture matches their combined
   height rather than either one of them. */
.infosplit__col { display: grid; gap: var(--space-md); align-content: start; }

/* No border. The facts read as the column's own content rather than as an
   object sitting on it — the raised surface and the padding are all the
   separation this one gets. */
.infosplit__facts {
  padding: var(--space-lg);
  border-radius: var(--radius-xl);
  background-color: var(--surface-raised);
}
.infosplit__media {
  display: block;
  width: 100%;
  height: auto;
  border-radius: var(--radius-xl);
}
/* The picture sits in a frame with no intrinsic height of its own, and that is
   the whole trick. An <img> placed straight into the grid contributes its
   NATURAL height to the row — `height: 100%` does not stop it, because for
   intrinsic sizing a percentage height resolves to auto. So on a wide screen
   the row was sized by the photograph (827px at 1920) rather than by the
   column beside it (803px), the column stretched to match, and its two boxes
   packed to the top and left a 24px shortfall. Below 66rem nothing showed it,
   which is why it read as a caching problem.

   An empty positioned box contributes nothing, so the row is sized by the
   column and the frame simply fills whatever that turns out to be. */
.infosplit__pic { position: relative; aspect-ratio: 1; }
.infosplit__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: var(--radius-xl);
}
@media (min-width: 66rem) {
  /* Side by side the square is dropped so the height can come from the column.
     min-height is a floor, so a ministry with only two or three facts cannot
     squeeze the photograph into a letterbox; where it applies, the column
     stretches to the picture instead. */
  .infosplit__pic { aspect-ratio: auto; min-height: 20rem; }
}

/* One label for both in-page section heads. */
.minihead {
  text-align: center;
  font-family: var(--font-condensed);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
  margin-bottom: var(--space-md);
}
/* In a column beside a picture the label sits over its list, not over the
   page, so it aligns with the list rather than centring on nothing. */
.infosplit .minihead,
.minihead--start { text-align: start; }

/* The rooms sit on the cream — already a themed pair, #E2D2BF on paper and a
   warm near-black in the dark, so nothing inside needs pinning.

   No border, and that is a rule rather than an omission: a card takes a border
   only when its own surface fails to separate it from the ground beneath it.
   Cream on paper measures 1.42:1 — a step you can see. The facts card below it
   goes without one too: white on paper is 1.04:1, so the line was the only
   edge it had, and losing it lets the facts sit in the column as copy. */
.gatherbox { background-color: var(--surface-cream); }


/* The section photo, under the thesis it belongs to. Held in the sticky
   column so it stays with the statement rather than scrolling off with the
   list beside it. */
.stickyside__media {
  display: block;
  width: 100%;
  height: auto;
  margin-top: var(--space-lg);
  border-radius: var(--radius-xl);
}

/* ==========================================================================
   KIDS SKY
   The band built out of the brand's own scene parts instead of a photograph
   under a wash. That swap is the whole design: a photograph carrying centred
   copy needs a scrim heavy enough to guarantee legible text over ANY pixel,
   which costs the artwork every one of its colours. A flat #c9e7eb sky needs
   no scrim at all, so the copy goes ink, the sun stays yellow, the grass stays
   green, and the header keeps its ordinary ink chrome straight over the top.

   One idea of motion: wind. Three clouds crossing on periods of two to three
   minutes, and a bob on the animals measured in single pixels. Both are below
   the threshold where you would call the page "animated" — you notice the
   second time you look that something has moved, which is the only kind of
   motion that survives repeat visits.
   ========================================================================== */
.ministryband--scene {
  /* Pinned, not tokenised. The sky is brand artwork in exactly the way the
     sage is: it does not have a dark-mode counterpart, it is the colour. */
  --sky: #c9e7eb;
  --cloud-fill: #ffffff;
  --day: 1;    /* sun */
  --night: 0;  /* moon and stars */
  --scene-ink: var(--gl-ink);
  --scene-ink-soft: rgb(40 40 39 / 0.78);
  --scene-line: rgb(40 40 39 / 0.42);
  --scene-inverse: var(--gl-ink);
  --logo-fill: #194430;
  /* Driven by WIDTH, not by the band's height, and that is not a stylistic
     choice. The hill is painted with `cover`, which scales the artwork by
     width; tie the strip to the band's height instead and you get a feedback
     loop — taller band, taller hill, more padding, taller band.

     14.6vw, and deliberately UNCAPPED at the top. The artwork is painted at
     true proportions so the slopes keep their full amplitude, and the strip
     shows it from the top down; the silhouette only becomes solid across the
     whole width at native y≈125 of 320, i.e. 0.125 × width. Cap the strip and
     on a wide enough screen its lower edge lands above that line, which opens
     a wedge of sky under the deepest part of the valley and leaves the hill
     unpinned from the bottom of the band. 14.6vw clears 12.5vw at every width,
     so the grass always reaches the floor and the ridge is always a ridge.
     Squashing it to fit instead would guarantee coverage too, but it flattens
     the slopes away — the crest is only 105 of 320 units tall to begin with. */
  --hill-h: max(5.5rem, 14.6vw);
  /* The stage is the screen. Nothing above the fold but sky, hill and the
     invitation. */
  min-height: 100svh;
  background-color: var(--sky);
  background-image: none;
  color: var(--scene-ink);
  transition: background-color var(--dur-tint) ease, color var(--dur-tint) ease;
  /* The sun runs off the right edge and the low cloud tucks behind the hill —
     both want the band to be the frame. */
  overflow: hidden;
}
.kidsky { position: absolute; inset: 0; z-index: 0; pointer-events: none; }

/* A sunrise, which means big and low and partly gone. It sits UNDER the hill
   in the stack, so a third of the disc is behind the ridge — that occlusion is
   the entire difference between a sun rising over a landscape and a yellow
   circle placed on top of one. Cropped by the right edge as well, so it reads
   as carrying on past the frame rather than as a graphic sized to fit inside
   it, and held off to that side so the centred copy stays on clean sky.

   Well clear of the chrome, so the nav never has to sit on yellow. */
.kidsky__sun {
  --sun: clamp(19rem, 40vw, 44rem);
  position: absolute;
  /* Pushed further past the edge as it grew, and dropped until half of it is
     under the ridge. Both numbers are set by where the CIRCLE goes, not where
     its box goes — a disc this size has a box that overlaps the headline while
     the disc itself passes well clear of it, and sizing to the box would have
     meant a needlessly smaller sun. */
  right: clamp(-13rem, -10vw, -5rem);
  /* Half of it behind the ridge. Occlusion is what makes it a sunrise; without
     it the same circle is a decal. */
  bottom: calc(var(--hill-h) - var(--sun) * 0.5);
  width: var(--sun);
  aspect-ratio: 1;
  background: url("../img/kids/scene/sun.svg") 50% 50% / contain no-repeat;
}

/* The drifting element is a full-width track, not the cloud: translate
   percentages resolve against the element's OWN width, so animating the cloud
   directly would move it by multiples of a cloud rather than across the band.
   The track is 100% of the band, so 134% means "off the far side". */
.kidsky__drift {
  position: absolute;
  left: 0;
  width: 100%;
  z-index: 1;
  animation: kid-drift var(--drift) linear var(--drift-in) infinite alternate;
}
/* Painted through a mask instead of drawn as an <img>: the cloud is white by
   day and a shade DARKER than the sky by night, and background-color is a
   property that interpolates, so the two states cross-fade with everything
   else rather than cutting. */
.kidsky__cloud {
  position: absolute; left: 0; top: 0;
  width: var(--cloud-w);
  aspect-ratio: var(--cloud-ar);
  background-color: var(--cloud-fill);
  -webkit-mask: var(--cloud-mask) 0 0 / 100% 100% no-repeat;
          mask: var(--cloud-mask) 0 0 / 100% 100% no-repeat;
  transition: background-color var(--dur-tint) ease;
}
/* Each cloud oscillates inside its own lane rather than crossing the whole
   band, and `alternate` is what makes a bounded lane possible without a jump:
   a bounded loop that RESETS snaps, a bounded loop that reverses just keeps
   drifting. At two to three minutes end to end, the turn is not something the
   eye can catch.

   The lanes are the point. A cloud that crosses the full width will, at some
   moment, sit behind the headline, behind the mark, or on the moon — and no
   vertical arrangement avoids it, because the copy is centred in a full-height
   band and the clear strips above and below it are thinner than a cloud.
   Bounding the travel is the only thing that keeps them out for good, and it
   buys a composition as well: the weather gathers on the left, the sun rises
   on the right, and the small one crosses the clear strip above the mark. */
@keyframes kid-drift {
  from { transform: translate3d(var(--from), 0, 0); }
  to   { transform: translate3d(var(--to), 0, 0); }
}
/* Negative delays start each one part-way across, so the band is never empty
   and the three are never in a row. Periods share no common factor. */
/* Top right, in the clear strip between the chrome and the mark, and held
   right of the moon's lane so the two never meet at night. Smaller than the
   others because that strip is only about a hundred pixels deep. */
.kidsky__drift--high {
  --cloud-mask: url("../img/kids/scene/cloud-sm.svg");
  --cloud-ar: 105.76 / 44.79;
  top: calc(var(--chrome-h, var(--header-h)) + clamp(0.5rem, 2.5vh, 1.5rem));
  --cloud-w: clamp(5.5rem, 11vw, 11rem);
  --from: 58%; --to: 108%;
  --drift: 187s; --drift-in: -71s; opacity: 0.8;
}
/* The big one, mid-sky on the left: below the moon, left of the copy column. */
.kidsky__drift--far {
  --cloud-mask: url("../img/kids/scene/cloud.svg");
  --cloud-ar: 147.08 / 65.15;
  top: calc(var(--chrome-h, var(--header-h)) + clamp(9rem, 30vh, 17rem));
  --cloud-w: clamp(11rem, 22vw, 21rem);
  --from: -22%; --to: 0%;
  --drift: 131s; --drift-in: -104s;
}
/* Low on the same side, sitting just deep enough that the hill cuts its base —
   the occlusion is what makes the ridge read as in front rather than below. */
.kidsky__drift--low {
  --cloud-mask: url("../img/kids/scene/cloud-sm.svg");
  --cloud-ar: 105.76 / 44.79;
  bottom: calc(var(--hill-h) - clamp(0.6rem, 1.4vw, 1.6rem));
  --cloud-w: clamp(8.5rem, 17vw, 16rem);
  --from: -16%; --to: 2%;
  --drift: 163s; --drift-in: -22s; opacity: 0.92;
}
/* The horizon cloud hangs UP from its line; the other two hang down from
   theirs. Without this it would grow downward out of a zero-height track and
   disappear behind the hill entirely. */
.kidsky__drift--low .kidsky__cloud { top: auto; bottom: 0; }

/* ---- phones --------------------------------------------------------------
   The wind stopped blowing, and the reason is that the lanes are percentages
   of the band while the periods are fixed seconds. Speed is the quotient of
   the two, so a band that narrows from 1280px to 390 cuts the speed by the
   same factor and nothing else changes to compensate: the far cloud crossed
   its lane at 2.1px/s on a laptop and 0.6px/s on a phone. Half a pixel a
   second is not slow motion, it is a still image — below roughly 1px/s the
   eye has no way to distinguish drift from a fixed position, so the scene
   read as artwork rather than weather.

   What the eye reads is SPEED, not period, so speed is what is held. All
   three lanes are re-cut to the same figure — 2.8px/s on a 390px screen,
   3.1 on a 430 — which is where they sit on a desktop, and which is slow
   enough that you notice the second time you look and not the first. Periods
   stay prime and share no factor, so the three never line up.

   The lanes can open up here because there is nothing left for them to dodge.
   They are narrow on desktop to keep the clouds off the copy column and away
   from the moon — but on a phone the copy runs the full width of the band at
   every height, so a cloud is behind the lead paragraph at any phase already,
   and the moon is cropped clean off the left edge by `cover` before it is
   ever drawn. Bounding the travel here buys nothing and costs the motion. */
@media (max-width: 47.999rem) {
  .kidsky__drift--high { --from: -14%; --to:  92%; --drift: 149s; --drift-in: -47s; }
  .kidsky__drift--far  { --from: -30%; --to:  62%; --drift: 127s; --drift-in: -80s; }
  .kidsky__drift--low  { --from: -24%; --to:  54%; --drift: 107s; --drift-in: -18s; }
}

/* ---- night ---------------------------------------------------------------
   Not a second scene: the same five elements, repainted. The sun fades out
   where it stands and the moon and stars fade in over --dur-tint, the same
   900ms the rest of the page takes to change temperature, so switching
   appearance reads as one move rather than as a slideshow. */
.kidsky__sun  { opacity: var(--day);   transition: opacity var(--dur-tint) ease; }
.kidsky__moon,
.kidsky__stars { opacity: var(--night); transition: opacity var(--dur-tint) ease; }
/* The moon and the star field are painted as two layers of ONE drawing: same
   viewBox, same size, same position, so they scale together and the moon lands
   in the clearing the constellation leaves for it. Placing the moon
   independently is what put it on top of a star — any independent placement
   would, at some viewport, because the two were then scaling by different
   rules. Separate layers only so the moon can be sized and, if it ever needs
   to, timed on its own.

   The shared nudge drops both clear of the chrome; the artwork's moon sits 13%
   down its own field, which lands just under the header at desktop sizes. It
   stays comfortably inside the top-left quadrant. */
.kidsky__moon,
.kidsky__stars {
  position: absolute;
  inset: 0;
  --sky-drop: calc(var(--chrome-h, var(--header-h)) * 0.35);
  background-position: 50% var(--sky-drop);
  background-size: cover;
  background-repeat: no-repeat;
}
.kidsky__moon  { background-image: url("../img/kids/scene/moon.svg"); }
.kidsky__stars { background-image: url("../img/kids/scene/stars.svg"); }

/* Narrower than desktop the copy column reaches the frame and there is no
   clear sky to put a 40vw disc in, so the sun drops until it is an arc on the
   horizon rather than a shape behind the words. */
@media (max-width: 63.999rem) {
  .kidsky__sun { bottom: calc(var(--hill-h) - var(--sun) * 0.72); }
}

/* Short viewports — the 800px-tall laptop, which is common enough to design
   for. The hill is sized by WIDTH, so on a wide-but-short screen it takes the
   same 200-odd pixels off a much shorter stage and the copy no longer fits
   inside one screen. Rather than let the hero run past the fold, the stack
   tightens: a smaller mark and closer gaps, which is the cheapest 70px on the
   page and costs the composition nothing at a size where nobody is admiring
   the mark anyway. */
@media (max-height: 54rem) {
  .ministryband--scene .ministryband__inner {
    padding-top: var(--space-lg);
    padding-bottom: calc(var(--hill-h) + var(--space-lg));
  }
  .ministryband--scene .ministry__logo { height: clamp(4.5rem, 13svh, 7rem); }
  .ministryband--scene .ministryband__mark { margin-bottom: var(--space-md); }
  .ministryband--scene .ministry__actions { margin-top: var(--space-md); }
}

/* `cover` with the crop taken off the BOTTOM. The lower two thirds of the
   artwork is solid fill, so cropping there costs nothing, and it means the
   crest keeps its true proportions at every band height — scaling the SVG to
   fit instead would flatten the curve on wide screens. */
.kidsky__hill {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 2;
  height: var(--hill-h);
  /* Painted through a mask, like the clouds, so the silhouette takes a colour
     token instead of an image's baked-in fill. That is what lets the band
     directly below it be the same green by construction rather than by two
     values matched by eye. Longhands, not the shorthand: the
     `position / size` slash syntax is not honoured everywhere, and a mask that
     fails to parse paints nothing at all. */
  background-color: var(--kid-grass);
  -webkit-mask-image: url("../img/kids/scene/grass.svg");
          mask-image: url("../img/kids/scene/grass.svg");
  -webkit-mask-position: 50% 0;   mask-position: 50% 0;
  -webkit-mask-size: cover;       mask-size: cover;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  transition: background-color var(--dur-tint) ease;
}

/* A marker, not a layer: no paint, no size in the picture, just a box the
   header's tone script can see. It covers the lower half of the hill, which is
   where the silhouette is opaque across enough of the width to make "dark" the
   right answer for a full-width bar — above that the crest dips and the header
   spans both green and sky at once, which no single tone can serve. It runs to
   the band's bottom edge and the grass band picks up immediately below, so the
   header flips once on the way in and once on the way out. */
.kidsky__toneline {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: calc(var(--hill-h) * 0.5);
  z-index: 0;
  pointer-events: none;
}

/* Ink, and pinned — the sky does not flip, so neither can the words on it.
   --surface-inverse is pinned too because .btn--glass mixes its tint from it;
   left to the theme, the "All Ministries" button would fill with a pale wash
   in dark mode and vanish into the sky. */
.ministryband--scene .ministryband__inner {
  z-index: 3;
  color: var(--scene-ink);
  --text-primary: var(--scene-ink);
  --text-secondary: var(--scene-ink-soft);
  --border-control: var(--scene-line);
  --surface-inverse: var(--scene-inverse);
  /* Keeps the whole stack in the sky. Centring against the full band would
     drop the buttons onto the green, where ink loses its contrast. */
  padding-bottom: calc(var(--hill-h) + var(--space-xl));
  /* Less than the band's default. The stage is pinned to the viewport and the
     hill takes a fixed share of the bottom, so the top padding is the only
     slack there is — and a second line of headline spends 41px of it. */
  padding-top: var(--space-xl);
}
.ministryband--scene .ministry__logo--drawn { color: var(--scene-ink); }

/* ==========================================================================
   GRASS BAND
   The strip of facts under the hero, painted the same green as the hill above
   it and butted straight against it. It was a rounded card on a sunken grey,
   which put a hard edge one hero-height below a picture of a hill: the page
   said "landscape" and then immediately said "card". Sharing the hill's own
   token means there is no seam to notice — the grass keeps going, and the
   facts sit in it.

   The type is pinned rather than themed, for the same reason the hero's is:
   this copy sits on brand artwork, not on the page, so it must not follow the
   surface tokens into a theme flip. --text-secondary is deliberately the same
   value as --text-primary here; at 11px on grass, dropping the label's alpha
   to distinguish it would cost about a point of contrast, and the labels are
   already distinguished by case, weight and tracking.
   ========================================================================== */
/* ==========================================================================
   KIDS ANNOUNCEMENT BAR
   The bar reads --surface-accent, which on this page is the grass — so it came
   out the same green as the Give button sitting a few pixels below it, and its
   14px copy measured 4.61:1: legal, but weak for the one strip on the page
   whose whole job is to be noticed.

   Deep pine instead. It separates the bar from the button, it is the mark's
   own green, and it bookends the pine footer at the other end of the page.
   The CTA inside then takes the star — the same highlight the mega menu uses,
   so "the interactive thing on a Kids page is star-coloured" holds in both
   places. The underline wipe follows currentColor, so it comes along.
   ========================================================================== */
.theme-kids .announce {
  background-color: var(--kid-pine-700);
  color: var(--kid-sky-100);
}
.theme-kids .announce__link { color: var(--kid-sun-300); }
/* The default hover well is 12% ink, which is invisible on a dark ground. */
.theme-kids .announce__close:hover { background-color: rgb(228 242 244 / 0.16); }

.grassband {
  background-color: var(--kid-grass);
  padding-block: var(--space-2xl);
  color: var(--kid-sky-100);
  --text-primary:   var(--kid-sky-100);
  --text-secondary: var(--kid-sky-100);
  /* Eyebrows read --text-muted, which is a stone grey and disappears on grass.
     Pinned here with the rest, at a step back rather than a different hue. */
  --text-muted:     rgb(228 242 244 / 0.72);
  --accent-mark:    var(--kid-sun-300);
  /* The one exception to the ink rule above: on the grass the marks take the
     star, which is the sub-brand's highlight everywhere else too. */
  transition: background-color var(--dur-tint) ease;
}
/* The question the four pairs answer. Sans rather than the condensed uppercase
   an eyebrow would use: this is a sentence someone might actually say, and
   setting it as signage would turn it back into a label.

   One rule, two call sites: the Kids grass band and the facts card on the
   photo ministries. They ask the reader the same thing, so they are the same
   piece of type. --grassband__q keeps the name the design system already
   documents; .factsq is what a card outside that band uses. */
.factsq,
.grassband__q {
  margin: 0 0 var(--space-lg);
  font-family: var(--font-sans);
  font-size: var(--text-xl);
  font-weight: var(--weight-medium);
  letter-spacing: -0.005em;
}

.grassband .quickinfo__icon { color: var(--accent-mark); }

/* The card comes off entirely — the band IS the surface now. */
.grassband .quickinfo {
  background-color: transparent;
  border-radius: 0;
  padding: 0;
}
/* The mark, inlined. Its green is currentColor and its white shapes are
   genuine holes, so it takes the sky's own ink by day and the type's colour by
   night while the knockouts cut through to whatever is behind them. */
.ministry__logo--inline {
  display: block;
  width: auto;
  height: clamp(5.5rem, 4rem + 6vw, 9rem);
  max-width: 100%;
  /* Falls back to the band's own pinned ink. Only the Kids scene defines
     --logo-fill; on a photo band the mark should simply be the light the rest
     of that copy is. */
  color: var(--logo-fill, currentColor);
  transition: color var(--dur-tint) ease;
}
/* The wide lockups — Students, Young Adults, Missions — are 2.5 to 3.2 times
   as wide as they are tall, where Kids is 1.33. Driving them off the same
   height would run them off the sides of a phone, so past a point they are
   governed by width instead. */
.ministryband--photo .ministry__logo--inline {
  height: auto;
  width: min(100%, clamp(15rem, 42vw, 30rem));
}

[data-theme="dark"] .ministryband--scene {
  /* The same sky at 16% lightness, holding its hue rather than sliding to the
     purple-black every dark mode reaches for. */
  --sky: #16323f;
  /* Darker than the sky it sits on, which is what makes a cloud at night a
     silhouette rather than a glowing shape. */
  --cloud-fill: #0f2733;
  --day: 0;
  --night: 1;
  --scene-ink: var(--gl-stone-50);
  --scene-ink-soft: rgb(250 250 249 / 0.8);
  --scene-line: rgb(250 250 249 / 0.46);
  --scene-inverse: var(--gl-stone-50);
  --logo-fill: var(--gl-stone-50);
}
/* Same values, for the OS preference when the reader has not chosen. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]):not([data-theme="dark"]) .ministryband--scene {
    /* The same sky at 16% lightness, holding its hue rather than sliding to the
       purple-black every dark mode reaches for. */
    --sky: #16323f;
    /* Darker than the sky it sits on, which is what makes a cloud at night a
       silhouette rather than a glowing shape. */
    --cloud-fill: #0f2733;
    --day: 0;
    --night: 1;
    --scene-ink: var(--gl-stone-50);
    --scene-ink-soft: rgb(250 250 249 / 0.8);
    --scene-line: rgb(250 250 249 / 0.46);
    --scene-inverse: var(--gl-stone-50);
    --logo-fill: var(--gl-stone-50);
  --logo-fill: var(--gl-stone-50);
  }
}

/* ==========================================================================
   FOOTER CAST
   The five brand animals standing along the top edge of the footer. That edge
   is already a hard line the full width of the page, which is all a row of
   standing figures needs in order to be standing on something — no plinth, no
   invented ground, and the page's own background behind them.
   ========================================================================== */
.footercast {
  position: relative;
  z-index: 1;
  /* Takes over the footer's own top margin and hands the footer a flush edge.
     Left alone, --section-y opened 89px of page between the animals' feet and
     the line they are supposed to be standing on.

     PADDING, not margin: the same air, but inside the element, so the ground
     it carries fills it. As a margin it collapsed to a strip of page showing
     between the section above and the band the animals stand on — which put a
     seam exactly where the two are supposed to read as one continuous piece
     of ground. */
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  padding-top: var(--section-y);
  padding-inline: clamp(var(--space-md), 6vw, var(--space-4xl));
  /* No ground of its own. The markup hands it the surface class of the section
     it follows, so the band behind the animals continues that section down to
     the footer's edge instead of reverting to the page ground and leaving them
     standing on a stripe. Stated here, the colour would have to be re-tuned
     every time the closing section changed — and a base rule in this file
     would beat the surface class from base.css on source order anyway. */
  pointer-events: none;
}
.bandchar {
  width: auto;
  height: var(--char-h);
  flex: none;
  /* Two or three pixels, over five to seven seconds, each on its own clock.
     Small enough that it never pulls the eye off the page; present enough that
     the row is not a decal. */
  animation: kid-bob var(--bob, 6s) ease-in-out var(--bob-in, 0s) infinite;
}
@keyframes kid-bob {
  0%, 100% { transform: translate3d(0, 0, 0); }
  50%      { transform: translate3d(0, -3px, 0); }
}
/* Below the tablet break the row scales to 62%, where all five together measure
   204px — they fit a 320px screen with room to spare. Three of them used to be
   dropped here, which was `space-between` being compensated for rather than a
   space problem: holding two figures at opposite edges of a 375px screen put
   214px between them and read as two unrelated stickers.

   space-evenly instead of a gap, because it cannot overflow and it needs no
   number that has to be re-checked at every width: the row distributes whatever
   is spare, huddling at 320px and opening out as the screen grows, until the
   full-width `space-between` treatment takes over at the tablet break. */
@media (max-width: 47.999rem) {
  .bandchar { height: calc(var(--char-h) * 0.62); }
  .footercast {
    justify-content: space-evenly;
    padding-inline: var(--space-md);
  }
}
.footercast + .footer { margin-top: 0; }

/* Wind and breathing are both ornament: with motion reduced the scene simply
   holds still. The clouds need explicit positions or all three would stack at
   the left edge where their un-animated transform leaves them. */
@media (prefers-reduced-motion: reduce) {
  .kidsky__drift, .bandchar { animation: none; }
  /* Parked mid-lane. Left alone each one holds at translate 0, which is not
     where any of their lanes are. */
  .kidsky__drift--high { transform: translate3d(83%, 0, 0); }
  .kidsky__drift--far  { transform: translate3d(-11%, 0, 0); }
  .kidsky__drift--low  { transform: translate3d(-7%, 0, 0); }
}

/* ==========================================================================
   THE PATHWAY
   Two steps, deliberately not the same weight. Step one is the Rooted brand on
   its own cream with the film beside the copy; step two is an ink card. Two
   pale blocks in a row read as one long section that happens to have a rule in
   it — a light card and a dark one read as two things, which is what they are:
   Rooted is the door, a Connect Group is where you end up.

   Neither carries a border. Each surface is doing the separating on its own,
   which is the rule this site follows everywhere else.
   ========================================================================== */
.pathway { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-lg); }

.pstep {
  display: grid;
  gap: var(--space-lg);
  padding: var(--space-lg);
  border-radius: var(--radius-xl);
}
@media (min-width: 60rem) {
  .pstep { grid-template-columns: minmax(0, 6fr) minmax(0, 5fr);
    gap: var(--space-lg) var(--space-2xl);
    align-items: center; padding: var(--space-xl); }
}

/* A card header, not a stack. The mark or the name sits at the left, the rule
   takes whatever width is left, and the step number closes the line at the
   right — so both cards open the same way and the lockup finally has room to
   be read rather than squeezed into a rail. */
.pstep__bar {
  display: flex;
  align-items: center;
  gap: var(--space-md);
  margin: 0;
}
@media (min-width: 60rem) {
  /* Spans the whole card, so the number lands on the card's right edge rather
     than halfway across it at the end of the copy column. */
  .pstep__bar { grid-column: 1 / -1; }
}
/* Stretches. The line is what makes the number feel like it belongs to the
   header rather than sitting loose in the corner. */
.pstep__rule { flex: 1 1 auto; width: auto; height: 1px;
  background-color: currentColor; opacity: 0.24; }
.pstep__num {
  flex: none;
  order: 3;
  font-family: var(--font-condensed);
  font-size: clamp(1.5rem, 1.2rem + 1vw, 2rem);
  font-weight: var(--weight-bold);
  line-height: 1;
  letter-spacing: 0.02em;
  color: var(--text-muted);
}
/* Sized to hold its own against the Rooted lockup opposite it — this is the
   name of the thing, not a label on it. */
.pstep__label {
  flex: none;
  font-family: var(--font-condensed);
  font-size: clamp(1.25rem, 1rem + 0.9vw, 1.75rem);
  font-weight: var(--weight-bold);
  letter-spacing: 0.01em;
  line-height: 1;
  text-transform: uppercase;
  color: var(--text-primary);
}
.pstep__title {
  margin: 0;
  font-family: var(--font-sans);
  font-size: clamp(1.25rem, 1.05rem + 0.9vw, 1.75rem);
  font-weight: var(--weight-medium);
  line-height: 1.15;
  letter-spacing: -0.005em;
}
.pstep__lede { margin: var(--space-2xs) 0 0; max-width: 52ch; color: var(--text-secondary); }
.pstep__points {
  list-style: none;
  margin: var(--space-md) 0 0;
  padding: 0;
  display: grid;
  gap: var(--space-3xs);
}
.pstep__points li { position: relative; padding-inline-start: 1.4rem; color: var(--text-secondary); }
.pstep__points li::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  top: 0.55em;
  width: 0.45rem; height: 0.45rem;
  border-radius: 50%;
  background-color: var(--accent-mark);
}
.pstep__cta { margin-top: var(--space-lg); justify-self: start; }

/* Stacked, the film comes second — under the header, above the copy — and
   bleeds to the card's edges. A 16:9 frame inset by the card's padding on a
   phone is a letterbox inside a letterbox; edge to edge it is simply the
   picture, and the square corners say it is touching the sides on purpose. */
@media (max-width: 59.999rem) {
  .pstep__bar  { order: 1; }
  .pstep--rooted .videoplay,
  .pstep--next .pstep__pic { order: 2;
    margin-inline: calc(-1 * var(--space-lg));
    border-radius: 0; }
  .pstep__main { order: 3; }
}

/* ---- step one: the Rooted brand ---------------------------------------
   Pinned in both appearances, like the sage and the Kids sky: it is somebody
   else's mark on our page, and a mark that changes colour with the reader's OS
   setting is not a mark. Every token it needs is redeclared here. */
.rooted {
  --rooted-bg: #EAE8DF;
  --rooted-ink: #344E41;
  --rooted-green: #619368;
  background-color: var(--rooted-bg);
  color: var(--rooted-ink);
  --text-primary: var(--rooted-ink);
  --text-secondary: color-mix(in srgb, var(--rooted-ink) 84%, transparent);
  /* 68% put the 11px label at 3.42:1 on this ground. A mid-toned brand surface
     has no room for three levels of text, so muted and secondary sit close
     together and both clear 4.5:1. */
  --text-muted: color-mix(in srgb, var(--rooted-ink) 86%, transparent);
  --accent-mark: var(--rooted-green);
}
/* Nudged down so the WORDMARK centres on the rule, not the artwork's box.
   The lockup is bottom-heavy by design: the wordmark occupies y 0–159.9 of a
   199.7 viewBox and the icon hangs 39.8 units below it, which puts the
   wordmark's centre 19.8 units — 9.94% of the height — above the box centre.
   Centre the box and the words read high; this puts them back on the line.

   A percentage on `translate` resolves against the element's own height, so it
   holds at any size the mark is set at. It also stays out of layout, so the
   icon simply overhangs into the gap under the header rather than growing the
   header to fit it. */
.rooted__mark {
  flex: none;
  display: block;
  width: min(60%, 11rem);
  height: auto;
  translate: 0 9.94%;
}
.rooted__cta {
  margin-top: var(--space-lg);
  justify-self: start;
  --btn-bg: transparent;
  --btn-fg: var(--rooted-ink);
  --btn-border: var(--rooted-ink);
  --btn-bg-hover: color-mix(in srgb, var(--rooted-ink) 10%, transparent);
}

/* ---- step two: ink -----------------------------------------------------
   Bold on purpose. Pinned dark in both appearances for the same reason the
   message card and the footer are: it is a fixed slab, not a surface that
   follows the page. Sage carries the bullets and the button, which is exactly
   what an accent on ink is for. */
.pstep--next {
  /* --surface-deep, not --gl-ink. Ink is #282827 and the tinted section in
     dark mode is stone-800 — 1.03:1 apart, so the "bold" card was invisible
     on exactly the ground it had to stand out from. --surface-deep exists for
     this: stone-950 on paper, true black in the dark, so it reads as a slab
     against the page in both. */
  background-color: var(--surface-deep);
  color: var(--gl-stone-50);
  --text-primary: var(--gl-stone-50);
  --text-secondary: rgb(250 250 249 / 0.76);
  --text-muted: rgb(250 250 249 / 0.62);
  --accent-mark: var(--gl-sage);
}
/* Same columns as step one, so the picture lands exactly where the film does
   and the two cards read as one sequence in two materials. Same 16:9 frame
   too: the frame carries the ratio, so the image cannot size the row. */
.pstep__pic {
  position: relative;
  margin: 0;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.pstep__pic img {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
}

/* ---- the film ----------------------------------------------------------
   Poster and a button; the player is only built when someone asks for it. A
   2:21 explainer is not the background film used elsewhere on this site — it
   has sound and a beginning, and nobody should pay to download it before they
   have decided to watch. */
.videoplay {
  position: relative;
  margin: 0;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-lg);
  overflow: hidden;
  background-color: var(--gl-ink);
}
.videoplay__poster,
.videoplay__iframe {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  border: 0;
  object-fit: cover;
  display: block;
}
.videoplay__btn {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2xs);
  width: 100%;
  border: 0;
  cursor: pointer;
  background: linear-gradient(to top, rgb(12 10 9 / 0.5), rgb(12 10 9 / 0.15));
  color: var(--gl-stone-50);
  font-family: var(--font-condensed);
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  transition: background var(--dur-fast) var(--ease-out);
}
.videoplay__btn svg { width: 1.5rem; height: 1.5rem; }
.videoplay__btn:hover { background: linear-gradient(to top, rgb(12 10 9 / 0.62), rgb(12 10 9 / 0.3)); }

/* ==========================================================================
   TRIPS
   A list, not a grid of cards. Five trips each carrying a place, a window and
   a price is four facts per row — a reader scans those down a column, not
   across a set of tiles, and a tile would have to invent a picture for each
   one. Same ruled rhythm the rooms and the run sheet use, so a ministry page
   with trips still reads as the same page as one with rooms.
   ========================================================================== */
/* Held to a measure. Five rows of four short facts spread across a 1376px
   container puts a third of a screen of empty space between a trip's name and
   its date, which reads as a table that lost its middle columns rather than as
   a list. Narrower, the four facts sit close enough to be read as one line. */
.trips {
  list-style: none;
  margin-inline: auto;
  padding: 0;
  display: grid;
  max-width: var(--measure-list);
}
.trip {
  display: grid;
  gap: var(--space-3xs);
  padding-block: var(--space-lg);
  border-top: 1px solid var(--border-subtle);
}
.trip:first-child { border-top: 0; padding-top: 0; }
@media (min-width: 48rem) {
  /* Name and place lead, the window and the price sit together, the action is
     last and aligned down the right — so five rows compare at a glance. */
  .trip {
    grid-template-columns: minmax(0, 7fr) minmax(0, 5fr) auto;
    align-items: baseline;
    gap: var(--space-3xs) var(--space-xl);
    padding-block: var(--space-md);
  }
  /* Both span the name/place pair and centre against it, rather than sitting
     on the first line's baseline — the row is two lines tall on the left and
     one on the right, so baseline alignment parks them at the top. */
  .trip__meta { grid-column: 2; grid-row: 1 / span 2; align-self: center; }
  .trip__cta { grid-column: 3; grid-row: 1 / span 2; align-self: center; }
}
.trip__name {
  margin: 0;
  font-family: var(--font-sans);
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  line-height: 1.2;
}
.trip__place { margin: 0; color: var(--text-secondary); }
.trip__meta {
  margin: 0;
  font-family: var(--font-condensed);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
}

/* ==========================================================================
   GATHERING CARD
   A family, not a component. Ten ways to state "here is where this meets",
   kept together because the differences between them are the useful part: one
   is half the height of the next, one is two tap targets rather than one, one
   is a photograph. Any of the ten drops into the same slot.

   --b is the one in production. See the design system for all ten rendered.
   ========================================================================== */
/* The real column width, so nothing here flatters itself with extra room. */
/* shared vocabulary ---------------------------------------------------- */
.gc {
  background-color: var(--surface-cream);
  border-radius: var(--radius-xl);
  padding: var(--space-lg);
  --text-muted: color-mix(in srgb, var(--text-primary) 74%, transparent);
  --hair: color-mix(in srgb, currentColor 18%, transparent);
}
.gc__pin { height: 2rem; width: auto; display: block; margin-bottom: var(--space-sm); }
.gc__label, .gc__edge, .gc__when, .gc__hours, .gc__rmeta {
  font-family: var(--font-condensed);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
}
.gc__label { display: block; margin-bottom: var(--space-md); }
.gc__names, .gc__run, .gc__bigtime {
  margin: 0;
  font-family: var(--font-condensed);
  font-weight: var(--weight-bold);
  text-transform: uppercase;
  line-height: 1.02;
  letter-spacing: -0.015em;
  font-size: clamp(1.75rem, 1.3rem + 1.8vw, 2.5rem);
}
.gc__when { display: block; margin-top: var(--space-sm); }
.gc .btn { margin-top: var(--space-lg); }

/* A — split ------------------------------------------------------------ */
.gc--a { display: grid; padding: 0; overflow: hidden; }
@media (min-width: 30rem) { .gc--a { grid-template-columns: 1fr 1fr; } }
.gc__half {
  display: grid; gap: var(--space-md); align-content: space-between;
  padding: var(--space-lg); border: 0; background: transparent;
  text-align: left; cursor: pointer; color: inherit; font: inherit;
  border-top: 1px solid var(--hair);
}
.gc__half:first-child { border-top: 0; }
@media (min-width: 30rem) {
  .gc__half { border-top: 0; }
  .gc__half + .gc__half { border-left: 1px solid var(--hair); }
}
.gc__hname {
  font-family: var(--font-condensed); font-weight: var(--weight-bold);
  text-transform: uppercase; line-height: 1.02; letter-spacing: -0.01em;
  font-size: clamp(1.375rem, 1.1rem + 1vw, 1.875rem);
}
.gc__hcta { display: inline-flex; align-items: center; gap: 0.4em;
  font-size: var(--text-sm); color: var(--text-muted); }
.gc__hcta svg { width: 1em; height: 1em; }
.gc__half:hover .gc__hcta { color: var(--text-primary); }

/* B — two rows --------------------------------------------------------- */
.gc--b { padding-bottom: var(--space-2xs); }
.gc__rows { display: grid; }
.gc__row {
  display: grid; grid-template-columns: 1fr auto; align-items: center;
  gap: var(--space-2xs) var(--space-md);
  padding-block: var(--space-md); border: 0; border-top: 1px solid var(--hair);
  width: 100%;
  background: transparent; text-align: left; cursor: pointer; color: inherit; font: inherit;
}
.gc__rname {
  font-family: var(--font-condensed); font-weight: var(--weight-bold);
  text-transform: uppercase; letter-spacing: -0.01em; line-height: 1;
  font-size: clamp(1.375rem, 1.1rem + 1vw, 1.875rem);
}
.gc__rmeta { grid-column: 1; }
.gc__row svg { grid-column: 2; grid-row: 1 / span 2; width: 1.25em; height: 1.25em;
  transition: transform var(--dur-fast) var(--ease-out); }
.gc__row:hover svg { transform: translateX(4px); }

/* C — time first ------------------------------------------------------- */
.gc__hours { display: block; margin-top: var(--space-sm); font-size: var(--text-base); }
.gc__rooms { margin: var(--space-2xs) 0 0; color: var(--text-secondary); }

/* D — one running line ------------------------------------------------- */
.gc__run span { opacity: 0.42; }

/* E — numbered --------------------------------------------------------- */
.gc__nums { list-style: none; margin: 0; padding: 0; display: grid; }
.gc__num {
  display: grid; grid-template-columns: 2.5rem 1fr; align-items: baseline;
  gap: var(--space-sm); padding-block: var(--space-sm);
  border-top: 1px solid var(--hair);
}
.gc__num:first-child { border-top: 0; padding-top: 0; }
.gc__num span { font-family: var(--font-condensed); font-size: var(--text-2xs);
  font-weight: var(--weight-bold); letter-spacing: var(--tracking-label);
  color: var(--text-muted); }
.gc__num b {
  font-family: var(--font-condensed); font-weight: var(--weight-bold);
  text-transform: uppercase; letter-spacing: -0.01em; line-height: 1;
  font-size: clamp(1.375rem, 1.1rem + 1vw, 1.875rem);
}

/* F — badges ----------------------------------------------------------- */
.gc__pills { display: flex; flex-wrap: wrap; gap: var(--space-2xs); }
.gc__pill {
  display: inline-block; padding: 0.5rem 1.1rem;
  border: 2px solid currentColor; border-radius: var(--radius-pill);
  font-family: var(--font-condensed); font-weight: var(--weight-bold);
  text-transform: uppercase; letter-spacing: 0.01em;
  font-size: clamp(1rem, 0.9rem + 0.5vw, 1.25rem);
}

/* G — ticket ----------------------------------------------------------- */
.gc--g { padding: 0; }
.gc__face { padding: var(--space-lg); }
/* The notch is two circles the colour of the page, sitting on the seam. */
.gc__stub {
  position: relative; display: flex; flex-wrap: wrap; align-items: center;
  justify-content: space-between; gap: var(--space-sm);
  padding: var(--space-md) var(--space-lg);
  border-top: 2px dashed var(--hair);
}
.gc__stub::before, .gc__stub::after {
  content: ""; position: absolute; top: 0; width: 1.25rem; height: 1.25rem;
  background-color: var(--surface-page); border-radius: 50%; translate: 0 -50%;
}
.gc__stub::before { left: 0; translate: -50% -50%; }
.gc__stub::after { right: 0; translate: 50% -50%; }

/* H — edge label ------------------------------------------------------- */
.gc--h { display: grid; grid-template-columns: auto 1fr; gap: var(--space-lg); }
.gc__edge { writing-mode: vertical-rl; rotate: 180deg; align-self: end;
  margin: 0; padding-block: var(--space-2xs); }

/* I — on the photograph ------------------------------------------------ */
.gc--i { position: relative; padding: 0; overflow: hidden; isolation: isolate;
  background-color: var(--gl-ink); }
.gc--i img { display: block; width: 100%; height: 100%; position: absolute;
  inset: 0; object-fit: cover; }
.gc__over {
  position: relative; z-index: 1; padding: var(--space-lg);
  min-height: 16rem; display: grid; align-content: end;
  background: linear-gradient(to top, rgb(12 10 9 / 0.82), rgb(12 10 9 / 0.34) 62%,
    rgb(12 10 9 / 0.12));
  color: var(--gl-stone-50);
  --text-primary: var(--gl-stone-50);
  --text-muted: rgb(250 250 249 / 0.78);
}

/* J — no card ---------------------------------------------------------- */
.gc--j { background: none; padding: 0; border-radius: 0; }
.gc__line { margin: 0 0 var(--space-sm); font-size: var(--text-lg); max-width: 34ch; }
.gc__line b { font-weight: var(--weight-semibold); }
/* No rule above the first row — the card's own edge is already the line. */
.gc__row:first-child { border-top: 0; padding-top: 0; }

/* The design system's gallery of the card family. */
.gopts { display: grid; gap: var(--space-3xl); margin-top: var(--space-2xl); }
@media (min-width: 72rem) { .gopts { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.gopt__head { display: flex; align-items: baseline; gap: var(--space-sm); }
.gopt__name { margin: 0; font-size: var(--text-lg); }
.gopt__note { margin: var(--space-2xs) 0 var(--space-md); color: var(--text-secondary);
  max-width: 54ch; }
.gopt__stage { max-width: 31.5rem; }

/* ==========================================================================
   WATCH HERO — the featured message
   The hero card with a stage row between the media and the lower edge, so the
   badge is centred by the layout instead of being floated over the whole card.
   An inset overlay is a positioned element and paints above the static content
   beneath it; on a short card that puts a 9rem disc on top of the title.
   ========================================================================== */
.watch-hero__stage {
  flex: 1;
  display: grid;
  place-items: center;
  /* A flex item's min-height is auto, so a tall child refuses to shrink and
     pushes the card past its min-height. The badge is decorative; the text at
     the bottom is not. */
  min-height: 0;
}

/* Condensed uppercase, the way message titles are set everywhere on this site.
   Smaller than a hero headline because it is a title, not a statement — the
   artwork behind it already carries the series at display scale. */
.watch-hero__title {
  font-family: var(--font-condensed);
  font-size: clamp(1.35rem, 1rem + 1.7vw, 2.25rem);
  font-weight: var(--weight-bold);
  line-height: 1.05;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  color: var(--gl-stone-50);
  text-wrap: balance;
  margin: 0;
}
.watch-hero__meta {
  margin-top: var(--space-3xs);
  font-family: var(--font-condensed);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: rgb(250 250 249 / 0.72);
}

/* ==========================================================================
   WATCH — archive layout
   Facets beside the grid from the desktop break; stacked below it, with the
   accordions carrying their own collapse, on anything narrower.
   ========================================================================== */
.watch__layout { display: grid; gap: var(--space-lg); }

@media (min-width: 64rem) {
  .watch__layout {
    grid-template-columns: minmax(0, 17rem) minmax(0, 1fr);
    gap: var(--space-2xl);
  }
  /* No align-items: start here. The sidebar has to stretch to the row's height
     or the sticky child has no range to travel through and scrolls away on the
     first pixel — sticky needs an outer box for range and an inner box that
     sticks. */
  .watch__sticky {
    position: sticky;
    /* --chrome-h is published by site.js and is the real distance to the
       bottom of the header, announcement bar included. The fallback is the
       header alone, for the frame before the script has measured. */
    top: calc(var(--chrome-h, var(--header-h)) + var(--space-md));
  }
}
.watch__sticky { display: grid; gap: var(--space-md); align-content: start; }
.watch__clear { justify-self: start; }

.watch__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-sm) var(--space-md);
  margin-top: var(--space-lg);
}
/* Visible as well as announced, unlike the Discover count: this grid pages, so
   "showing 24 of 48" is the only thing that says the rest exist. */
.watch__count {
  font-family: var(--font-condensed);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* A filter panel is not a page's FAQ. The accordion's own trigger is display
   sized because that is what it does on Plan A Visit; in a 17rem column the
   same rule sets two words at 18px bold with 24px of padding around them. */
.accordion--filters .accordion__trigger {
  font-size: var(--text-base);
  padding-block: var(--space-sm);
}
.accordion--filters .accordion__icon { width: 1rem; height: 1rem; }
/* A chevron turns over, it does not become a plus sign. Specificity beats the
   base rule's 45deg rather than relying on source order. */
.accordion--filters .accordion__trigger[aria-expanded="true"] .accordion__icon {
  transform: rotate(180deg);
}

/* ==========================================================================
   FACETS — a counted checkbox list
   ========================================================================== */
.facets { display: grid; list-style: none; margin: 0; padding: 0 0 var(--space-sm); }
.facet {
  display: flex;
  align-items: center;
  gap: var(--space-2xs);
  padding-block: 0.3rem;
  font-size: var(--text-sm);
  color: var(--text-secondary);
  cursor: pointer;
}
.facet:hover { color: var(--text-primary); }
/* Weight rather than colour: a checked row has to read as chosen at a glance
   down a list of fourteen, and the checkbox itself is only 17px of signal. */
.facet:has(input:checked) { color: var(--text-primary); font-weight: var(--weight-bold); }
.facet input {
  flex: none;
  width: 1.05rem; height: 1.05rem;
  accent-color: var(--surface-accent);
}
.facet__name { flex: 1 1 auto; min-width: 0; }
.facet__count {
  flex: none;
  font-family: var(--font-condensed);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* ==========================================================================
   MESSAGE CARD — 16:9 still, title, and one meta line
   The whole card is a play control: an empty button stretched across it, so
   the heading can stay a real <h3> outside the button's phrasing-only content
   model while the hit area is still the size of the card.
   ========================================================================== */
.msggrid {
  display: grid;
  gap: var(--space-lg) var(--space-md);
  grid-template-columns: minmax(0, 1fr);
  list-style: none;
  margin: 0;
  padding: 0;
}
@media (min-width: 40rem) { .msggrid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 75rem) { .msggrid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
/* The container runs to 2200px, so on a wide desktop three columns means three
   470px stills of the same 768px source — larger than the artwork, and larger
   than a card whose job is to be scanned. A fourth column past 1600px keeps
   the still near its natural size and puts more of the archive on screen. */
@media (min-width: 100rem) { .msggrid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
/* Filtered and paged-out cards leave the flow — a grid with holes in it reads
   as a loading state rather than a filtered one. */
.msgcard[hidden] { display: none; }

.msgcard {
  position: relative;
  display: grid;
  gap: var(--space-2xs);
  align-content: start;
}
.msgcard__media {
  position: relative;
  display: block;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-lg);
  overflow: hidden;
  /* Geometric clip as well as overflow: the card lifts its still on hover,
     which promotes it to its own layer, and a paint-time clip is not reliably
     applied to composited descendants. */
  clip-path: inset(0 round var(--radius-lg));
  background-color: var(--surface-media);
}
.msgcard__media img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform var(--dur-normal) var(--ease-out);
}
/* The stills are YouTube-style series graphics; without a play mark the grid
   reads as a page of pictures rather than a page of video. */
.msgcard__play {
  position: absolute;
  right: 0.625rem; bottom: 0.625rem;
  display: grid;
  place-items: center;
  width: 2.25rem; height: 2.25rem;
  border-radius: var(--radius-pill);
  background-color: rgb(12 10 9 / 0.55);
  -webkit-backdrop-filter: blur(8px);
          backdrop-filter: blur(8px);
  color: var(--gl-stone-50);
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.msgcard__play svg {
  width: 0.85rem; height: 0.85rem;
  /* A triangle's optical centre is left of its bounding box's centre. */
  margin-left: 0.09em;
}
.msgcard__title {
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-normal);
  text-transform: none;
  color: var(--text-primary);
  margin: 0;
}
/* The rule is on the inner span so it tracks the text rather than the block —
   the same treatment .tile__title carries, because these are the same gesture. */
.msgcard__title > span { position: relative; }
.msgcard__title > span::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  bottom: -0.16em;
  height: 2px;
  border-radius: 2px;
  background-color: var(--accent-mark);
  transform: scaleX(0);
  transform-origin: right center;
  transition: transform var(--dur-normal) var(--ease-out);
}
.msgcard__meta {
  font-family: var(--font-condensed);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
}
.msgcard__hit {
  position: absolute;
  inset: 0;
  z-index: 1;
  padding: 0;
  border: 0;
  border-radius: var(--radius-lg);
  background: none;
  cursor: pointer;
}
.msgcard__hit:focus-visible {
  outline: var(--border-width-thick) solid var(--focus-ring);
  outline-offset: 4px;
}
.msgcard:has(.msgcard__hit:focus-visible) .msgcard__title > span::after,
.msgcard:hover .msgcard__title > span::after {
  transform: scaleX(1);
  transform-origin: left center;
}
/* Sage takes ink, never light text — on the play mark it is a ground, which is
   the only thing sage is allowed to be. */
@media (hover: hover) and (pointer: fine) {
  .msgcard:hover .msgcard__media img { transform: scale(1.02); }
  .msgcard:hover .msgcard__play { background-color: var(--accent-mark); color: var(--gl-ink); }
}
@media (prefers-reduced-motion: reduce) {
  .msgcard__media img { transition: none; }
  .msgcard:hover .msgcard__media img { transform: none; }
}

/* ==========================================================================
   PLAYER — one dialog, every message
   ========================================================================== */
.player {
  /* Centred explicitly, for the reason .dirs is: this build's reset opens with
     `* { margin: 0 }`, which out-specifies the UA sheet's `inset: 0; margin:
     auto` and pins a modal dialog to the top-left corner. */
  position: fixed;
  inset: 0;
  margin: auto;
  width: min(64rem, calc(100vw - 2 * var(--gutter)));
  max-height: calc(100svh - 3rem);
  padding: var(--space-md);
  border: 0;
  border-radius: var(--radius-xl);
  background-color: var(--surface-raised);
  color: var(--text-primary);
  overflow: auto;
}
.player::backdrop {
  background-color: rgb(12 10 9 / 0.72);
  -webkit-backdrop-filter: blur(3px);
          backdrop-filter: blur(3px);
}
.player__head {
  display: flex;
  align-items: start;
  justify-content: space-between;
  gap: var(--space-md);
  margin-bottom: var(--space-sm);
}
.player__title {
  font-family: var(--font-condensed);
  font-size: var(--text-lg);
  font-weight: var(--weight-bold);
  line-height: var(--leading-snug);
  letter-spacing: 0.01em;
  text-transform: uppercase;
  margin: 0;
}
.player__meta {
  margin-top: var(--space-3xs);
  font-family: var(--font-condensed);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
}
.player__close {
  display: grid;
  place-items: center;
  width: 2.25rem; height: 2.25rem;
  flex: none;
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--text-muted);
  cursor: pointer;
  transition: background-color var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.player__close:hover { background-color: var(--surface-hover); color: var(--text-primary); }
.player__close svg { width: 1.1rem; height: 1.1rem; }
.player__frame {
  position: relative;
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-md);
  overflow: hidden;
  background-color: var(--gl-black);
}
.player__frame iframe { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; }

/* ==========================================================================
   GIVE
   ========================================================================== */
/* ---- the numbers --------------------------------------------------------
   A stats row is usually a trust-building trope and worth avoiding. It earns
   its place here because every figure is an EVENT rather than a metric — 158
   baptisms happened, $2.5m of medical debt was actually cleared — and each one
   carries a line saying what it paid for.

   The label sits above the figure, which is the whole difference between a
   list of things done and a scoreboard. */
.figures {
  list-style: none;
  margin: var(--space-2xl) auto 0;
  padding: 0;
  display: grid;
  gap: var(--space-xl) var(--space-2xl);
  max-width: var(--measure-list);
}
@media (min-width: 40rem) { .figures { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 64rem) { .figures { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.figure {
  display: grid;
  gap: var(--space-3xs);
  padding-top: var(--space-md);
  border-top: 1px solid var(--border-default);
}
/* Ink, like every other hand-drawn mark on the site — these are signage for
   the figure beneath them, not decoration beside it. */
.figure__icon {
  height: 2.75rem;
  width: auto;
  color: var(--text-primary);
  margin-bottom: var(--space-2xs);
}
.figure__label {
  margin: 0;
  font-family: var(--font-condensed);
  font-size: var(--text-2xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-label);
  text-transform: uppercase;
  color: var(--text-muted);
}
/* Condensed, because at this size the sans would be a headline competing with
   the section head rather than a figure. */
.figure__value {
  margin: 0;
  font-family: var(--font-condensed);
  font-size: clamp(2rem, 1.4rem + 2.4vw, 3.25rem);
  font-weight: var(--weight-bold);
  line-height: 0.95;
  letter-spacing: -0.02em;
}
.figure__note { margin: var(--space-3xs) 0 0; color: var(--text-secondary); }

/* ---- the story ---------------------------------------------------------- */
.givestory { display: grid; gap: var(--space-xl); align-items: center; }
@media (min-width: 66rem) {
  /* Copy left, film right — the same arrangement the pathway uses, so a reader
     meets one layout for "here is a thing to watch" across the site. */
  .givestory { grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); gap: var(--space-3xl); }
}
.givestory__copy { display: grid; justify-items: start; }
.givestory__copy .lead { margin: var(--space-md) 0 0; max-width: 46ch; }
.givestory__copy .btn { margin-top: var(--space-lg); }
