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

@import url("tokens.170642e14c.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. */
.nav{
  /* The mobile pill was border-radius:999px, which on a 64px-tall bar is past
     half the height -- the point where a radius stops being a corner and the
     shape becomes a lozenge. These are deliberately under that line so it reads
     as a rounded bar in every engine. */
  --radius-nav:15px;
  position:fixed;top:0;left:0;right:0;z-index:var(--z-nav);
  display:flex;align-items:center;justify-content:space-between;
  gap:var(--gap-md);
  padding:14px var(--gutter);
  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-bottom:var(--border);
  transition:background-color var(--dur-med),border-color var(--dur-med),
             -webkit-backdrop-filter var(--dur-med),backdrop-filter var(--dur-med);
}

/* 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-bottom-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{
  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(--radius-btn);cursor:pointer;
  transition:transform var(--dur-fast) var(--ease-out),
             border-radius var(--dur-btn) var(--ease-btn);
}
.nav-toggle:hover{
  border-radius:var(--radius-btn-hover);
}
/* The lift is desktop-pointer only. On a touchscreen :hover latches after a tap
   and does not clear until you touch something else, so the pill would sit
   raised long after the press -- and the pill is the mobile control, where a
   2px hop reads as a glitch rather than as feedback. `hover: hover` excludes
   touch; the width bound keeps it off the pill layout entirely. */
@media (hover: hover) and (pointer: fine) and (min-width:1024px){
  .nav-toggle:hover{transform:translateY(-2px);}
}
.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;
    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;}
}

/* ---------- hero ---------- */
.hero{
  position:relative;min-height:100svh;
  display:flex;align-items:center;justify-content:center;
  overflow:hidden;
}
.hero-inner{
  position:relative;z-index:2;text-align:center;
  padding:90px var(--gutter) 110px;max-width:900px;
}
.hero-script{
  font-size:clamp(28px,4.6vw,44px);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 rails ----------
   Desktop: two vertical columns flanking the type, drifting opposite ways.
   Mobile: the same tracks flip to horizontal rows above and below the type. */
.rail{
  position:absolute;top:0;bottom:0;width:clamp(168px,19vw,290px);
  overflow:hidden;z-index:var(--z-rail);pointer-events:none;
  padding:0 var(--rail-inset);
}
/* The fade is a mask, not a panel of background colour laid over the photos.
   An overlay only vanishes where it happens to match whatever is behind it, so
   it has to be re-tuned for every background it crosses and gives itself away
   the moment one of them changes. A mask takes the pixels out, so the rail
   fades to whatever is actually there.

   Stops mirror the alphas the old overlay used, inverted: it covered the photo
   by 0 / .18 / .72 / 1, so the photo survives at 1 / .82 / .28 / 0. */
.rail-left{
  left:0;
  -webkit-mask-image:linear-gradient(90deg,#000 0%,rgba(0,0,0,.82) 42%,rgba(0,0,0,.28) 80%,transparent 100%);
          mask-image:linear-gradient(90deg,#000 0%,rgba(0,0,0,.82) 42%,rgba(0,0,0,.28) 80%,transparent 100%);
}
.rail-right{
  right:0;
  -webkit-mask-image:linear-gradient(270deg,#000 0%,rgba(0,0,0,.82) 42%,rgba(0,0,0,.28) 80%,transparent 100%);
          mask-image:linear-gradient(270deg,#000 0%,rgba(0,0,0,.82) 42%,rgba(0,0,0,.28) 80%,transparent 100%);
}
.rail-track{
  display:flex;flex-direction:column;gap:var(--gap-sm);will-change:transform;
}
/* The column itself stays pointer-transparent -- it runs the full height of the
   hero and would otherwise swallow clicks meant for the type behind it. Only
   the photos take the pointer, which is also exactly the surface someone
   reaches for when they want the drift to stop. Events still bubble up to
   .rail, so the pause listeners sit on the column, not on every photo. */
/* A real button, so Enter and Space open the photo without reimplementing
   either. Everything below is the browser's button chrome being removed --
   the photo is the control. */
.rail-item{
  flex:0 0 auto;position:relative;pointer-events:auto;cursor:zoom-in;
  appearance:none;-webkit-appearance:none;
  background:none;border:0;padding:0;margin:0;
  font:inherit;color:inherit;display:block;text-align:inherit;
}
/* The rails sit behind a gradient that fades them into the page edge, so the
   default 2px ring can land on the faded side and read as barely there. A
   thicker offset ring keeps the focused photo obvious against the photograph
   underneath it. */
.rail-item:focus-visible{outline:3px solid var(--color-accent);outline-offset:-3px;border-radius:2px;}
/* grain bound to the photo, so it travels with the image instead of the
   page grain sliding across a moving picture */
.rail-item::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);
  border-radius:var(--radius-lg);
}
/* native ratio: no aspect-ratio, no cover-crop. The column takes the width
   and each photo sets its own height, so nothing is cropped or clipped. */
.rail-item img{
  width:100%;height:auto;
  filter:var(--grade);display:block;
  border-radius:var(--radius-lg);
}

@media (max-width:1023px){
  /* Height comes from the content on mobile, not the viewport. With
     min-height gone there is no free space left for justify-content to
     distribute, so the padding is what sets the breathing room. */
  .hero{
    flex-direction:column;gap:clamp(18px,4vh,34px);
    min-height:auto;padding-block:5svh;
  }
  .rail{
    position:relative;top:auto;bottom:auto;left:auto;right:auto;
    width:100%;height:clamp(112px,17vh,168px);
    padding:0;
  }
  .rail-track{flex-direction:row;gap:var(--gap-sm);height:100%;}
  .rail-item{height:100%;}
  .rail-item img{height:100%;width:auto;}
  .rail-left{order:1;}
  .hero-inner{order:2;padding-top:0;padding-bottom:0;}
  .rail-right{order:3;}
  /* Rows now, not columns: feather both ends rather than one inner edge. */
  .rail-left,.rail-right{
    -webkit-mask-image:linear-gradient(90deg,transparent 0%,#000 14%,#000 86%,transparent 100%);
            mask-image:linear-gradient(90deg,transparent 0%,#000 14%,#000 86%,transparent 100%);
  }
  .hero-cue{display:none;}
}

/* ---------- 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;
}
/* City Groups come from Planning Center, so the count is whatever the church
   has published that week. The shared `.cards` grid is auto-fit, which leaves a
   bare cell whenever the count does not divide evenly -- and because the hairline
   rules are drawn by the container's background showing through the gaps, that
   empty cell reads as a grey block rather than as nothing.

   Column counts are stated outright at the three site breakpoints rather than
   left to auto-fit. Auto-fit was the original trap: it silently moves to two
   columns near 520px, so any breakpoint chosen above that left a band where two
   columns rendered while the fill rule was still off. */
#groups-live{grid-template-columns:1fr;}

/* Tablet -- two columns. Bounded above so the desktop rules below are not
   fighting a `1 / -1` inherited from a min-width query. */
@media (min-width:768px) and (max-width:1023px){
  #groups-live{grid-template-columns:1fr 1fr;}
  #groups-live > .card:last-child:nth-child(odd){grid-column:1 / -1;}
}

/* Desktop -- three columns, over six tracks rather than three. Three columns
   cannot split two cards evenly, so a row holding the last two of five groups
   would either sit lopsided or leave a hole. On six tracks each card spans two
   (three per row), a lone leftover spans all six, and a leftover pair spans
   three each -- so any count fills its final row. */
@media (min-width:1024px){
  #groups-live{grid-template-columns:repeat(6,1fr);}
  #groups-live > .card{grid-column:span 2;}
  /* one card over: it takes the whole row */
  #groups-live > .card:last-child:nth-child(3n+1){grid-column:span 6;}
  /* two cards over: they take half each */
  #groups-live > .card:nth-child(3n+1):nth-last-child(2),
  #groups-live > .card:nth-child(3n+1):nth-last-child(2) ~ .card{grid-column:span 3;}
}

.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;}

/* ---------- 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;}
}

/* ---------- 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) + 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) + 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{--squiggle:url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 16'><path d='M0%2C9.0 C14%2C3.4 26%2C2.2 42%2C7.4 C56%2C12.0 66%2C14.6 82%2C10.0 C98%2C5.4 108%2C2.8 124%2C6.6 C138%2C9.9 148%2C14.2 164%2C11.0 C178%2C8.2 188%2C5.6 200%2C9.0' fill='none' stroke='%23000' stroke-width='3.1' stroke-linecap='round' stroke-linejoin='round'/></svg>");}
.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. Still under half the bar's height, so the
     squircle renders as a corner rather than collapsing the shape. */
  .nav { --radius-nav:26px; }
}

@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;
}

/* ---------- photo lightbox ----------
   Deliberately not the usual black sheet. The scrim is the site's own pine
   carrying the page grain, and the photo sits in a paper frame with the same
   radius, grade and grain the section images use -- so an opened photo reads
   as part of this site rather than as a viewer bolted onto it. */
.lb{
  position:fixed;inset:0;z-index:var(--z-doc);
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:clamp(14px,2.4vh,22px);
  padding:clamp(16px,4vw,48px);
  /* published so the script can fold them into min() alongside each photo's
     natural size, instead of one cap fighting the other */
  --lb-vw:92vw;--lb-vh:74vh;
  background:rgba(var(--ink-rgb),.92);          /* fallback */
  /* Blur rather than a heavier wash. At an opacity high enough to suppress the
     rails on its own the scrim goes flat and the photo looks pasted onto black;
     blurring what is behind pushes it back while the pine still reads as this
     site's dark, not a generic overlay. Same technique the nav already uses. */
  backdrop-filter:blur(14px) saturate(.9);
  -webkit-backdrop-filter:blur(14px) saturate(.9);
}
@supports (color: color-mix(in srgb, red, blue)){
  .lb{background:color-mix(in srgb, var(--pine) 86%, transparent);}
}
/* No blur available: lean on opacity instead so the rails still recede. */
@supports not ((backdrop-filter:blur(1px)) or (-webkit-backdrop-filter:blur(1px))){
  .lb{background:rgba(var(--ink-rgb),.96);}
}
/* the same grain the page carries, so the scrim is not a flat wash */
.lb::before{
  content:"";position:absolute;inset:0;pointer-events:none;
  background-image:var(--grain-img);background-size:var(--grain-tile);
  opacity:.12;mix-blend-mode:overlay;
}

.lb-figure{
  position:relative;margin:0;line-height:0;min-height:0;
  border-radius:var(--radius-lg);overflow:hidden;
  background:var(--color-surface);box-shadow:var(--shadow-lift);
}
.lb-img{
  display:block;width:auto;height:auto;
  /* The rails are the only source and they are 426x640, so the ceiling is set
     from each photo's own natural size by the script. Without that cap a big
     display would stretch a small JPEG into mush. */
  max-width:var(--lb-vw);max-height:var(--lb-vh);
  filter:var(--grade);
}
.lb-figure::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);
}

.lb-controls{
  position:relative;z-index:1;
  display:flex;align-items:center;gap:clamp(12px,2vw,20px);
}
.lb-btn{
  position:relative;z-index:1;flex:0 0 auto;
  width:46px;height:46px;padding:0;cursor:pointer;
  display:inline-flex;align-items:center;justify-content:center;
  background:var(--color-surface);color:var(--color-text);
  border:var(--border);border-radius:var(--radius-pill);
  transition:transform var(--dur-fast) var(--ease-out),
             background var(--dur-fast);
}
.lb-btn:hover{transform:scale(1.07);}
.lb-btn:active{transform:scale(.96);}
.lb-btn svg{
  width:19px;height:19px;
  fill:none;stroke:currentColor;stroke-width:2;
  stroke-linecap:round;stroke-linejoin:round;
}
.lb-close{position:absolute;top:clamp(14px,3vw,30px);right:clamp(14px,3vw,30px);}

/* Reads on the dark scrim, where --color-text-muted would not. */
.lb-count{
  margin:0;min-width:6ch;text-align:center;
  font-family:var(--font-body);font-size:var(--text-xs);
  letter-spacing:var(--tracking-label);text-transform:uppercase;
  color:var(--sage);
}

@media (max-width:767px){
  .lb{--lb-vh:64vh;}
  .lb-btn{width:42px;height:42px;}
}
@media (prefers-reduced-motion:reduce){
  .lb-btn{transition:none;}
  .lb-btn:hover,.lb-btn:active{transform:none;}
}

/* ---------- 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;}


