/* River City Church -- components.
   Every value below comes from tokens.css. No raw hex, no literal font stack. */

@import url("tokens.430189d55b.css");

*{box-sizing:border-box;}
/* The `hidden` attribute is a UA style, and an attribute selector carries the
   same specificity as a class, so ANY `display` set on a class beats it. That
   is not a hypothetical: .cards{display:grid} was keeping an empty, bordered
   #groups-live in the layout, where it rendered as a stray 2px rule under the
   City Groups copy, and .quiz-pick was keeping the native-share link in the
   accessibility tree while it was supposed to be gone.

   This had been patched four separate times, once per component, which is a
   pattern that only ever loses -- the fifth component to need it is the one
   nobody remembers. One !important here settles it for good: `hidden` means
   hidden, and nothing should be overriding that. */
[hidden]{display:none !important;}
html{
  background:var(--color-bg);
  scroll-behavior:smooth;
  overflow-x:hidden;                 /* fallback */
}
@supports (overflow:clip){
  /* clip contains the overflow without creating a scroll container at all */
  html{overflow-x:clip;}
}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto;}}

body{
  position:relative;
  margin:0;
  background:var(--color-bg);
  color:var(--color-text);
  font-family:var(--font-body);
  font-size:var(--text-base);
  line-height:var(--leading-body);
  -webkit-font-smoothing:antialiased;
}

/* film grain rides ON the sliding layer. Left on <body> it would stay put
   while the page slid, smearing texture across the uncovered panel.
   Noise is carried in the ALPHA channel so it reads on the near-white
   paper as well as on the photographs, multiplied so it darkens rather than
   screening out. Absolute, not fixed, so it travels with the page. */
.page::after{
  content:"";position:absolute;inset:0;z-index:var(--z-grain);pointer-events:none;
  background-image:var(--grain-img);
  background-size:var(--grain-tile);
  mix-blend-mode:multiply;
  opacity:var(--grain-opacity);
}

img{max-width:100%;display:block;}
a{color:var(--color-accent);}
::selection{background:rgba(var(--current-rgb),0.15);
background:color-mix(in srgb, var(--current) 15%, transparent);}
:focus-visible{outline:2px solid var(--color-accent);outline-offset:3px;}

.pic{font-family:var(--font-heading);font-weight:400;}
.scr{font-family:var(--font-accent);font-weight:400;}
.lab{
  font-size:var(--text-xs);letter-spacing:var(--tracking-label);
  text-transform:uppercase;color:var(--color-text-muted);
  margin:0 0 14px;font-variant-numeric:tabular-nums;
}
.dim{color:var(--color-text-muted);}
.skip{
  position:absolute;left:-9999px;top:0;z-index:100;
  background:var(--color-text);color:var(--color-text-invert);
  padding:10px 18px;font-size:var(--text-sm);text-decoration:none;
  border-radius:var(--radius-sm);
}
.skip:focus{left:0;}

/* ---------- nav ----------
   Desktop: fixed bar at the top, logo left, Menu right.
   Mobile: the same element becomes a floating centred pill at the bottom. */
/* How far the bar floats off the edges. Published on :root rather than kept
   inside .nav because the document overlays position themselves under the bar
   from --nav-h, which is its HEIGHT -- insetting the bar moves its bottom edge
   down by this much, and without the token those overlays would open 16px too
   high. Zero below 1024, where the bar is not a top bar at all. */
:root{--nav-inset:16px;}
@media (max-width:1023px){ :root{--nav-inset:0px; } }

.nav{
  /* A capsule, not a rounded bar: this radius is past half the bar's height,
     which is the point where the ends go fully round. That is deliberate here
     and the opposite of the call made for the mobile pill below, where the
     same value would have turned a small control into a lozenge. */
  --radius-nav:32px;
  /* Concentric host: the Menu pill sits in the bar's right-hand corners at
     exactly this inset, so its radius is derived rather than set. */
  --r:var(--radius-nav);
  /* The distance between the two corners is the padding plus the border the
     radius is drawn on, not the padding alone. */
  --r-inset:calc(11px + var(--border-width));
  position:fixed;
  top:var(--nav-inset);left:var(--nav-inset);right:var(--nav-inset);
  z-index:var(--z-nav);
  display:flex;align-items:center;justify-content:space-between;
  gap:var(--gap-md);
  /* Asymmetric: the lockup needs air on the left, the Menu pill nests close to
     the right edge the way the CTA does in the reference. */
  padding:11px 11px 11px clamp(20px,2.2vw,30px);
  background-color:var(--light-overlay);
  backdrop-filter:blur(6px) contrast(0.5) brightness(1.35) saturate(1.5);
  -webkit-backdrop-filter:blur(6px) contrast(0.5) brightness(1.35) saturate(1.5);
  border:var(--border);
  border-radius:var(--radius-nav);
  transition:background-color var(--dur-med),border-color var(--dur-med),
             -webkit-backdrop-filter var(--dur-med),backdrop-filter var(--dur-med);
}

/* From 1024 up the bar is a centred pill rather than a full-width rail, at the
   reference's own width. The lockup and the Menu pill sit at its two ends the
   way the reference's logo and CTA do. */
@media (min-width:1024px){
  .nav{
    left:50%;right:auto;
    width:min(493px, calc(100vw - 2 * var(--nav-inset)));
    transform:translateX(-50%);
  }
}

/* Open state. The bar spans the full width, so while the panel is uncovered its
   left half sits over the dimmed page and its right half sits over the dark
   panel -- one surface cannot serve both. The bar therefore gives up its own
   frosted ground (a light strip across the top of the panel is the giveaway),
   the logo stays dark because it sits over the dimmed page, and only the toggle
   inverts because it is the part that ends up over the panel. */
.menu-open .nav{
  background-color:transparent;
  backdrop-filter:none;-webkit-backdrop-filter:none;
  border-color:transparent;
}
.menu-open .nav-toggle{background:var(--paper);border-color:var(--paper);}
.menu-open .nav-toggle-label{color:var(--pine);}
.nav-mark{
  display:flex;align-items:center;
  /* 44px minimum touch target (WCAG 2.5.5). On mobile the visible badge is only
     30px inside the pill; the negative margin grows the hit area without
     changing the pill's layout. */
  min-height:44px;min-width:44px;justify-content:center;
}
.nav-lockup{width:auto;height:clamp(30px,3vw,42px);display:block;}
.nav-badge{width:34px;height:34px;display:block;fill:var(--color-text);}
.nav-badge{display:none;}          /* desktop shows the full lockup */
.nav-toggle{
  position:relative;overflow:hidden;
  display:flex;align-items:center;justify-content:center;
  min-width:96px;padding:11px 26px;
  background:var(--color-text);border:var(--border-width) solid var(--color-text);
  border-radius:var(--r-inner);cursor:pointer;
}
/* Above the fill, so the wipe passes behind the word rather than over it. */
.nav-toggle-label{position:relative;z-index:1;}

/* Hover fills the pill with the brand green, rising from the bottom edge.
   Desktop pointers only: on a touchscreen :hover latches after a tap and does
   not clear until you touch something else, so the pill would sit filled long
   after the press -- and the pill is the mobile control. `hover: hover`
   excludes touch, and the width bound keeps it off the mobile layout entirely.

   clip-path rather than a scaled element: a transform would scale the corner
   radius with it, and a clip is applied at composite time, so the fill stays
   inside the pill's curve for the whole sweep. Nothing moves and nothing
   reflows -- the pill keeps its size, its position and its corners. */
@media (hover: hover) and (pointer: fine) and (min-width:1024px){
  .nav-toggle::before{
    content:"";position:absolute;left:0;right:0;bottom:0;
    /* One wave taller than the pill, so at the top of the travel the crest has
       cleared the top edge and the fill is flat -- the wave reads as the
       surface rising, not as a notch cut out of the finished state. */
    --wave-h:9px;
    --wave-tile:calc(var(--wave-h) * 12.5);
    height:calc(100% + var(--wave-h));
    background:var(--color-accent);
    /* Two mask layers: the filled wave is the surface, the flat block below it
       is the body of the fill. The wave art is closed to the bottom of its own
       box, so the two butt together with no seam. */
    -webkit-mask-image:var(--squiggle-fill),linear-gradient(#000,#000);
            mask-image:var(--squiggle-fill),linear-gradient(#000,#000);
    -webkit-mask-repeat:repeat-x,no-repeat;
            mask-repeat:repeat-x,no-repeat;
    -webkit-mask-size:auto var(--wave-h),100% calc(100% - var(--wave-h));
            mask-size:auto var(--wave-h),100% calc(100% - var(--wave-h));
    -webkit-mask-position:0 top,0 bottom;
            mask-position:0 top,0 bottom;
    transform:translateY(100%);
    transition:transform var(--dur-btn) var(--ease-out);
  }
  /* Not while the panel is open: the toggle is already inverted there, and a
     green sweep over it would be a second, conflicting state. */
  html:not(.menu-open) .nav-toggle:hover::before{
    transform:translateY(0);
    /* The surface travels sideways while it is up, the same gesture the menu
       links make. Exactly one wavelength per cycle, so the loop never jumps. */
    animation:nav-fill-wave 1.1s linear infinite;
  }
}
/* Only the wave layer moves; the block beneath it stays put. */
@keyframes nav-fill-wave{
  from{-webkit-mask-position:0 top,0 bottom;mask-position:0 top,0 bottom;}
  to{-webkit-mask-position:var(--wave-tile) top,0 bottom;
             mask-position:var(--wave-tile) top,0 bottom;}
}
@media (prefers-reduced-motion:reduce){
  .nav-toggle::before{transition:none;}
  .nav-toggle:hover::before{animation:none;}
}
.nav-toggle-label{
  font-family:var(--font-heading);font-size:var(--btn-size);line-height:1;
  color:var(--color-text-invert);letter-spacing:.01em;
}
/* Parked off-screen only when JS is running and will animate it back in.
   Without JS, or in lite mode, the nav is simply present. */
/* The bar is the fixed layer everything else moves underneath, so it is
   present from first paint rather than appearing once the hero is gone. */

@media (max-width:1023px){
  .nav{
    top:auto;bottom:calc(env(safe-area-inset-bottom,0px) + 14px);
    left:auto;right:calc(env(safe-area-inset-right,0px) + 14px);
    width:auto;justify-content:center;gap:var(--gap-sm);
    padding:8px 8px 8px 18px;
    /* Back under half the height: see the note on the desktop bar. At 32px a
       control this small stops reading as a pill and becomes a lozenge. */
    --radius-nav:15px;
    --r-inset:calc(8px + var(--border-width));
    border:var(--border);border-radius:var(--radius-nav);
    box-shadow:var(--shadow-lift);
  }
  :root{--menu-width:82vw;}
  /* the pill is bottom-right, so when the panel is open it lands entirely on
     dark ground -- a solid light chip reads there, a translucent one does not */
  .menu-open .nav{
    background-color:var(--paper);
    border-color:var(--stone);
  }
  .menu-open .nav-toggle{background:var(--color-text);border-color:var(--color-text);}
  .menu-open .nav-toggle-label{color:var(--color-text-invert);}
  .nav-lockup{display:none;}
  .nav-badge{display:block;width:30px;height:30px;}
  .nav-mark{margin:-7px;}
  .nav-toggle{min-width:0;padding:10px 22px;}

}

/* ---------- concentric corners ----------
   A rendition of SwiftUI's ConcentricRectangle (iOS 26). Apple's rule: a
   nested shape's corner radius is its container's radius minus the distance
   between the two corners, so the curves share a centre instead of running at
   different rates. A child holding its own fixed radius inside a rounded
   parent is the thing that reads as wrong -- either a tight corner rattling
   around inside a soft one, or a rounder corner bulging out of it.

   The convention: a container declares the radius it draws (--r) and the
   inset it holds its children at (--r-inset). Both inherit, so any descendant
   can take var(--r-inner) as its own radius. --r-min is Apple's
   `.concentric(minimum:)` -- the floor that stops a deep inset squaring a
   child off entirely.

   Only containers whose children actually sit in their corners need this. A
   child that fills its parent edge to edge is already concentric at inset 0,
   and a control floating in the middle of a wide panel is not cornered by
   anything. */
.nav,
.cookie-note{
  --r-inner:max(var(--r-min, 4px), calc(var(--r) - var(--r-inset)));
}

/* ---------- hero ---------- */
/* Band on top, wordmark under it. A column at every width now -- the two-column
   arrangement the rails needed is gone, so there is no breakpoint that changes
   the direction, only the spacing. */
.hero{
  /* The band's geometry lives here rather than on .hero-band, because the
     wordmark is positioned against the tile width too -- it nests up into the
     band by a multiple of it.

     Every ratio below was measured off the reference rather than guessed. The
     band is a fixed-width centred cluster there, not a full-bleed strip: at
     1320px across eight columns a tile comes out 151px, and that is the size
     the photographs are meant to read at. */
  --band-cols:8;
  --band-gap:16px;
  --script-size:clamp(28px,4.6vw,44px);
  /* Tile heights are fixed, not multiples of the width -- that is how the
     reference does it, and it is why its band is the same 530 tall on a phone
     as on a desktop. Only the width is fluid. */
  --tile-h1:356px;   /* a lone tile */
  --tile-h2:230px;   /* one of a pair */
  --tile-h3:166px;   /* one of a trio */
  /* Fluid between two ends. It spreads to fill a 1320 cluster where there is
     room, and stops shrinking at 93 -- below that the band simply runs past
     both edges rather than dwindling, which is what the reference does on a
     phone: the eight columns stay, and the four centre ones are what you
     see. */
  --tile-w:max(93px, calc(
    (min(1320px, 100vw - 2 * var(--gutter)) - (var(--band-cols) - 1) * var(--band-gap))
    / var(--band-cols)
  ));
  position:relative;min-height:100svh;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  /* No gap: the wordmark overlaps the band, and that overlap is set as a
     negative margin on .hero-inner. A gap here would only fight it. */
  gap:0;
  /* Clears the floating bar from the same two tokens the bar is positioned
     with, so the band can never slide under it when either one changes. The
     bottom matches so the composition sits optically centred rather than
     pushed down by the top padding alone. */
  padding-block:calc(var(--nav-h) + var(--nav-inset) + clamp(12px,2vh,24px))
                clamp(24px,4.5vh,60px);
  overflow:hidden;
}
.hero-inner{
  /* Above the band rather than under it: the band is raised over the page
     grain, and the wordmark overlaps the band, so it has to clear both. */
  position:relative;z-index:calc(var(--z-rail) + 1);text-align:center;
  padding:0 var(--gutter);max-width:900px;
}
.hero-script{
  font-size:var(--script-size);line-height:var(--leading-snug);
  color:var(--color-accent);margin:0 0 6px;
}
.hero-name{
  font-size:var(--text-hero);line-height:.98;margin:0;
  letter-spacing:var(--tracking-tight);
}
/* "Church" as artwork rather than type: the script face is set as outlines so
   it always renders exactly as drawn, independent of the webfont. */
.hero-church{
  font-size:var(--text-hero);
  width:3.4em;margin:.04em auto 0;
  line-height:0;
}
/* The clip is what makes the raise read: the artwork starts below it and
   rises into view, exactly like .split-line does for "River City". The small
   padding/negative-margin pair keeps the glyph edges from being shaved at rest. */
.hero-church__clip{overflow:hidden;padding:.02em .1em .04em;margin:-.02em -.1em -.04em;}
.hero-church__svg{width:100%;height:auto;display:block;fill:var(--color-text);}

.hero-mission{
  font-size:clamp(19px,2.4vw,24px);line-height:1.5;
  color:var(--color-text-muted);max-width:34ch;margin:var(--gap-lg) auto 0;
}
.hero-cue{
  position:absolute;bottom:34px;left:50%;transform:translateX(-50%);z-index:2;
}
.hero-cue-line{
  display:block;width:1px;height:56px;background:var(--color-text-muted);
  transform-origin:top;opacity:.55;
}

/* ---------- hero band ----------
   A still block of photographs beneath the wordmark. Nothing moves; one tile
   at a time trades its photograph for another (see the hero band module in
   main.js), which is the only animation here.

   Every value is a token or derived from one: the column count and the tile
   width are the two numbers, and the three heights are multiples of the
   width. That is what keeps the band in proportion at any size without a
   breakpoint per width. */
.hero-band{
  width:100%;overflow:hidden;z-index:var(--z-rail);
}
/* Centred, and aligned to the top rather than the middle: every column offset
   below is measured down from a shared top edge, which is how the reference
   builds the shape. */
.hero-track{
  display:flex;align-items:flex-start;justify-content:center;gap:var(--band-gap);
}
/* Columns of 2, 3, 1, 1, 1, 1, 3, 2 -- fourteen photographs, which is what the
   pattern costs and why there are fourteen of them. A tile's height depends on
   how many share its column: a lone tile runs the full height, a pair splits
   it, a trio splits it again. */
.hero-col{
  flex:0 0 var(--tile-w);
  display:flex;flex-direction:column;gap:var(--band-gap);
}
.hero-col--one   .hero-tile{height:var(--tile-h1);}
.hero-col--two   .hero-tile{height:var(--tile-h2);}
.hero-col--three .hero-tile{height:var(--tile-h3);}

/* The shape. Three of the four column kinds hang below the top edge, and the
   amounts are the reference's: the pairs drop a little, the two outer singles
   drop a lot, and the trios and the two centre singles stay flush. It is the
   centre pair staying up that opens the dip the wordmark rises into. */
.hero-col--two {transform:translateY(27px);}
.hero-col--drop{transform:translateY(87px);}

/* The tile is the stage for the swap: the outgoing photograph stays put while
   the incoming one fades in over it, so the tile never shows its background
   and the change reads as a dissolve rather than a blink. */
.hero-tile{
  /* Proportional rather than a fixed token, because the tiles resize: the
     reference rounds a 151px tile by 32px, which is .212 of its width. The
     squircle case carries the same ~1.5x the radius tokens use, so the fuller
     curve reads at that roundness instead of rounder. */
  border-radius:calc(var(--tile-w) * .212);
  overflow:hidden;position:relative;
}
@supports (corner-shape: squircle){
  .hero-tile{border-radius:calc(var(--tile-w) * .318);}
}
.hero-tile picture{position:absolute;inset:0;}
.hero-tile img{
  width:100%;height:100%;object-fit:cover;display:block;
  filter:var(--grade);
}
/* The incoming layer starts invisible; the module adds .is-on a frame later,
   once the photograph has decoded, and the transition carries it in. */
.hero-tile picture.is-next{opacity:0;transition:opacity var(--dur-med) var(--ease-out);}
.hero-tile picture.is-next.is-on{opacity:1;}
@media (prefers-reduced-motion: reduce){
  .hero-tile picture.is-next{transition:none;}
}

/* The wordmark nests up into the band rather than sitting below it -- the
   difference between a block of photographs with a caption under it and one
   composition. */
/* How far up, measured off the reference: its text starts 467 below the band's
   top edge, and the band is 530 tall, so the text begins 63 above its bottom.
   The same at every width there, because the heights are fixed.

   That clears every column the text is wide enough to reach -- the outer
   singles end at 443 and the centre pair at 356 -- while the four stacked
   columns, which hang to 503 and 530, sit beyond its ends and flank it.

   Closer than the reference sits its heading, though. What has to clear 443 is
   "River City", the widest line, not the lead-in above it -- so the pull also
   carries the lead-in's own line height. Otherwise the gap grows with the
   lead-in as it scales, which is why it read as 60 of air on a phone and 76 on
   a desktop. Carrying it keeps "River City" a dozen or so below the columns at
   every width. */
.hero-inner{margin-top:calc(-72px - var(--script-size) * var(--leading-snug));}

/* Below 1024 the bar is the bottom-right pill rather than a top bar, so the
   hero's clearance moves with it: a small pad on top, and one at the bottom
   deep enough that the pill never lands on the mission line. Both are derived
   from the pill's own inset and height so the two cannot drift apart. */
@media (max-width:1023px){
  /* The band holds its full height here, so what is left over for the
     wordmark and the mission is tighter than on a desktop: the clearance
     above and the space before the mission both come in a step. */
  .hero-mission{margin-top:var(--gap-md);}
  .hero{
    padding-block:clamp(12px,2svh,28px)
                  calc(env(safe-area-inset-bottom,0px) + 14px + 64px + var(--gap-md));
  }
}

/* On a phone the hero stops claiming the whole viewport: a full-height band
   plus the wordmark leaves the mission line below the fold, and the band is
   the part that can afford to give up room. */
@media (max-width:767px){
  .hero{min-height:auto;}
  .hero-cue{display:none;}
}

/* The SMS opt-in. Quieter than the questions above it -- it is a permission,
   not part of the request -- but the box itself is full size, because a
   consent control that is hard to hit is not consent. */
.field-consent{margin-top:var(--gap-sm);}
/* Scoped through .field to outrank `.field label`, which is written for the
   short capitalised labels above ("First Name") and would otherwise stack this
   one and title-case a whole sentence into "Text Me Announcements From". */
.field .consent-opt{
  display:flex;align-items:flex-start;gap:11px;cursor:pointer;
  margin-bottom:0;
  font-size:.92rem;font-weight:400;line-height:var(--leading-body);
  text-transform:none;letter-spacing:normal;
  color:var(--color-text-muted);
}
.field .consent-opt input{
  flex:0 0 auto;width:20px;height:20px;margin:1px 0 0;cursor:pointer;
  accent-color:var(--color-accent);
}
.field .consent-opt a{color:var(--color-accent);}

/* ---------- cookie notice ----------
   Bottom left on a desktop, where nothing else lives. On a phone the Menu pill
   owns the bottom right, so the notice sits across the width just above it,
   using the pill's own inset and height rather than a guessed number. */
.cookie-note{
  --r:var(--radius-md);
  /* Two different numbers that are easy to conflate: --pad is the space the
     button sits in, --r-inset is the distance between the two corners, which
     is that padding PLUS the border the outer radius is drawn on. */
  --pad:10px;
  --r-inset:calc(var(--pad) + var(--border-width));
  /* Fixed to the viewport, which means living outside .page -- that wrapper is
     transformed for smooth scrolling, and a transformed ancestor would become
     this element's containing block and strand it at the foot of the document. */
  position:fixed;z-index:var(--z-nav);
  left:var(--nav-inset);bottom:var(--nav-inset);
  max-width:min(430px, calc(100vw - 2 * var(--nav-inset)));
  display:flex;align-items:center;gap:14px;
  padding:var(--pad) var(--pad) var(--pad) 16px;
  background:var(--color-surface);border:var(--border);
  border-radius:var(--r);box-shadow:var(--shadow-lift);
}
.cookie-note p{
  margin:0;font-size:.86rem;line-height:var(--leading-snug);
  color:var(--color-text-muted);
}
.cookie-note a{color:var(--color-accent);}
/* Concentric inside the notice, same rule the nav uses. */
.cookie-ok{
  flex:0 0 auto;cursor:pointer;
  padding:9px 16px;border:var(--border-width) solid var(--color-text);
  background:var(--color-text);color:var(--color-text-invert);
  font-family:var(--font-heading);font-size:.85rem;line-height:1;
  border-radius:var(--r-inner);
}
@media (max-width:1023px){
  .cookie-note{
    left:14px;right:14px;max-width:none;
    bottom:calc(env(safe-area-inset-bottom,0px) + 14px + 64px + var(--gap-sm));
  }
}

/* ---------- standalone document pages ----------
   Privacy and Terms. They reuse .doc-panel and the page's own type scale, so
   there is nothing new to maintain here beyond the chrome that replaces the
   homepage's nav and footer -- these pages carry no menu, because there is no
   script on them to open one. */
.doc-page{background:var(--color-bg);}
.doc-top{padding:clamp(20px,4vw,34px) var(--gutter) 0;}
.doc-home{
  display:inline-flex;align-items:center;gap:10px;
  font-family:var(--font-heading);font-size:1.05rem;
  color:var(--color-text);text-decoration:none;
}
.doc-home:hover{color:var(--color-accent);}
.doc-badge{display:block;}
.doc-date{color:var(--color-text-muted);font-size:.95rem;margin:0 0 var(--gap-lg);}
.doc-panel h2{
  font-family:var(--font-heading);
  font-size:clamp(19px,2.4vw,23px);line-height:var(--leading-snug);
  margin:var(--gap-lg) 0 .4em;
}
.doc-panel p,.doc-panel li{line-height:var(--leading-body);}
.doc-panel ul{margin:0 0 1em;padding-left:1.1em;}
.doc-panel li{margin-bottom:.5em;}
.doc-panel a{color:var(--color-accent);}
.doc-foot{
  padding:var(--gap-lg) var(--gutter) clamp(40px,8vw,80px);
  text-align:center;
}
.doc-foot p{margin:0 0 .5em;}
.doc-fine{color:var(--color-text-muted);font-size:.9rem;}

/* ---------- sections ---------- */
.sec{padding:var(--section-y) 0;}
/* a section carrying a scheme gets rules top and bottom to delineate it */
.sec.scheme-white,.sec.scheme-dark{
  border-top:var(--border);border-bottom:var(--border);
}
.col{max-width:var(--page-max);margin:0 auto;padding:0 var(--gutter);}

.h-xl{
  font-size:var(--text-h1);line-height:1.02;margin:0 0 40px;
  letter-spacing:var(--tracking-tight);text-wrap:balance;
}
.h-l{
  font-size:var(--text-h2);line-height:var(--leading-tight);margin:0 0 28px;
  letter-spacing:var(--tracking-tight);text-wrap:balance;
}
.lede{
  font-size:var(--text-md);line-height:1.55;max-width:var(--measure);
  margin:0 0 18px;
}
.note{
  font-size:15.5px;color:var(--color-text-muted);
  max-width:var(--measure);margin:18px 0 0;
}
.big-line{font-size:var(--text-lg);line-height:1.4;margin:0 0 6px;}

/* launch + youth detail grid */
.launch-grid{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(270px,1fr));
  gap:0;background:var(--color-surface);
  border:var(--border);border-radius:var(--radius-md);
  overflow:hidden;margin:8px 0 var(--gap-lg);
}
.launch-cell{padding:28px 26px 32px;}
.launch-cell p{margin:0;}
.launch-cell .lab{margin-bottom:12px;}
.launch-cell .dim{margin:4px 0 0;font-size:15.5px;}

/* The line that answers the question people actually arrive with. Sits between
   the headline and the logistics on purpose: the worry comes before the
   address, so it gets answered first. */
.launch-lead{
  font-size:var(--text-md);line-height:var(--leading-body);
  color:var(--color-text-muted);
  max-width:46ch;margin:18px 0 0;
}

/* Three-up variant. Two cells sat happily in one bordered box with no
   dividers; three need them, and the hairlines come from a 1px gap letting the
   grid's own background show through -- so a cell that wraps to its own row is
   divided correctly with no nth-child bookkeeping.

   Columns are declared per breakpoint rather than auto-fit: auto-fit drops to
   two tracks on a tablet and leaves the third cell sitting in column one with a
   hole beside it. Spanning it is only correct in that two-column case, which is
   exactly what a media query can say and auto-fit cannot. */
.launch-grid--three{
  grid-template-columns:1fr;
  gap:1px;background:var(--color-border);
}
.launch-grid--three .launch-cell{background:var(--color-surface);}
@media (min-width:768px) and (max-width:1023px){
  .launch-grid--three{grid-template-columns:1fr 1fr;}
  .launch-grid--three > .launch-cell:last-child{grid-column:1 / -1;}
}
@media (min-width:1024px){
  .launch-grid--three{grid-template-columns:repeat(3,1fr);}
}

/* ---------- buttons ---------- */
.cta-row{display:flex;gap:14px;flex-wrap:wrap;align-items:center;margin:0;}
.cta-after-cards{margin-top:30px;}
.btn{
  display:inline-block;cursor:pointer;
  background:var(--color-text);color:var(--color-text-invert);
  text-decoration:none;
  font-family:var(--font-heading);font-size:var(--btn-size);letter-spacing:.01em;
  padding:14px 30px;
  border:var(--border-width) solid var(--color-text);
  border-radius:var(--radius-btn);
  /* Two stacked rings, both present at 0 width so the spread has something to
     animate from -- a shadow that only exists on :hover has no start value and
     would pop in. The inner ring paints the scheme background, so it reads as a
     gap between the button and the outer ring on any scheme. */
  box-shadow:0 0 0 0 var(--color-bg), 0 0 0 0 var(--color-text);
  transition:border-radius var(--dur-btn) var(--ease-btn),
             box-shadow var(--dur-btn) var(--ease-btn);
}
/* Hover changes shape only. Fill and text colour stay put, so a button never
   inverts under the cursor -- the rings and the radius carry the whole gesture. */
.btn:hover{
  border-radius:var(--radius-btn-hover);
  box-shadow:0 0 0 3px var(--color-bg), 0 0 0 5px var(--color-text);
}
.btn-quiet{background:transparent;color:var(--color-text);}
/* Share glyph. Sized in em so it tracks --btn-size, and the viewBox is taller
   than it is wide (362.5 x 512), so the width is set from that ratio rather
   than left square -- otherwise the arrow sits in a box with dead space either
   side and the gap after "Share" looks wrong. */
.share-ico{
  width:.5em;height:.72em;fill:currentColor;
  vertical-align:-.055em;margin-left:.18em;
}
/* Tertiary: same type size, tighter box. Overriding the two radius custom
   properties rather than border-radius directly keeps it inside the existing
   hover animation -- and it has to be smaller, because the 1.75rem default on a
   42px-tall control exceeds half its height, so both the resting and hover radii
   would clamp to a pill and the transition would be invisible.
   Composes: class="btn btn-quiet btn-tertiary" for a compact outline button. */
.btn-tertiary{
  /* 1.17rem is 2rem x 0.583 -- the round branch has to be smaller to land at the
     same apparent roundness as the squircle one. See the parity note in tokens. */
  --radius-btn:1.17rem;
  --radius-btn-hover:1.1rem;
  padding:0 18px;
}
@supports (corner-shape: squircle){
  .btn-tertiary{--radius-btn:2rem;}
}
.btn-soon{
  display:inline-block;font-family:var(--font-heading);font-size:var(--btn-size);
  color:var(--color-text-muted);
  border:var(--border-width) dashed var(--color-border-strong);
  padding:13px 24px;border-radius:var(--radius-btn);
}

/* ---------- photo breaks ---------- */
.break{
  margin:0;position:relative;overflow:hidden;
  z-index:var(--z-rail);
  height:clamp(380px,68vh,760px);
}
/* the break image parallaxes, so its grain rides with it */
.break::after{
  content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
  background-image:var(--grain-img);
  background-size:var(--grain-tile);
  mix-blend-mode:multiply;opacity:var(--grain-opacity);
}
.break-short{height:clamp(300px,52vh,560px);}
.break-tall{height:clamp(420px,74vh,820px);}
.break img{
  width:100%;height:120%;object-fit:cover;filter:var(--grade);
  position:absolute;inset:0;
}
.break-veil{
  position:absolute;inset:0;z-index:1;
  background:linear-gradient(180deg,
    rgba(var(--ink-rgb),0) 30%,
    rgba(var(--ink-rgb),0.32) 68%,
    rgba(var(--ink-rgb),0.66) 100%);
  background:linear-gradient(180deg,
    color-mix(in srgb, var(--ink) 0%, transparent) 30%,
    color-mix(in srgb, var(--ink) 32%, transparent) 68%,
    color-mix(in srgb, var(--ink) 66%, transparent) 100%);
}
.break-caption{
  position:absolute;left:0;right:0;bottom:0;z-index:2;
  padding:clamp(24px,4.5vw,48px) var(--gutter);
  max-width:var(--page-max);margin:0 auto;
}
.break-script{
  font-size:clamp(30px,4.8vw,52px);line-height:var(--leading-snug);
  color:var(--color-text-invert);display:inline-block;
}

/* ---------- cards ---------- */
.cards{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));
  gap:var(--border-width);background:var(--color-border);
  border:var(--border);border-radius:var(--radius-md);
  overflow:hidden;margin-top:38px;
}
/* The City Groups grid used to live here: `.cards` is auto-fit, which leaves a
   bare cell when the count does not divide evenly, and because that grid draws
   its hairlines as container background showing through the gaps, an empty
   cell read as a grey block. Three breakpoints of explicit column counts and
   six-track span arithmetic existed to fill the last row.

   All of it went when the groups became listings. They no longer sit inside
   the bordered `.cards` container, so there is no background to show through
   and nothing to fill -- a short final row is just a short final row. The
   layout is `.glist`, further down. These rules were keyed to #groups-live,
   which outranks any class, so leaving them would have quietly overridden it. */

.card{
  background:var(--color-surface);padding:30px 28px 34px;
  display:flex;flex-direction:column;gap:10px;
}
.card h3{
  font-family:var(--font-heading);font-weight:400;font-size:26px;
  margin:0;line-height:1.1;
}
/* `.card p` (0,1,1) outranks a bare `.card-age` (0,1,0), so the accent never
   applied. Matching the element keeps the two selectors from cancelling. */
.card p.card-age,
.card p.card-start{
  font-size:var(--text-xs);letter-spacing:.14em;text-transform:uppercase;
  color:var(--color-accent);margin:0;
}
/* The schedule and the start date read as one two-line eyebrow, so the card's
   10px gap is too airy between them. Spacing only -- the type treatment above
   is deliberately shared. */
.card p.card-start{margin-top:-4px;}
.card p{margin:0;font-size:15.5px;line-height:1.55;color:var(--color-text-muted);}
.card p:not(.card-age):not(.card-start):not(.card-cta){flex-grow:1;}
/* auto, not a fixed gap. Group cards carry no body copy to stretch, so without
   this the buttons sit at different heights the moment one card has a start
   line and the card beside it does not.

   Matched as `.card p.card-cta` for the same reason `.card-age` is: a bare
   `.card-cta` (0,1,0) loses to `.card p` (0,1,1), which is why the `margin-top`
   that used to live here never applied at all. */
.card p.card-cta{margin-top:auto;padding-top:8px;}

/* The house and the group name, kept deliberately quiet. Muted and sentence
   case, so that under a city heading a name like "Sheffield City Group" reads
   as the group's name rather than as a claim about where it meets. Same
   specificity dance as the eyebrows above: a bare `.card-detail` loses to
   `.card p`. */
.card p.card-detail{
  font-size:var(--text-xs);color:var(--color-text-muted);
  margin:6px 0 0;line-height:1.45;
}

/* ---------- groups by city ----------
   Only rendered once some city holds more than one group; below that the city
   is the card's own heading and this never appears. Sections rather than one
   long grid so a city reads as a set, with the heading close to the cards it
   labels. */
/* ---------- group listings ----------
   Laid out like a property listing rather than as panels in a bordered grid:
   the image floats, the text sits on the page beneath it, and the whole thing
   is one link. Borrowed deliberately -- someone picking a group is asking the
   same shape of question as someone picking a house, so the slot order that
   works there works here.

   No shared container border, which is the main departure from `.cards`. That
   component stays exactly as it is for the beliefs and kids grids; this is its
   own thing. */
.glist{
  display:grid;grid-template-columns:1fr 1fr;
  /* Split, not one `gap`. The columns want to stay tight so a pair reads as a
     pair, but the rows need far more room: a card ends on a small outlined
     button and the next one starts with a block of solid green, so at the same
     spacing the button looks like it belongs to the image below it. */
  column-gap:clamp(15px,3vw,34px);
  row-gap:clamp(38px,8vw,56px);
  margin-top:38px;
}
/* Two or four, never three. With four groups a three-column row leaves the
   fourth stranded on a line of its own, and a lone card under a full row reads
   as a mistake rather than as a list that ran out. Both 2 and 4 divide four
   evenly; the tablet tier that used to sit between them is gone for that
   reason alone.

   Worth knowing when a fifth group arrives: five divides by neither, so it
   will strand one whichever of these is in play. Three columns is the only
   count that handles five cleanly. */
@media (min-width:1024px){ .glist{grid-template-columns:repeat(4,1fr);} }

.glist-card{
  position:relative;
  display:flex;flex-direction:column;gap:5px;
}

/* The badge is positioned against this, and the radius has to be clipped here
   rather than on the image: a border-radius on the <img> alone leaves the
   badge's corner sitting outside the curve. */
.glist-media{
  position:relative;display:block;overflow:hidden;
  border-radius:var(--radius-md);margin-bottom:11px;
}
/* Square, one ratio at every width. object-fit:cover trims whichever axis
   overflows, and the artwork is landscape, so a square frame takes it off the
   sides -- the lettering has to sit inside the middle of each file for the
   word to survive. It does; the wordmarks are drawn with that margin on
   purpose. Worth knowing if they are ever redrawn nearer their edges: the
   symptom would be a missing letter, which reads as a bad image rather than
   as a layout bug. */
.glist-media img{
  display:block;width:100%;aspect-ratio:1/1;object-fit:cover;
  transition:transform var(--dur-med) var(--ease-out);
}
/* Two up on a phone, where a square image stacks the cards very tall. 2:1
   crops the top and bottom rather than the sides, so the wordmarks are never
   at risk -- measured at 30-40px of clearance on the current artwork. */
@media (max-width:767px){
  .glist-media img{aspect-ratio:2/1;}
}
/* A listing grid gives no other hint that a card is clickable, so the image
   answers the pointer. Suppressed where motion is unwelcome. */
@media (hover:hover){
  .glist-card:hover .glist-media img{transform:scale(1.03);}
}
@media (prefers-reduced-motion:reduce){
  .glist-media img{transition:none;}
  .glist-card:hover .glist-media img{transform:none;}
}

/* Sits where "Active" sits on a property card, and carries the one fact with
   a deadline on it. The proxy stops sending a start date once the group is
   running, so this disappears on its own. */
.glist-title{
  font-family:var(--font-heading);font-weight:400;font-size:26px;
  margin:0;line-height:1.1;text-decoration:none;color:inherit;
}
.glist-title::after{content:"";position:absolute;inset:0;z-index:1;}

/* Above the stretched link, or the link would swallow the click. The dark
   circle is the page's own ink rather than a translucent scrim: the wordmarks
   are a flat mid-green, and a scrim over them reads as a smudge. */
.glist-share{
  position:absolute;top:7px;right:7px;z-index:2;
  width:26px;height:26px;display:grid;place-items:center;
  border:0;padding:0;cursor:pointer;
  border-radius:var(--radius-pill);
  background:var(--color-text);color:var(--color-text-invert);
  transition:transform var(--dur-fast) var(--ease-out);
}
.glist-share svg{width:11px;height:11px;fill:currentColor;display:block;}
@media (min-width:768px){
  .glist-share{top:10px;right:10px;width:32px;height:32px;}
  .glist-share svg{width:13px;height:13px;}
}
@media (min-width:1024px){
  .glist-share{top:12px;right:12px;width:38px;height:38px;}
  .glist-share svg{width:15px;height:15px;}
}
@media (hover:hover){ .glist-share:hover{transform:scale(1.08);} }
@media (prefers-reduced-motion:reduce){ .glist-share{transition:none;} }
.glist-share:focus-visible{outline:3px solid var(--color-accent);outline-offset:3px;}

/* Only reached when there is no native share sheet, which is to say on a
   desktop: the link went to the clipboard and the button has to say so, since
   nothing else on screen changed. */
.glist-share.is-copied::after{
  content:"Link copied";position:absolute;right:0;top:calc(100% + 8px);
  background:var(--color-text);color:var(--color-text-invert);
  font-size:var(--text-xs);letter-spacing:.06em;
  padding:6px 11px;border-radius:var(--radius-pill);white-space:nowrap;
}
.glist-spec{font-size:var(--text-sm);color:var(--color-text);}

/* Carries .btn .btn-quiet .btn-tertiary, so the shape, type and hover
   animation all come from the button component rather than being restated
   here. Only three things differ: it must not stretch to the card's width, it
   needs the gap above, and its hover is driven by the CARD -- the pointer is
   never on the span itself, it is anywhere on the tile. */
.glist-cue{
  /* auto, not a fixed gap -- the same reasoning as .card p.card-cta. House
     names run to one line or two depending on the name, and grid stretches
     every card in a row to the tallest. Without this the buttons sit at
     whatever height their own text left them, so a row of them steps up and
     down; with it they land on one line across the row. */
  align-self:flex-start;margin-top:auto;padding-top:12px;
  /* Smaller than a page-level button. --btn-size is the component's own knob,
     so overriding it here scales the type without restating any of the box.

     The radii come down with it for a reason the .btn-tertiary note explains:
     a radius above half the control's height clamps to a pill, and if both the
     resting and hover values clamp, the hover animation has nowhere to travel
     and silently does nothing. At this size 2rem would do exactly that. */
  --btn-size:.95rem;
  --radius-btn:.75rem;
  --radius-btn-hover:1.4rem;
  padding:0 15px;
}
@media (min-width:1024px){
  .glist-cue{--btn-size:1.05rem;padding:0 17px;}
}
.glist-card:hover .glist-cue{
  border-radius:var(--radius-btn-hover);
  box-shadow:0 0 0 3px var(--color-bg), 0 0 0 5px var(--color-text);
}
.glist-title:focus-visible{
  outline:3px solid var(--color-accent);outline-offset:6px;
  border-radius:var(--radius-sm);
}

/* ---------- expect list ---------- */
.expect{list-style:none;margin:26px 0 0;padding:0;display:flex;flex-direction:column;}
.expect li{padding:26px 0;border-bottom:var(--border);max-width:62ch;}
.expect li:last-child{border-bottom:0;}
.expect h3{
  font-family:var(--font-heading);font-weight:400;font-size:var(--text-h3);
  margin:0 0 6px;line-height:1.2;
}
.expect p{margin:0;color:var(--color-text-muted);font-size:16px;}

/* ---------- beliefs ---------- */
.beliefs{margin-top:40px;}
/* Belief statements are the point of the section, so they keep the full text
   colour and size rather than the muted body the other cards use.

   The 340px floor forces a 2x2 rather than the default grid's 3-across, which
   left the fourth card orphaned on its own row. min(100%, 340px) lets it fall
   to a single column on a phone without needing a breakpoint. */
.cards--beliefs{grid-template-columns:repeat(auto-fit,minmax(min(100%,340px),1fr));}
.cards--beliefs .card p{color:var(--color-text);font-size:16.5px;line-height:1.55;}
/* Section head: copy on the left, a supporting control on the right. The aside
   is a grid cell rather than a float so the button stays top-aligned with the
   label no matter how many lines the heading runs to. */
.sec-head{display:grid;grid-template-columns:2fr 1fr;gap:1rem;}
.sec-head-aside{justify-self:end;text-align:right;}
.sec-head-aside .cta-row,
.sec-head-aside .btn-soon{margin-top:0;}
.sec-head > :first-child > :last-child{margin-bottom:0;}
/* One column on a phone. This has to sit AFTER the desktop rule, not up in the
   earlier mobile block: both selectors are .sec-head, a media query adds no
   specificity, so whichever comes last wins -- and from up there the desktop
   2fr 1fr was quietly overriding it at every width. */
@media (max-width:767px){
  .sec-head{grid-template-columns:1fr;gap:0;}
  .sec-head-aside{justify-self:start;text-align:left;margin-top:18px;}
}

/* City Groups on a phone: the mark and the button share the top line, and the
   heading starts underneath them. Only this section has a mark to pair with,
   so it is scoped rather than applied to every sec-head.

   display:contents on the inner wrapper is what makes it possible without
   touching the markup -- it dissolves the box so the mark, heading and lede
   become grid items of .sec-head itself and can be placed individually. The
   visually-hidden label inside it is position:absolute, so it takes no track.

   The mark needs an explicit width because it is a masked span: its height is
   set by --ico-h and the mask paints at the artwork's own ratio, so nothing
   would give the grid a width to work with. */
@media (max-width:767px){
  #groups .sec-head{
    grid-template-columns:auto 1fr;column-gap:14px;align-items:center;
  }
  /* :not(.sec-head-aside) is load-bearing. The aside is also a direct div
     child, so a bare `> div` dissolves it too -- the button loses its box,
     collapses to 0x0, and the placement below has nothing to place. */
  #groups .sec-head > div:not(.sec-head-aside){display:contents;}
  #groups .sec-ico--groups{
    grid-column:1;grid-row:1;
    width:calc(var(--ico-h) * 1.1);margin:0;
  }
  #groups .sec-head-aside{
    grid-column:2;grid-row:1;justify-self:end;align-self:center;margin-top:0;
  }
  #groups .sec-head h2,
  #groups .sec-head .lede{grid-column:1 / -1;}
  #groups .sec-head h2{margin-top:20px;}
}

/* ---------- belief icons ----------
   Masks, not <img> and not inlined SVG. Three reasons, all of them blocking:
   the source files are fill:#fff and would be invisible on a light card; all
   four declare id="b"/id="c" and share class names, so inlining them together
   would collide; and at ~90KB total they belong in the cache, not in every HTML
   response. A mask ignores the file's own colour and paints with
   background-color, which also carries the 25%-opacity second tone through as
   25% of currentColor, and makes them scheme-aware for free.

   auto 100% sizes by height so the four different aspect ratios (the crown is
   1.3 wide, the cross is 0.7 tall) sit on a common baseline. */
.belief-ico,
.sec-ico{
  display:block;width:100%;height:var(--ico-h,42px);
  margin:0 0 18px;
  background-color:var(--color-accent);
  -webkit-mask:var(--ico) left center / auto 100% no-repeat;
          mask:var(--ico) left center / auto 100% no-repeat;
}
.belief-ico--god        {--ico:url("../assets/img/icons/god.a6e8f11d5d.svg");}
.belief-ico--jesus      {--ico:url("../assets/img/icons/jesus.cd00ae75fc.svg");--ico-h:48px;}
.belief-ico--holy-spirit{--ico:url("../assets/img/icons/holy-spirit.b44af64f2f.svg");--ico-h:46px;}
.belief-ico--bible      {--ico:url("../assets/img/icons/bible.6e23381799.svg");}

/* Same treatment above a section label rather than inside a card, so the mark
   reads as one of the set rather than a pasted-in logo. Its file is cropped to
   the artwork like the others: Illustrator exported it on a square 2000
   artboard with the ink filling only 89% of the height, which at a shared
   height would have rendered it visibly smaller than its neighbours.

   Taller than the belief icons on purpose. Those are pictograms -- a crown, a
   cross -- and read perfectly at 42px. This one has the words CITY GROUPS set
   inside a house outline, and lettering needs the height that a silhouette
   does not. */
.sec-ico{margin:0 0 14px;}
.sec-ico--groups{--ico:url("../assets/img/icons/city-groups.89d9de9df3.svg");--ico-h:78px;}

/* ---------- glossed term ----------
   A real <button>, not a title attribute: title never fires on touch, is
   unstyleable, and screen readers treat it inconsistently. aria-describedby
   ties the note to the term, so it is announced as a description rather than
   read as a stray sentence in the paragraph. Opens on hover, on keyboard
   focus, and on tap (the .is-open class the script toggles). */
.gloss{position:relative;display:inline-block;}
.gloss-term{
  font:inherit;color:inherit;background:none;border:0;padding:0;cursor:help;
  text-decoration:underline dotted currentColor;
  text-underline-offset:.22em;text-decoration-thickness:1px;
}
/* When a glossed term sits in a paragraph SplitText has processed, each word
   becomes an inline-block -- and text-decoration does not propagate into an
   inline-block, so the dotted underline drew only in the gaps BETWEEN words
   and read as two stray dots. Inline restores it; the reveal animates opacity
   only, which behaves identically either way. !important because SplitText
   writes display:inline-block as an inline style. */
.gloss-term .reveal-word{display:inline !important;}
.gloss-note{
  position:absolute;left:50%;bottom:calc(100% + 10px);
  /* --shift is written by the script when centring the note on the word would
     push it past the edge of its column. .cards--beliefs clips its overflow, so
     a note that spills is not merely ugly there, it is cut mid-sentence.
     Zero on the common path.

     It is a margin and not part of the transform below on purpose: transform is
     transitioned, and a var() change inside a transitioned transform does not
     reliably re-resolve -- the note reads the new number and keeps the old
     position. Margin is not transitioned, so it simply applies. */
  margin-left:var(--shift, 0px);
  transform:translateX(-50%) translateY(4px);
  width:max-content;max-width:min(320px, 78vw);
  background:var(--pine);color:var(--paper);
  font-family:var(--font-body);font-size:14px;line-height:1.5;
  text-align:left;letter-spacing:0;
  padding:12px 14px;border-radius:var(--radius-sm);
  box-shadow:var(--shadow-lift);
  opacity:0;visibility:hidden;pointer-events:none;z-index:var(--z-nav);
  transition:opacity var(--dur-fast),transform var(--dur-fast),visibility var(--dur-fast);
}
.gloss-note::after{           /* the little pointer */
  content:"";position:absolute;top:100%;left:var(--arrow-x,50%);
  transform:translateX(-50%);
  border:6px solid transparent;border-top-color:var(--pine);
}
.gloss:hover .gloss-note,
.gloss:focus-within .gloss-note,
.gloss.is-open .gloss-note{
  opacity:1;visibility:visible;transform:translateX(-50%) translateY(0);
}
@media (prefers-reduced-motion:reduce){.gloss-note{transition:none;}}

/* On a phone the panel is nearly as wide as the column, so centring it on the
   word only works while the word happens to sit mid-line: at 375px the term has
   to land between x=146 and x=229 or the panel leaves the screen. Anchor it to
   the paragraph instead and let it span the column; the pointer still tracks the
   word via --arrow-x, which the script sets from the measured positions. */
@media (max-width:1023px){
  .has-gloss{position:relative;}
  .gloss{position:static;}
  .gloss-note{
    left:0;right:0;width:auto;max-width:none;
    /* already spans the column, so there is nothing to nudge it away from */
    margin-left:0;
    transform:translateY(4px);
  }
  .gloss:hover .gloss-note,
  .gloss:focus-within .gloss-note,
  .gloss.is-open .gloss-note{transform:translateY(0);}
}

/* ---------- document modal ---------- */
.doc{
  position:fixed;inset:0;z-index:var(--z-doc);overflow-y:auto;
  background:var(--color-scrim);
  display:flex;align-items:flex-start;justify-content:center;
  /* published so .doc-close can work out where the panel's edge actually is */
  --doc-pad:clamp(16px,4vw,56px);
  padding:var(--doc-pad);
  /* opens directly under the nav bar rather than floating in the middle */
  padding-top:calc(var(--nav-h) + var(--nav-inset) + 16px);
}
.doc-panel{
  position:relative;overflow:hidden;
  background:var(--color-surface);border:var(--border);
  border-radius:var(--radius-lg);
  max-width:720px;width:100%;margin:0;
  padding:clamp(30px,5vw,56px) clamp(24px,5vw,60px) clamp(40px,5vw,64px);
}
.doc-panel > .pic{font-size:clamp(30px,4.6vw,42px);line-height:var(--leading-tight);margin:0 0 8px;}
.doc-close{
  position:fixed;
  top:calc(var(--nav-h) + var(--nav-inset) + 30px);
  /* Anchored to where the panel's right edge actually is. The panel is 720px
     wide when there is room and viewport-minus-padding when there is not, so
     its inset from the viewport is max(--doc-pad, 50% - 360px); +14px places
     the control inside that edge, matching the 14px gap below the panel top.
     The previous formula assumed 720px unconditionally, which jammed the
     button 3px from the edge on any viewport where padding was the limit. */
  right:calc(max(var(--doc-pad), 50% - 360px) + 14px);
  width:44px;height:44px;z-index:1;
  background:var(--color-surface);border:var(--border);
  box-shadow:var(--shadow-lift);
  cursor:pointer;color:var(--color-text-muted);
  font-size:28px;line-height:1;font-family:var(--font-body);
  border-radius:var(--radius-pill);
}
.doc-close:hover{color:var(--color-text);}

/* These sat in the early mobile block and never applied: a media query adds no
   specificity, so a later base rule beats it regardless of viewport. They have
   to live after .doc / .doc-close in the source. On phones the nav pill is at
   the bottom, so the modal needs no clearance for it at the top. */
@media (max-width:1023px){
  .doc{padding-top:var(--doc-pad);}
  .doc-close{top:calc(env(safe-area-inset-top,0px) + var(--doc-pad) + 14px);}
}
.faith-body h3{
  font-family:var(--font-heading);font-weight:400;font-size:20px;margin:26px 0 6px;
}
.faith-body p{margin:0;font-size:15px;line-height:1.68;color:var(--color-text-muted);}

/* ---------- contact quiz ----------
   The site had no form styling at all before this, so every input rule here is
   new. Controls borrow the existing token vocabulary rather than introducing a
   parallel one, and the buttons compose .btn / .btn-quiet / .btn-tertiary so a
   change to the shared hover gesture reaches them too. */
.quiz-step{animation:quiz-in var(--dur-med) var(--ease-out) both;}
@keyframes quiz-in{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:none;}}

/* Each step's heading takes focus so a screen reader announces which step this
   is. It carries tabindex="-1", so it is never reachable by Tab -- the ring would
   only ever appear on that programmatic focus, where it reads as a stray box
   around the title. Removing it costs no keyboard affordance. */
.quiz-step h2:focus{outline:none;}

.quiz-lede{margin:0 0 26px;font-size:var(--text-md);line-height:var(--leading-body);color:var(--color-text-muted);}

/* step 1 -- the three choices */
.quiz-picks{display:grid;gap:10px;}
.quiz-pick{
  display:block;width:100%;text-align:left;text-decoration:none;cursor:pointer;
  background:var(--color-surface-alt);color:var(--color-text);
  border:var(--border-width) solid var(--color-border);
  border-radius:var(--radius-md);
  padding:18px 22px;font-family:inherit;
  transition:border-color var(--dur-fast) var(--ease-out),
             background-color var(--dur-fast) var(--ease-out),
             transform var(--dur-fast) var(--ease-out);
}
.quiz-pick:hover,.quiz-pick:focus-visible{
  border-color:var(--color-border-strong);
  background:var(--color-surface);
  transform:translateX(3px);
}
.quiz-pick-t{display:block;font-family:var(--font-heading);font-size:var(--text-h3);line-height:var(--leading-snug);}
.quiz-pick-d{display:block;margin-top:3px;font-size:var(--text-sm);color:var(--color-text-muted);}

/* step 2 -- the form */
.quiz-back{
  display:inline-block;cursor:pointer;background:none;border:0;padding:0;margin:0 0 18px;
  font-family:var(--font-body);font-size:var(--text-sm);color:var(--color-text-muted);
}
.quiz-back:hover{color:var(--color-text);}

.quiz-form{margin:26px 0 0;}
.field{position:relative;margin:0 0 18px;}
.field-row{display:grid;grid-template-columns:1fr 1fr;gap:14px;}
.field label{
  display:block;margin-bottom:6px;
  /* Capitalised rather than uppercase, so --tracking-label comes off with it:
     .15em exists to open up all-caps and reads as broken on mixed case. */
  font-size:var(--text-sm);font-weight:600;text-transform:capitalize;
  letter-spacing:normal;
  color:var(--color-text-muted);
}
.field input,.field textarea{
  display:block;width:100%;box-sizing:border-box;
  font-family:var(--font-body);font-size:var(--text-base);line-height:var(--leading-body);
  color:var(--color-text);background:var(--field-bg);
  border:var(--border-width) solid var(--color-border-strong);
  border-radius:var(--radius-sm);
  padding:12px 14px;
  transition:border-color var(--dur-fast) var(--ease-out),
             box-shadow var(--dur-fast) var(--ease-out);
}
.field textarea{resize:vertical;min-height:120px;}
.field input:focus,.field textarea:focus{
  outline:none;border-color:var(--color-text);
  box-shadow:0 0 0 3px var(--color-bg),0 0 0 4px var(--color-text);
}
/* Only ever set by JS after a server rejection -- never :invalid, which fires
   while someone is still halfway through typing their email. */
.field.is-bad input,.field.is-bad textarea{border-color:var(--color-accent);}
.field.is-bad label{color:var(--color-accent);}

.field-count{
  position:absolute;right:2px;bottom:-17px;
  font-size:var(--text-xs);color:var(--color-text-muted);opacity:0;
  transition:opacity var(--dur-fast) var(--ease-out);
}
.field textarea:focus ~ .field-count,.field-count.is-near{opacity:1;}

/* Off-screen rather than display:none -- some bots skip hidden inputs, and the
   ones that do not are exactly the ones worth catching. */
.hp-field{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden;}

/* Yes/No pairs. A fieldset carries the question so screen readers announce it
   with each option; .field is reused so the invalid state styles it for free. */
.choice{border:0;padding:0;margin:0 0 18px;min-width:0;}
.choice legend{
  padding:0;margin-bottom:8px;
  /* matches .field label -- these read as one set of form labels */
  font-size:var(--text-sm);font-weight:600;text-transform:capitalize;
  letter-spacing:normal;
  color:var(--color-text-muted);
}
.choice-set{display:flex;gap:10px;}
.choice-opt{
  display:inline-flex;align-items:center;gap:8px;cursor:pointer;
  padding:10px 20px;
  background:var(--field-bg);
  border:var(--border-width) solid var(--color-border-strong);
  border-radius:var(--radius-sm);
  font-size:var(--text-base);
  transition:border-color var(--dur-fast) var(--ease-out),
             background-color var(--dur-fast) var(--ease-out);
}
.choice-opt:hover{border-color:var(--color-text);}
.choice-opt input{accent-color:var(--color-text);margin:0;}
/* :has is safe to lean on here -- without it the control still works and simply
   does not tint, which is what the radio dot already communicates. */
.choice-opt:has(input:checked){border-color:var(--color-text);background:var(--color-surface);}
.choice-opt:has(input:focus-visible){box-shadow:0 0 0 3px var(--color-bg),0 0 0 4px var(--color-text);}
.field.is-bad legend{color:var(--color-accent);}
.field.is-bad .choice-opt{border-color:var(--color-accent);}

.quiz-turnstile{margin:4px 0 18px;min-height:65px;}
.quiz-turnstile:empty{min-height:0;}

.form-error{
  margin:0 0 16px;padding:12px 14px;
  font-size:var(--text-sm);line-height:var(--leading-body);color:var(--color-text);
  background:var(--color-surface-alt);
  border:var(--border-width) solid var(--color-accent);
  border-radius:var(--radius-sm);
}
.form-error a{color:inherit;}

.quiz-actions{display:flex;align-items:center;gap:14px;flex-wrap:wrap;}
.form-status{font-size:var(--text-sm);color:var(--color-text-muted);}
.btn[disabled]{opacity:.55;cursor:default;pointer-events:none;}
.quiz-fine{margin:16px 0 0;font-size:var(--text-xs);line-height:var(--leading-body);color:var(--color-text-muted);}

/* step 3 */
[data-step="done"] .btn{margin-top:8px;}

@media (max-width:767px){
  .field-row{grid-template-columns:1fr;gap:0;}
  .quiz-pick{padding:16px 18px;}
}
@media (prefers-reduced-motion:reduce){
  .menu-link,.menu-link::after{transition:none;}
  .menu-link:hover::after,.menu-link:focus-visible::after{animation:none;}
  .quiz-step{animation:none;}
  .quiz-pick:hover,.quiz-pick:focus-visible{transform:none;}
}

/* ---------- footer ---------- */
/* ---------- footer: dark plane ----------
   The semantic tokens are redefined on .footer itself, so every nested
   component (labels, links, hairlines, the badge fill) re-resolves against
   the dark ground with no per-element overrides. */
.footer{
  position:relative;
  border-top:var(--border);
  padding:72px 0 44px;
}
/* the page grain multiplies, which is invisible on a dark ground -- any dark
   scheme carries a screen-blended copy instead so the tooth still reads */
.scheme-dark{position:relative;}
.scheme-dark::after{
  content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
  background-image:var(--grain-img);
  background-size:var(--grain-tile);
  mix-blend-mode:screen;opacity:.06;
}
.scheme-dark > *{position:relative;z-index:2;}
.footer-grid{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));
  gap:40px;align-items:start;
}
/* Full lockup: mark and wordmark in one glyph, so it scales as a unit and
   inherits the footer's text colour rather than carrying a baked-in white. */
.footer-logo{
  display:block;width:min(268px,78%);height:auto;
  fill:var(--color-text);margin-bottom:16px;
}
.footer-badge{width:52px;height:52px;fill:var(--color-text);margin-bottom:14px;}
.footer-name{font-size:26px;margin:0 0 2px;line-height:1.1;}
.footer-brand p{margin:0;}
.footer-cell p{margin:0;font-size:15.5px;line-height:var(--leading-body);}
.footer-cell .lab{margin-bottom:10px;}
/* Footer link column. A nav rather than a paragraph so it can be skipped as a
   group, and stacked because four action links read as a list, not a sentence. */
.footer-links{display:flex;flex-direction:column;align-items:flex-start;gap:7px;margin-top:2px;}
.footer-links a{
  font-size:15.5px;line-height:var(--leading-body);
  color:var(--color-text);text-decoration:none;
  border-bottom:1px solid transparent;
  transition:color var(--dur-fast) var(--ease-out),border-color var(--dur-fast) var(--ease-out);
}
.footer-links a:hover,.footer-links a:focus-visible{
  color:var(--color-accent);border-bottom-color:currentColor;
}
.footer-social{margin-top:12px !important;display:flex;gap:14px;}
.footer-fine{margin-top:48px;padding-top:22px;border-top:var(--border);}
.footer-fine p{margin:0;font-size:13px;}


/* ---------- fixed underlay navigation ----------
   The panel does not animate in. It is permanently fixed to the right edge,
   underneath the page, and opening simply slides the page off it. Everything
   here exists to make that one transform read correctly. */
.page{
  position:relative;z-index:var(--z-page);
  /* must paint: any gap and the panel shows through the page */
  background:var(--color-bg);
  will-change:transform;
}

/* Slides with the page, draws only on top of it. */
.page-veil{
  position:fixed;inset:0 -1px 0 0;   /* -1px right kills a hairline seam */
  z-index:var(--z-veil);
  pointer-events:none;visibility:hidden;overflow:clip;
  will-change:transform;
}
.menu-open .page-veil{pointer-events:auto;cursor:pointer;}
.page-veil__dark{
  position:absolute;inset:0;opacity:0;
  background:rgba(26,36,28,.38);
}
/* An inverted radius: the quarter-disc nearest the page stays transparent and
   the rest is painted in the panel colour, so the sliding edge reads rounded
   without clipping the page itself. */
.page-veil__corner{
  position:absolute;right:0;
  width:var(--radius-lg);height:var(--radius-lg);
  transform:scale(0);
}
.page-veil__corner--top{
  top:0;transform-origin:100% 0;
  background-image:radial-gradient(circle farthest-side at 0 100%, transparent 98%, var(--pine) 100%);
}
.page-veil__corner--bottom{
  bottom:0;transform-origin:100% 100%;
  background-image:radial-gradient(circle farthest-side at 0 0, transparent 98%, var(--pine) 100%);
}

/* ---------- menu overlay ---------- */
.menu{
  position:fixed;inset:0 0 0 auto;width:var(--menu-width);
  z-index:var(--z-menu);overflow-y:auto;
  /* hidden, not display:none -- it has to be laid out so the page can uncover
     it, and visibility keeps it out of the focus order and the a11y tree */
  visibility:hidden;
}
.menu-inner{
  /* Desktop: links top, contact block stays pinned to the bottom where it
     already lived -- flex-start would drag it up under the links and leave a
     void beneath. Mobile keeps the whole stack at the bottom, by the thumb. */
  min-height:100%;display:flex;flex-direction:column;justify-content:space-between;
  padding:clamp(80px,10vh,112px) clamp(28px,4vw,48px) clamp(48px,7vh,72px);
}
.menu-nav{display:flex;flex-direction:column;align-items:flex-start;gap:var(--menu-gap);}
.menu-link{
  position:relative;
  font-size:var(--menu-size);line-height:1.08;color:var(--color-text);
  text-decoration:none;letter-spacing:var(--tracking-tight);
  text-wrap:balance;
  transition:color var(--dur-med) var(--ease-out);
}
.menu-link:hover{color:var(--color-accent);}
/* Hand-drawn underline. Painted as a solid bar masked by the squiggle rather
   than set as a background image, so it inherits currentColor and crossfades
   with the text instead of staying a fixed colour. It tiles on repeat-x, so it
   fits any link width without stretching the wave. It fades in place rather
   than drawing across, so the whole line appears at once. */
.menu-link::after{
  content:"";position:absolute;left:0;right:0;bottom:-.13em;
  /* The mask tiles at `auto 100%`, so one wavelength is the height times the
     art's aspect ratio (200/16 = 12.5). Travelling exactly that far per cycle
     lands the wave back where it started, so the loop never visibly jumps. */
  --wave-h:.21em;
  --wave-tile:calc(var(--wave-h) * 12.5);
  height:var(--wave-h);
  background:currentColor;pointer-events:none;
  -webkit-mask-image:var(--squiggle);mask-image:var(--squiggle);
  -webkit-mask-repeat:repeat-x;mask-repeat:repeat-x;
  -webkit-mask-size:auto 100%;mask-size:auto 100%;
  -webkit-mask-position:0 center;mask-position:0 center;
  opacity:0;
  /* The wave runs all the time rather than starting on hover, so letting go
     leaves it still moving while it fades. Speed is not set here: CSS cannot
     interpolate animation-duration, so switching it would snap to the new tempo
     instead of slowing. main.js ramps this animation's playbackRate instead,
     which changes speed without moving the playhead. With JS off it simply runs
     at full tempo throughout, which is the old behaviour and still fine. */
  animation:menu-wave 1s linear infinite;
  transition:opacity 1s var(--ease-out);
}
.menu-link:hover::after,.menu-link:focus-visible::after{
  opacity:1;
}
/* The mask moves, not the element: the bar has to stay pinned to the link's
   width, and only the pattern inside it should travel. */
@keyframes menu-wave{
  from{-webkit-mask-position:0 center;mask-position:0 center;}
  to{-webkit-mask-position:var(--wave-tile) center;mask-position:var(--wave-tile) center;}
}
.menu-foot{margin-top:28px;display:flex;flex-direction:column;gap:10px;}
.menu-foot p{margin:0;font-size:14.5px;line-height:var(--leading-body);}
.menu-social{display:flex;gap:14px;}

/* overlays sit above the page grain layer, so they carry their own */
.menu::after,.doc-panel::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background-image:var(--grain-img);
  background-size:var(--grain-tile);
  mix-blend-mode:multiply;opacity:var(--grain-opacity);
}

/* ---------- motion ----------
   Hidden/faded states are applied by JS only, so the page stays fully
   readable when JS fails or reduced-motion is on. */
.split-line{overflow:hidden;padding:.09em .12em .22em;margin:-.09em -.12em -.22em;}
.reveal-word{display:inline-block;will-change:opacity;}

@media (max-width:767px){
  .launch-grid{margin-bottom:28px;}
  .break{height:clamp(320px,52vh,560px);}
}

/* menu: bigger type, more air between links, right-aligned on phones.
   Padding is tuned so the whole column clears the fixed bottom nav pill
   rather than scrolling underneath it. */
@media (max-width:1023px){
  .menu-inner{
    justify-content:flex-end;
    padding:clamp(72px,11vh,104px) var(--gutter)
            calc(env(safe-area-inset-bottom,0px) + 104px);
  }
  .menu-nav{align-items:flex-end;text-align:right;align-self:stretch;gap:clamp(10px,1.9vh,20px);}
  /* Info above the links on a phone. The stack is bottom-anchored here, so
     putting the address and socials first leaves the links themselves nearest
     the thumb and the fixed pill. order on the flex column rather than moving
     the markup, so the desktop menu is untouched -- the gap moves with it,
     from above the block to below it. */
  .menu-foot{order:-1;text-align:right;align-items:flex-end;margin:0 0 32px;}
  .menu-social{justify-content:flex-end;}
}
/* short phones: give the links back some room by trimming the chrome */
@media (max-width:1023px) and (max-height:730px){
  .menu-inner{padding-top:64px;}
  .menu-foot{margin:0 0 20px;}
}

/* ---------- per-section photo ----------
   One image inside each section's column. Same grade and grain as the rails,
   so it reads as part of the same set rather than a stock inset. */
.sec-media{
  position:relative;margin:clamp(38px,5vw,58px) 0 0;
  /* Matches the cards rather than the other image frames: one value feeds the
     radius and the clip below, so the two cannot drift apart. */
  --media-radius:var(--radius-md);
  border-radius:var(--media-radius);overflow:hidden;
  /* overflow:hidden is not enough once a photo inside carries a filter. A
     blurred element paints outside its own box, the browser gives it its own
     composited layer, and it then clips that layer to a RECTANGLE -- the
     rounded part of the ancestor's clip is dropped. The corners go square for
     exactly as long as the blur is up, which is why they appeared to flick in
     and out in time with the animation.

     clip-path is applied when layers are composited rather than when the box
     is painted, so the rounding survives. overflow:hidden stays for the
     ordinary case and for anything that does not support clip-path. */
  clip-path:inset(0 round var(--media-radius));
  /* The grain below is mix-blend-mode:multiply, and blending needs its
     backdrop to be one compositing group. Neither position:relative without a
     z-index nor overflow:hidden makes a stacking context, so without this the
     grain blends against the root group -- and the moment a photo inside gains
     or loses a filter it becomes its own composited layer, the backdrop has to
     be regrouped, and the blend drops out for a frame. That is the blink.
     Isolating pins the group to this figure, where it belongs anyway. */
  isolation:isolate;
}
.sec-media img{
  /* height:auto is load-bearing. The width/height HTML attributes are
     presentational hints that set the CSS height, and a hint beats nothing --
     without this, height:1399px wins and aspect-ratio never gets to crop. */
  width:100%;height:auto;aspect-ratio:16/9;object-fit:cover;
  object-position:center;
  filter:var(--grade);display:block;
}
/* the kids frame is a tall shot: bias the crop upward so faces and the
   check-in sign stay in view instead of being cut at the top */
#kids .sec-media img{object-position:center 30%;}
#youth .sec-media img{object-position:center 20%;}
.sec-media::after{
  content:"";position:absolute;inset:0;pointer-events:none;
  background-image:var(--grain-img);
  background-size:var(--grain-tile);
  mix-blend-mode:multiply;opacity:var(--grain-opacity);
  /* Explicit, and above every photo in the figure. On z-index:auto the grain
     sits wherever paint order happens to put it, which is under anything
     inside carrying a z-index -- and the rotating photos do. That left the
     showing photo above the grain and the incoming one below it, so the
     moment the two swapped the new photo lost a multiply layer and the frame
     jumped lighter for an instant. */
  z-index:3;
}
@media (max-width:767px){
  .sec-media img{aspect-ratio:4/3;}
}

/* ---------- social icons ----------
   currentColor so each row inherits its context: accent teal on the light
   pages, the lighter accent inside the dark footer. */
.footer-social a,
.menu-social a,
.soon-social a{
  display:inline-flex;align-items:center;justify-content:center;
  width:44px;height:44px;margin:-11px;     /* 44px tap target (WCAG 2.5.5), no visual bulk */
  border-radius:var(--radius-pill);
  color:var(--color-accent);
  transition:color var(--dur-fast),transform var(--dur-fast) var(--ease-out);
}
.footer-social a:hover,
.menu-social a:hover,
.soon-social a:hover{
  color:var(--color-text);
  transform:translateY(-2px);
}
.social-ico{
  width:22px;height:22px;display:block;
  fill:currentColor;
}

/* ---------- squircle corners ----------
   `corner-shape` changes only the CURVE; `border-radius` still supplies the
   SIZE. So every existing radius declaration stays exactly as written and the
   fallback is automatic: engines without corner-shape render normal circular
   corners from the same tokens.

   Applied universally on purpose. corner-shape is inert wherever border-radius
   resolves to 0, so this costs nothing on square elements and means any
   component added later is squircled without touching this rule.

   Gate is the plain feature test rather than Calendly's extra
   `and (background: paint(x))` Chromium proxy, so Safari and Firefox pick it
   up as soon as they ship the property. */
@supports (corner-shape: squircle) {
  *, *::before, *::after { corner-shape: squircle; }

  /* Larger radius to land at the same apparent roundness as a circular corner,
     same ratio the tokens use. */
  .nav { --radius-nav:40px; }
}

@media (prefers-reduced-motion:reduce){
  .btn,.nav-toggle{transition:background var(--dur-btn),color var(--dur-btn);}
}

/* Lite and reduced-motion never build the timeline, so the states are declared
   here. Scoped to .lite so they can never fight GSAP's inline transforms. */
.lite.menu-open .menu,
.lite.menu-open .page-veil{visibility:visible;}
.lite.menu-open .page,
.lite.menu-open .page-veil{transform:translateX(calc(var(--menu-width) * -1));}
.lite.menu-open .page-veil__dark{opacity:1;}
.lite.menu-open .page-veil__corner{transform:scale(1);}

/* ---------- Lenis smooth scroll ----------
   The library's own stylesheet, inlined. .lenis-stopped is how stop() locks the
   page, which is what the menu and the modal use instead of overflow:hidden.
   data-lenis-prevent marks the two panels that own their own scrolling. */
html.lenis,html.lenis body{height:auto;}
.lenis:not(.lenis-autoToggle).lenis-stopped{overflow:clip;}
.lenis [data-lenis-prevent]{overscroll-behavior:contain;}
.lenis.lenis-smooth iframe{pointer-events:none;}

/* ---------- misc utilities ----------
   Both of these used to live in the flip-clock section, which was removed when
   the countdown came out. They are unrelated to it and still in use. */
/* menu footer address line: keep the phone/ pieces intact and the divider quiet */
.menu-foot .nb{white-space:nowrap;}
.menu-foot .sep{opacity:.55;}
/* the eyebrow above a section heading; .lab supplies the type, this the space */
.countdown{margin-bottom:22px;}
.sr-only{
  position:absolute;width:1px;height:1px;padding:0;margin:-1px;
  overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0;
}

/* ---------- rotating section photos ----------
   A .sec-media holding more than one <picture> cycles through them. The figure
   carries the aspect ratio so the stack has a box to live in, and every picture
   is absolutely positioned inside it -- swapping one for the next then cannot
   move the page a single pixel.

   Only the first picture is marked is-on in the markup, so with JS off, or
   before the script runs, exactly one photo shows and the rest stay put. */
.sec-media[data-rotate]{aspect-ratio:16/9;}
@media (max-width:767px){.sec-media[data-rotate]{aspect-ratio:4/3;}}

.sec-media[data-rotate] picture{
  position:absolute;inset:0;display:block;
  opacity:0;visibility:hidden;
  /* No z-index here on purpose: anything above the grain's stacking level
     hides it. isolation keeps each frame's own layering -- the blurred
     placeholder underneath, the photo on top -- contained, so the photo's
     z-index cannot escape into the figure and outrank the grain either. */
  isolation:isolate;
}
.sec-media[data-rotate] picture.is-on{
  opacity:1;visibility:visible;
}

/* ---------- moving between frames ----------
   The incoming frame rises in from the bottom while the one leaving slides
   out through the top, the two moving together as though the stack were being
   wound on. Upward on purpose: that is how the page moves under a thumb
   scrolling down, and travelling the other way fights the gesture. The figure
   already has overflow:hidden, which is what keeps the travelling frames off
   the page.

   transform rather than a mask: a mask sweep uncovers a stationary photo,
   which reads as a crop opening up rather than a move. The pixels have to
   travel for it to look like one.

   Both the opening reel and the single steps of the settled cycle run through
   the same script-driven loop, so while either is going every frame has to be
   visible and script owns transform and filter outright. A CSS transition
   would be cheaper for the single steps, but it cannot report how fast it is
   going, and the motion blur is a function of speed. */
.sec-media[data-rotate].is-moving picture{
  opacity:1;visibility:visible;transition:none;
}
/* the figure owns the ratio now, so the image just fills it */
.sec-media[data-rotate] img{
  width:100%;height:100%;aspect-ratio:auto;object-fit:cover;
}
/* The portrait check-in shot is biased upward by the #kids rule so faces stay
   in frame. A landscape source does not need that and loses feet to it. */
#kids .sec-media img.is-wide{object-position:center;}

/* Auto-advancing motion is the first thing to drop: the script does not start
   the timer at all here, and this keeps the swap instant if one ever happens. */
@media (prefers-reduced-motion:reduce){
  .sec-media[data-rotate] picture{transition:none;}
}

/* ---------- blur-up placeholders ----------
   Each rotating picture carries a 24px webp inlined in the HTML as a data URI:
   about 180 bytes, and no network request at all, so it paints with the first
   frame. Stretched to fill the frame it is inherently soft, which is the blur.
   The real photo then fades in over it, so the slot is never empty. */
.sec-media[data-rotate] picture{
  background-size:cover;background-position:center;background-repeat:no-repeat;
}
/* Opt-in, not opt-out. The fade only applies once the script has added
   .blurup, because the script is the only thing that ever adds .is-loaded.
   Defaulting the photo to transparent would leave it invisible forever
   wherever the script does not run -- no JS at all, and, less obviously,
   reduced-motion, where the rotation module returns before it starts. */
/* The blur lives on a layer BELOW the photo, carrying only the placeholder.
   backdrop-filter was tried here and is the wrong tool: it sits above the
   image and keeps a compositing layer over it even at blur(0), which
   re-rasterises the real photo and leaves it looking soft after it has
   loaded. A plain filter on a pseudo-element never touches the photo at all,
   and it leaves ::after free to stay exactly what it was -- the grain over
   the full-resolution image.

   background-image:inherit picks up the 274-byte data URI already on the
   <picture>, so the placeholder is not duplicated. The slight scale pushes
   the blur's feathered edge outside the frame, which would otherwise show as
   a pale halo inside the rounded corners. */
.sec-media[data-rotate].blurup picture::before{
  content:"";position:absolute;inset:0;z-index:0;
  background-image:inherit;
  background-size:cover;background-position:center;
  filter:blur(2rem);
  transform:scale(1.08);
  transition:opacity var(--dur-med) var(--ease-out);
}
.sec-media[data-rotate].blurup picture img{
  position:relative;z-index:1;
  opacity:0;
  transition:opacity var(--dur-med) var(--ease-out);
}
/* the photo has arrived: fade it in and retire the blurred stand-in */
.sec-media[data-rotate].blurup picture.is-loaded img{opacity:1;}
.sec-media[data-rotate].blurup picture.is-loaded::before{opacity:0;}


