@charset "UTF-8";
/* =========================================================================
   TABERNACLE  --  Hubb Component Library
   =========================================================================
   A reusable styled-module library for hubb.church sites. The name is
   Exodus's portable dwelling: a precisely specified pattern of
   standardised panels, frames and curtains, set up identically wherever
   it's deployed. Each church gets its own colour and font tokens but
   inherits the same structure -- the design pattern persists, the
   surface adapts.

   Currently in VANILLA state -- neutral white/grey/black palette with
   Inter for typography. Re-skin via the token block in section 1.1
   (no module rules need to change). See HOW TO RETHEME below.

   The Tabernacle deliberately maintains as a single CSS file. When
   a real church deployment needs branded styling, re-skinning is a
   30-line token-block edit performed at deployment time -- not a
   permanent parallel file to keep in sync.

   Version:    v0.278
   History:    See CHANGELOG.md alongside this file. The changelog
               used to live in this file header but was moved out at
               v0.135 to keep the CSS lean and stop shipping ~5KB of
               historical narrative to every browser. Inline comments
               throughout the file still record WHY specific rules
               look the way they do, where the rationale is most
               useful next to the code.
   =========================================================================

   -------------------------------------------------------------------------
   Load this on every page that uses the "Vivid" template, via the
   Head Contents. Module styles live in clearly labelled sections below.

   !! CRITICAL: The Head Contents must include this viewport meta tag, or
   mobile browsers render the page at desktop width and just zoom out
   (making everything look tiny):

     <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">

   TABLE OF CONTENTS
   -----------------
   1. Foundation
        1.1  Design Tokens
        1.2  Reset
        1.3  Base Typography
        1.4  Layout Wrapper  .hubb-wrap
        1.5  Page Load & Reveal Animations

   2. Site Header Layout     .site-header (three rows)

   3. Modules
        3.1  Site Logo        .site-logo
        3.2  Site Name        .site-name
        3.3  Status Bar       .statusbar
        3.4  Search           .site-search
        3.5  Drop-down Menus  .main-nav
        3.6  Articles List    .al-cards
        3.7  Hero Slideshow   .hero_slideshow  (Gallery module)
        3.8  Mobile Menu      .mobile-menu-toggle  (hamburger + panel)
        3.9  Welcome Article  .welcome_article  (Article module)
        3.10  Upcoming Events    .upcoming_events    (4-col card grid)
        3.10b Latest Media       .latest_media       (4-col card grid)
        3.10c Upcoming Events 2  .upcoming_events_2  (horizontal carousel)
        3.10d Latest Media 2     .latest_media_2     (horizontal carousel)
        3.10e Latest Media 3     .latest_media_3     (2x2 grid, narrow)
        3.11 Site Footer      .site-footer  (Email + Phone + Footer bar)
        3.24 Content Article  .content_article  (internal page body)
        3.25 Shop Homepage    .shop-utility strip + .shop-layout
                              (sidebar .shop-cat-nav | content:
                              .shop_featured_categories, .shop-cards)

   4. Mobile Responsiveness  @media (max-width: 1400px)  hamburger menu
                             @media (max-width: 1024px)  Article List tablet collapse (.al-cards only)
                             @media (max-width: 720px)   phone-only tweaks
                             @media (max-width: 540px)   single-column cards

   SLICE CATALOGUE  (added v0.130)
   -------------------------------
   Every content block inside <main> is a "slice" -- a <section> with
   the .slice base class and a .slice--name modifier. Authors should
   use the canonical patterns documented in BODY-CONTENTS.html rather
   than inventing markup. Current slice types:

     .slice--hero            Hero slideshow (Gallery module, full bleed)
     .slice--welcome         Welcome article (Article module)
     .slice--where-when      Where & When info card (Article module)
     .slice--announcement    Slim notice bar (Article module, optional)
     .slice--feature         Image-backed feature panel (Image + Article)
     .slice--explore-church  Explore Our Church gallery (Gallery module)
     .slice--upcoming-events Upcoming Events grid or carousel (Events)
     .slice--latest-media    Latest Sermons grid or carousel (Media)
     .slice--latest-news     Latest News grid (Articles List)
     .slice--testimonials    Testimonial slideshow (Blog module)
     .slice--partner-logos   Footer partner logo strip (Gallery, in <footer>)
     .slice--shop-utility    Shop toolbar: search + basket strip
     .shop-layout            Shop two-column grid: sidebar + content
       .shop-sidebar         Boxed category menu (Category nav module)
       .shop-content         Featured categories (Gallery), featured
                             products (Hotpicks -> .shop-cards), and
                             .slice--shop-info article

   Inner head layouts (optional, opt-in via class):
     .slice__head--centred   Centred H2 + accent rule (default for grids)
     .slice__head--intro     Left H2 + tagline + CTA (carousel variants)

   HOW TO BUILD A NEW SLICE
   ------------------------
   When adding a new module type to the system (e.g. testimonials):

     1. Pick a slice name. Kebab-case, descriptive, plural if it's
        a collection (.slice--testimonials, .slice--staff-bios).
     2. Decide which inner head layout fits, or none.
     3. Add the slice's CSS rules in a numbered section below
        (sections 3.x), reading from existing tokens for all
        colours, fonts, and spacing.
     4. Add the canonical HTML pattern to BODY-CONTENTS.html under
        a new "Slice: ..." heading.
     5. Add the slice name to the catalogue list above.

   HOW TO RETHEME FOR A CHURCH
   ---------------------------
   The token block in :root (section 1.1) is the only thing you should
   need to edit to re-skin Tabernacle for a new church. Module rules
   below all read from these tokens and do not contain hardcoded
   colours, fonts, or sizes.

   Standard re-skin checklist:
     1. Replace --bg, --surface, --ink, --ink-soft with the church's
        page palette. Verify --ink on --bg gives at least 4.5:1
        contrast (use webaim.org/resources/contrastchecker).
     2. Replace --accent / --accent-dark with the brand colours.
        --accent is used for button BACKGROUNDS and decorative bars
        (3:1 contrast minimum is fine for these). --accent-dark MUST
        give 4.5:1+ against --bg because it's used as a TEXT colour
        (e.g. the date labels in Latest Sermons cards).
     3. Update --bg-rgb to match --bg (rgb components, no #).
     4. Pick fonts. Replace --font-display and --font-body, AND
        update the Google Fonts <link> in Head Contents to load them.
        Two distinct families (display serif + body sans) reads as
        "designed"; one family for both reads as "default".
     5. Optional: tweak --radius (4px = sharp, 8-12px = soft,
        999px = full pill -- but pills are already used for CTAs
        regardless of this token).

   Reference deployment recipes (record the token values used; the
   files themselves are produced at deployment time, not maintained
   as parallel files):
     - Town Church  (cream + terracotta + Fraunces / Manrope)
         --bg          #FAF7F2     --accent       #B85C3A
         --ink         #1A1612     --accent-dark  #963F23
         --ink-soft    #57524B     --accent-soft  #E8D5C4
         --rule        #E5DED4     --bg-rgb       250, 247, 242
         --font-display "Fraunces", Georgia, serif
         --font-body    "Manrope", -apple-system, sans-serif
     - [Future churches: record their token values here as deployed.]
   ========================================================================= */


/* =========================================================================
   !IMPORTANT USAGE POLICY (v0.159)
   -------------------------------------------------------------------------
   This file uses ~228 `!important` declarations. To an auditor scanning
   for code smell, that number is alarming. To a reader who knows the
   environment, every one is defensible. This block exists so the
   auditor can become the second kind of reader without having to
   reverse-engineer the rationale.

   Hubb is a multi-tenant ASP.NET Web Forms platform. We do not
   control:

   1. Hubb's base.min.css, which ships with the platform and applies
      rules at specificity 0,1,1 / 0,2,1 / 0,2,2 / 0,4,1 to module
      output. Some of those rules disagree with our design system.
   2. Hubb's runtime JavaScript, which writes inline `style="..."`
      attributes on its own modules (most aggressively on the jQuery
      UI dialog widget, the gallery slider, and the navigation
      dropdowns). Inline style beats any external rule of the same
      specificity, so the only way to override is `!important`.
   3. jQuery UI's dialog CSS, included by Hubb when any popup is
      rendered. It uses inline-styled coordinates calculated at
      runtime that fight our pinned-panel layout if not overridden.

   Legitimate categories of use in this file:

   A. **Beat Hubb's base.min.css.** The bulk. Always paired with a
      preceding comment naming the rule we're beating and the
      specificity. Bare reads like `font-size: inherit !important`
      override `div.media-player { font-size: 16px }` (0,1,1).
   B. **Beat inline `style="..."` written at runtime.** The jQuery
      UI dialog block (Section 4.3) is the canonical example -- jQ
      writes `style="width: 540px; height: 600px; top: 102px;
      left: 393px;..."` on every render, and our pinned-panel layout
      can only assert itself with `!important` on every position
      and dimension property.
   C. **State toggles that must never lose.** `.HideElement
      { display: none !important }`, `.menu-open` body lock,
      `.is-loaded` reveal -- these gate user-visible state and a
      cascade defeat would mean a broken interface, not a styling
      glitch.
   D. **Accessibility primitives.** `.sr-only`'s clipping rules
      use `!important` because Hubb's auto-generated forms
      occasionally apply visibility that would expose the screen-
      reader-only content.
   E. **Defensive scroll/overflow restoration.** `html { overflow-y:
      auto !important }` is the strongest layer of defence against
      Hubb's modal scroll-lock leak (documented in the dialog
      section). Without `!important`, the bug returns the moment
      Hubb's cleanup misfires.

   What this file does NOT do:

   * Use `!important` to win specificity battles within OUR own CSS.
     If our rule isn't winning against another of our rules, the
     fix is the selector, not `!important`. Audit any `!important`
     whose comment doesn't reference Hubb / jQuery / inline style /
     state / a11y -- it's probably legitimate but worth a sanity
     check.
   * Use `!important` to short-cut writing the right selector. Every
     instance was added against a documented adversary.

   Short rationale comments (in standard CSS comment syntax) may be
   added inline next to individual `!important` declarations, but only
   when the reason is non-obvious from the section context. Rules that
   sit inside a section whose master comment explains the policy
   (e.g. "Use !important for everything that needs to beat jQuery UI's
   CSS") inherit that explanation; re-stating it on every line would
   be noise.

   Note: do NOT write a literal CSS comment example anywhere inside a
   bigger comment block -- the inner close-marker terminates the outer
   block and silently breaks the entire stylesheet from that point on.
   (Hard-won lesson, v0.159 -> v0.160.)
   ========================================================================= */


/* =========================================================================
   1. FOUNDATION
   ========================================================================= */

/* -------------------------------------------------------------------------
   1.1  DESIGN TOKENS
   ------------------------------------------------------------------------- */
:root {

  /* ---- Colours ----
     Vanilla neutral palette. Override these tokens (without touching
     module rules) to re-skin the template for any new church. Every
     contrast pair below passes WCAG AA comfortably -- white text on
     --accent gives 13.8:1, --ink on --bg gives 17.4:1, --accent-dark
     on --bg gives 21:1. */
  --bg:          #FFFFFF;  /* Page background */
  --bg-rgb:      255, 255, 255;
                           /* Same as --bg, as rgb components for use inside
                              rgba() -- e.g. fade gradients that need to fade
                              alpha without drifting towards black.
                              KEEP IN SYNC WITH --bg WHEN RETHEMING. */
  --surface:     #FFFFFF;  /* Card / panel backgrounds */
  --ink:         #1A1A1A;  /* Primary text */
  --ink-soft:    #5A5A5A;  /* Secondary text, meta info */
  --accent:      #2D2D2D;  /* Primary accent -- buttons, decorative use only.
                              In branded themes this is where the church's
                              brand colour goes. WCAG note for re-skinning:
                              if you replace this with a low-contrast colour
                              (e.g. mid-tone blue/orange/green), check that
                              --accent-dark below gives at least 4.5:1 against
                              --bg before using accent for any text. */
  --accent-ink:  #FFFFFF;  /* Text colour on top of accent */
  --accent-soft: #F0F0F0;  /* Muted accent tint -- card backgrounds, hover */
  --accent-dark: #000000;  /* Hover state for accent buttons, AND the correct
                              text colour for accent-toned text on cream/white
                              (when accent itself is too light to pass AA). */
  --rule:        #E0E0E0;  /* Hairline borders */
  --section-light: #F4F3F1;  /* "light-grey" section band -- a very light grey */
  --section-light-rgb: 244, 243, 241;  /* rgb of --section-light, for edge-fades painted over the band */
  --section-dark:  #262626;  /* "dark-grey" section band */
  --section-pad-block: var(--space-16);  /* vertical breathing room inside a themed band */

  /* ---- Typography ----
     Vanilla uses Inter for both display and body -- a single neutral
     sans-serif that reads as "system default" rather than "design
     opinion". Inter is also the default for Vercel, Linear, GitHub
     etc., so it's the safest "I haven't picked a font yet" choice.
     For branded themes, replace these with two distinct families
     (typically a display serif for headings + a body sans-serif).
     The Google Fonts <link> in Head Contents must be updated to
     match -- it currently loads:
       Inter:wght@400;500;600;700
     Town Church's branded version loaded:
       Fraunces:opsz,wght@9..144,400;9..144,500;9..144,700
       + Manrope:wght@400;500;700  */
  --font-display: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
  --font-body:    "Inter", -apple-system, BlinkMacSystemFont, sans-serif;

  --text-xs:    clamp(0.75rem,  0.7vw + 0.60rem, 0.825rem);
  --text-sm:    clamp(0.875rem, 0.8vw + 0.70rem, 0.950rem);
  --text-base:  clamp(1.00rem,  0.9vw + 0.80rem, 1.125rem);
  --text-lg:    clamp(1.125rem, 1.0vw + 0.90rem, 1.375rem);
  --text-xl:    clamp(1.50rem,  1.8vw + 1.00rem, 2.000rem);
  --text-2xl:   clamp(2.00rem,  3.0vw + 1.00rem, 3.000rem);
  --text-3xl:   clamp(2.50rem,  5.0vw + 1.00rem, 4.500rem);

  /* ---- Spacing (4px grid) ---- */
  --space-1:  0.25rem;
  --space-2:  0.50rem;
  --space-3:  0.75rem;
  --space-4:  1.00rem;
  --space-5:  1.25rem;     /* 20px -- added because two existing rules
                              referenced var(--space-5) without it ever
                              being defined; both fell back to invalid
                              CSS (carousel gap collapsed to 0, dialog
                              padding fell back to a hardcoded 20px). */
  --space-6:  1.50rem;
  --space-8:  2.00rem;
  --space-10: 2.50rem;     /* 40px -- added v0.277 for exactly the reason
                              --space-5 was: .content_article set its h2/h3/h4
                              and hr margins from var(--space-10), which was
                              never defined, so the whole shorthand was
                              invalid at computed-value time and margin
                              resolved to 0. Every content heading on every
                              site had no margin above or below. */
  --space-12: 3.00rem;
  --space-16: 4.00rem;
  --space-24: 6.00rem;

  /* ---- Layout ---- */
  --container-width:  1600px;
  --container-gutter: clamp(1rem, 4vw, 2.5rem);

  /* ---- Other ---- */
  --radius:     4px;
  --radius-lg:  12px;
  --transition: 200ms ease;

  /* ---- Animation tokens ---- */
  /* Soft easing curve for reveals -- gentle deceleration, no bounce.
     This is the "expo out" curve, ideal for content fade-ups. */
  --ease-out-soft:    cubic-bezier(0.16, 1, 0.3, 1);
  --reveal-distance:  24px;     /* how far elements travel during reveal */
  --reveal-duration:  1100ms;   /* how long each reveal takes -- soft & slow */
  --reveal-stagger:   450ms;    /* delay between staggered children -- JS reads this */
  --page-fade:        500ms;    /* initial body fade-in duration */
}


/* -------------------------------------------------------------------------
   1.2  RESET
   ------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; }
html, body { height: 100%; }

/* Horizontal-overflow guard. Several full-bleed sections (the section-theme
   bands, the where_when_2 strip, the parallax feature) span 100vw so their
   backgrounds reach the viewport edges. 100vw INCLUDES the vertical scrollbar
   width, so each ends up a scrollbar-width wider than the real content area --
   enough to trigger a faint horizontal scrollbar. Clipping <main> on the x-axis
   trims that excess. clip (NOT hidden) is deliberate: clip leaves overflow-y
   visible and creates no scroll container, so the sticky header (outside <main>)
   and vertical scroll/reveal transforms are untouched. */
main { overflow-x: clip; }
body { -webkit-font-smoothing: antialiased; line-height: 1.5; }
/* When width/height attributes are present (set by reserveImageSpace
   for CLS prevention), browsers compute aspect-ratio from them. Our
   explicit `height: auto` override means images still scale fluidly
   to their container width while the attributes provide the
   pre-load aspect-ratio hint. Best of both: zero layout shift AND
   responsive image sizing. */
img, picture, video, canvas, svg { display: block; max-width: 100%; height: auto; }
input, button, textarea, select { font: inherit; }
p, h1, h2, h3, h4, h5, h6 { overflow-wrap: break-word; }

/* ---- Content-visibility: skip rendering off-screen sections ----
   Tells the browser it's safe to skip layout, paint, and style
   recalc for the section until it's about to be scrolled into
   view. The `contain-intrinsic-size` provides a reasonable
   placeholder height so scrollbar sizing and skip-link offsets
   stay correct.

   Big win on long pages and weak devices: typical first-paint
   work drops by 30-50% because below-the-fold sections aren't
   processed until needed. Supported in Chrome/Edge 85+, Safari 18+,
   Firefox 125+. Older browsers ignore both properties (no harm).

   Excluded: sections that are likely to be above the fold (hero,
   welcome article, first feed module) so they paint normally
   without the skip overhead. Also excluded: footer (small enough
   that the skip cost outweighs the gain). */
.upcoming-events,
.latest-media,
.feature,
.explore-church,
.footer-safeguarding,
.umbrella_org_logo {
  content-visibility:     auto;
  /* Reserved height fallback per section. Chosen as plausible
     averages -- doesn't have to be exact, just close enough to
     stop scrollbar from jumping when sections become visible.
     Browsers correct to actual size on render. */
  contain-intrinsic-size: auto 600px;
}


/* -------------------------------------------------------------------------
   1.3  BASE TYPOGRAPHY
   ------------------------------------------------------------------------- */
body {
  font-family: var(--font-body);
  font-size:   var(--text-base);
  color:       var(--ink);
  background:  var(--bg);
}

h1, h2, h3, h4, h5, h6 {
  font-family:     var(--font-display);
  font-weight:     500;
  line-height:     1.1;
  letter-spacing: -0.02em;
  color:           var(--ink);
  font-variation-settings: "SOFT" 30, "WONK" 0;
}

h1 { font-size: var(--text-3xl); }
h2 { font-size: var(--text-2xl); }
h3 { font-size: var(--text-xl);  }
h4 { font-size: var(--text-lg);  }

p + p { margin-top: var(--space-4); }

a {
  color:                     var(--accent);
  text-decoration:           underline;
  text-decoration-thickness: 1px;
  text-underline-offset:     3px;
  transition:                opacity var(--transition);
}
a:hover { opacity: 0.7; }


/* -------------------------------------------------------------------------
   1.3b  TOUCH / HOVER HANDLING  --  philosophy
   -------------------------------------------------------------------------
   This file uses a hybrid approach to interactive states:

   * Plain color/opacity hover effects (e.g. links going from accent to
     ink-soft) live OUTSIDE any media query. They look fine even when
     "stuck" after a tap on touch devices because the change is subtle.

   * Transform/scale-based hover effects (image zoom on cards, arrow
     shifts, underline animations) live INSIDE @media (hover: hover) so
     they only fire on devices with a real mouse cursor. On touch,
     these would otherwise stay stuck after a tap until the user taps
     elsewhere -- the classic "sticky hover" UX bug.

   * Primary CTAs (.feature__cta, .hero-caption__cta, .ArrowToRight)
     have :active states that fire on touch -- a subtle background
     darken or scale-down gives tap feedback that hover never can.

   * iOS/Android tap-highlight flash is suppressed globally below.
     We replace it with our own :active states where it matters.

   When adding new hover rules, follow this pattern:
     1. If it's just a color shift -> no media query needed.
     2. If it involves transform / scale / animation -> wrap in
        @media (hover: hover) { ... }
     3. For tappable buttons -> add an :active state too.
   ------------------------------------------------------------------------- */

/* Suppress the default iOS/Android grey/blue tap flash on every
   interactive element. Our :active states (where defined) provide
   intentional touch feedback instead. */
a, button, [role="button"], input[type="submit"], input[type="button"] {
  -webkit-tap-highlight-color: transparent;
}


/* -------------------------------------------------------------------------
   1.3b  TOUCH FOUNDATION  (Pass A)
   -------------------------------------------------------------------------
   Best-practice touch behaviour for iOS, Android, and pen-enabled
   devices. Each rule below addresses a documented platform quirk.
   Comments explain WHY each rule exists; do not remove without
   reading them.
   ------------------------------------------------------------------------- */

/* (1) Eliminate iOS Safari's 300ms tap delay on every interactive
   element. Pre-iOS 9.3, Safari waited 300ms after touch-end to see
   if the user was double-tapping (zoom gesture). Modern Safari
   removes the delay if the page declares a viewport or touch-action.
   `touch-action: manipulation` keeps single-tap and pan/zoom but
   removes the legacy double-tap-to-zoom shortcut on the element
   itself, which is the source of the delay. Effect: taps feel
   instant on every device. */
a, button, [role="button"],
input[type="submit"], input[type="button"], input[type="reset"],
.ArrowToRight, .feature__cta, .hero-caption__cta,
.section-link, .account-toggle, .account-panel__primary,
.account-panel__secondary, .account-panel__link,
.side-panel__close {
  touch-action: manipulation;
}

/* (2) Ensure :active feedback IS instant on touch.
   `transition` properties suppress the active-state flash because
   the colour change animates over 200ms instead of appearing
   immediately. Touch users expect <100ms response on touch-down
   for the device to feel responsive (Apple HIG, Material design).
   We don't strip transitions globally -- just speed up the active
   transition specifically. Achieved per-component below where
   :active rules are defined. */

/* (3) Establish a baseline tap target floor. WCAG 2.5.5 (Level AAA)
   requires 44x44 CSS px minimum for primary controls; iOS HIG
   requires 44pt. We enforce this via min-height/width on the
   coarse-pointer breakpoint (touchscreen devices) without
   affecting fine-pointer (mouse) layouts where tighter targets
   are fine. Buttons and primary nav links get the floor; small
   inline links inside running text are exempt (would break
   typography).

   `pointer: coarse` matches devices whose primary input is a
   finger or stylus -- iPad, iPhone, Android phones/tablets, even
   Surface in tablet mode. On laptop with trackpad it's `fine`. */
@media (pointer: coarse) {
  /* Primary buttons and CTAs */
  .feature__cta, .hero-caption__cta, .section-link,
  .account-toggle, .account-panel__primary,
  .account-panel__secondary, .account-panel__link,
  .ArrowToRight,
  .visitor-widget__close, .side-panel__close,
  button.bx-prev, button.bx-next,
  .site-search-toggle, .menu-button {
    min-height: 44px;
    min-width:  44px;
  }
  /* Nav links: vertical padding ensures 44px height even with
     small text. Keeps horizontal padding tight to fit menu. */
  .site-header__nav a {
    padding-block: max(var(--space-3), 12px);
  }
  /* Slider control dots: enforce 24px hit target as a minimum
     (visible dot stays small via ::before in the existing rule). */
  .bx-pager-item {
    min-width:  24px;
    min-height: 24px;
  }
}

/* (4) Suppress the iOS/Android "scroll chaining" effect on any
   scrollable panel we own. Without this, scrolling to the end of
   the visitor widget panel or a side panel will continue scrolling
   the page behind it, which on iOS also triggers pull-to-refresh.
   `overscroll-behavior: contain` makes the scroll stop at the
   panel boundary -- a much more focused experience that feels
   right on touch. */
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle,
.side-panel__content,
.side-panel__sheet,
.endis-jquery-ui-dialog .ui-dialog-content,
.site-header__nav {  /* mobile drawer */
  overscroll-behavior: contain;
}

/* (5) Permit pinch-to-zoom on images at all times.
   Some sites disable pinch-to-zoom site-wide via the viewport meta;
   we don't, but we explicitly opt-in for images so that even if a
   parent element ever sets `touch-action: none` for some reason,
   image zoom keeps working. Critical for accessibility -- low-vision
   users zoom photos to read embedded text. */
img {
  touch-action: pan-x pan-y pinch-zoom;
}

/* (6) Prevent text selection from triggering on rapid double-taps
   on interactive elements. Without this, double-tapping a button
   on iOS often selects the button's label text instead of firing
   the second tap, looking broken. We apply only to controls --
   normal text content stays selectable. */
button, [role="button"],
.section-link, .feature__cta, .hero-caption__cta,
.account-toggle, .account-panel__primary, .account-panel__secondary,
.ArrowToRight,
.bx-pager-item, .bx-prev, .bx-next,
.menu-button, .site-search-toggle,
.visitor-widget__close, .side-panel__close,
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .seeker_box {
  -webkit-user-select: none;
  user-select:         none;
}

/* (7) Disable iOS callout menu on long-press for our buttons.
   Long-pressing an anchor on iOS shows a "Open / Copy Link" menu
   which is unhelpful when the anchor is a styled button. We keep
   the callout enabled for normal in-content links (where users
   may legitimately want to copy them). */
button, [role="button"],
.feature__cta, .hero-caption__cta,
.section-link, .account-toggle,
.account-panel__primary, .account-panel__secondary,
.ArrowToRight,
.menu-button, .site-search-toggle,
.visitor-widget__close, .side-panel__close,
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .seeker_box {
  -webkit-touch-callout: none;
}


/* -------------------------------------------------------------------------
   1.3c  VIEWPORT FOUNDATION  (Pass B)
   -------------------------------------------------------------------------
   Best-practice viewport handling for iOS/Android quirks.

   Three classes of issue addressed:

   A) Notch + home-bar safe areas
      iPhone 14+, iPad Pro, and many Android devices have screen
      regions that may overlap fixed-position UI -- the notch at
      the top, the home indicator at the bottom, the dynamic
      island. The `env(safe-area-inset-*)` CSS values report how
      many pixels each edge needs to clear. We expose these as
      design tokens so component rules can compose safely.

   B) Dynamic viewport height (100dvh)
      `100vh` on iOS Safari counts the viewport AS IF the URL bar
      were collapsed, even when it's expanded -- so full-height
      elements get cut off behind the URL bar. Same problem on
      Android Chrome with its expanding/collapsing chrome.
      `100dvh` (dynamic viewport height) is the spec-blessed
      replacement: respects the actually-visible area and updates
      smoothly as the bar shows/hides. Supported in all modern
      browsers (iOS 15.4+, Chrome 108+, FF 101+).

   C) Input zoom on focus (iOS Safari)
      iOS Safari zooms the viewport when a form input is focused,
      IF that input has a font-size below 16px. The zoom doesn't
      reverse on blur, so users get stranded on a partially-zoomed
      page. Fix: force >=16px on every input/textarea.
   ------------------------------------------------------------------------- */

/* (A) Safe-area design tokens. These resolve to 0 on devices
   without notches/home-bars and to the OS-reported inset
   otherwise. We can use them confidently throughout component
   rules without conditionally guarding every declaration. */
:root {
  --safe-top:    env(safe-area-inset-top,    0px);
  --safe-right:  env(safe-area-inset-right,  0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left:   env(safe-area-inset-left,   0px);
}

/* (C) Force 16px minimum on all native form inputs to prevent the
   iOS zoom-on-focus behaviour. Our design tokens (--text-base
   et al.) already deliver >=16px on most components, but we
   target form elements explicitly to be defensive against any
   future component or third-party module that uses smaller text
   for inputs. The 16px is a HARD floor regardless of token
   values; max() takes the larger of the two so existing larger
   sizes are preserved. */
input[type="text"],
input[type="email"],
input[type="tel"],
input[type="url"],
input[type="password"],
input[type="number"],
input[type="search"],
input[type="date"],
input[type="datetime-local"],
input[type="time"],
input[type="month"],
input[type="week"],
input:not([type]),
select,
textarea {
  font-size: max(16px, 1rem);
}


/* -------------------------------------------------------------------------
   1.4  LAYOUT WRAPPER
   ------------------------------------------------------------------------- */
.hubb-wrap {
  width:          100%;
  max-width:      var(--container-width);
  margin-inline:  auto;
  padding-inline: var(--container-gutter);
}


/* -------------------------------------------------------------------------
   1.5  PAGE LOAD & REVEAL ANIMATIONS
   -------------------------------------------------------------------------
   A small, friendly animation system. Two flavours:

   1. Page-load fade -- body starts at opacity 0 and fades to 1 once the
      stylesheet + fonts are ready. The JS adds `body.is-loaded` after
      DOMContentLoaded; we also have a CSS fallback in case JS doesn't
      run, so content never stays invisible.

   2. Scroll reveal -- elements with `.hubb-reveal` start hidden and
      translated down 20px. As they scroll into view, IntersectionObserver
      adds `.is-visible`, triggering a soft fade-up over 700ms. Cards in
      a grid get staggered delays (set inline via JS as
      `style="--reveal-delay: 160ms"`).

   The easing curve is `cubic-bezier(0.16, 1, 0.3, 1)` -- sometimes called
   "expo out". It decelerates softly and never overshoots, which gives
   that calm, premium feel of sites like hopebeaconsfield.co.uk and
   hubb.church.

   Accessibility: all animation respects `prefers-reduced-motion: reduce`.
   Users with that setting see content immediately, no transitions.
   ------------------------------------------------------------------------- */

/* ---- Initial body fade ---- */
body {
  opacity:    0;
  transition: opacity var(--page-fade) var(--ease-out-soft);
}
body.is-loaded {
  opacity: 1;
}

/* CSS fallback -- if JS doesn't run for any reason, show body after a
   short delay rather than leaving the page blank forever. */
@media (prefers-reduced-motion: no-preference) {
  body {
    animation: hubb-body-show 0ms var(--page-fade) forwards;
  }
}
@keyframes hubb-body-show {
  to { opacity: 1; }
}

/* ---- Reveal-on-scroll ---- */
.hubb-reveal {
  opacity:        0;
  transform:      translateY(var(--reveal-distance));
  transition:     opacity var(--reveal-duration) var(--ease-out-soft),
                  transform var(--reveal-duration) var(--ease-out-soft);
  transition-delay: var(--reveal-delay, 0ms);
  /* will-change deliberately NOT set in default state. Earlier
     versions had `will-change: opacity, transform` here, but that
     keeps every revealable element on its own GPU compositing
     layer permanently -- including elements that have already
     animated and will never animate again, AND elements that may
     never animate at all (e.g. footer items the user never scrolls
     to). Permanent will-change burns GPU memory, especially bad on
     mid-range Android. We apply it transiently below only while
     the reveal transition is actually running. */
}
.hubb-reveal.is-visible {
  opacity:   1;
  transform: translateY(0);
}
/* Apply will-change only during the brief window the transition
   is running, by keying off a JS-toggled is-revealing class. Falls
   back gracefully: if the JS hook isn't applied, animations still
   run; they just don't get the compositor hint. The hint is a
   "nice to have" not a correctness requirement. */
.hubb-reveal.is-revealing {
  will-change: opacity, transform;
}

/* Opacity-only reveal -- for horizontal scrollers (rails / carousels). A
   translateY would be clipped by the track's overflow:hidden, and a
   per-card reveal can't work because off-screen cards never intersect the
   viewport. So the whole track fades in as one block instead. */
.hubb-reveal--fade,
.hubb-reveal--fade.is-visible {
  transform: none;
}

/* ---- Smooth scroll for anchor links ---- */
html {
  scroll-behavior:    smooth;
  scroll-padding-top: 100px;   /* anchor link offset for the sticky
                                  header. ~100px matches the expanded
                                  header height (24px padding + 48px
                                  logo + 24px padding + a small buffer);
                                  using the larger of the two states
                                  ensures targets land below the header
                                  even when not scrolled (compact state). */
}

/* ---- Reduced motion -- disable everything for users who prefer it ---- */
@media (prefers-reduced-motion: reduce) {
  body,
  body.is-loaded {
    opacity:    1;
    transition: none;
    animation:  none;
  }
  .hubb-reveal {
    opacity:    1;
    transform:  none;
    transition: none;
  }
  html {
    scroll-behavior: auto;
  }
  /* Slideshow: bxSlider auto-rotation runs from inline JS we don't
     control, so we can't fully stop it from CSS -- but we can at least
     freeze our own caption/control transitions. */
  .hero_slideshow * {
    animation-duration:   0.01ms !important;
    transition-duration:  0.01ms !important;
  }
  /* Dialog slide-in (added v0.58) -- render instantly with no animation
     for users who prefer reduced motion. */
  .endis-jquery-ui-dialog .ui-dialog,
  .ui-widget-overlay {
    animation: none !important;
  }
  /* Section-link hover nudge animations */
  .section-link::before,
  .section-link::after,
  .section-link {
    transition: none !important;
  }
  /* Image-tile hover zooms (explore galleries + shop categories) --
     transform-based motion, frozen for reduced-motion users. Added
     v0.162 alongside the shop tiles; the explore tiles were a
     pre-existing gap in this block. */
  .explore_church .gallery_li img,
  .explore_church_2 .gallery_li img,
  .shop_featured_categories .gallery_li img {
    transition: none !important;
    transform:  none !important;
  }
}


/* =========================================================================
   ACCESSIBILITY UTILITIES  --  used across the site
   -------------------------------------------------------------------------
   .sr-only         Visually hidden but accessible to screen readers and
                    keyboard focus. Use for skip links, off-screen labels,
                    or anything that needs to be announced but not seen.
   .skip-to-content Standard a11y skip link -- invisible until keyboard-
                    focused, then becomes a high-contrast pill at the top
                    of the page that jumps to main content. Injected by
                    addSkipToContentLink() in script.js.
   ========================================================================= */

.sr-only {
  position: absolute !important;
  width:    1px !important;
  height:   1px !important;
  padding:  0 !important;
  margin:   -1px !important;
  overflow: hidden !important;
  clip:     rect(0, 0, 0, 0) !important;
  white-space: nowrap !important;
  border:   0 !important;
}

.skip-to-content {
  position:        absolute;
  top:             var(--space-2);
  left:            var(--space-2);
  z-index:         99999;
  padding:         var(--space-3) var(--space-4);
  background:      var(--accent-dark);   /* AAA contrast vs white */
  color:           var(--accent-ink);
  font-family:     var(--font-body);
  font-size:       var(--text-sm);
  font-weight:     600;
  letter-spacing:  0.02em;
  text-decoration: none;
  border-radius:   var(--radius);
  /* Hidden off-screen until focused */
  transform:       translateY(-150%);
  transition:      transform 200ms ease;
}
/* Show only on keyboard focus -- :focus-visible fires when the
   browser determines the focus came from keyboard nav (Tab key),
   not from clicks or programmatic focus shifts (e.g. when DevTools
   opens / closes, or when a script .focus()es an element). Without
   this distinction the skip link flashes visible whenever focus
   moves to it for any reason. */
.skip-to-content:focus-visible {
  transform: translateY(0);
  outline:   2px solid var(--accent-ink);
  outline-offset: 2px;
}

/* Make any focused tabindex="-1" main target invisible-focus (so it
   doesn't show a default outline on click but does on keyboard nav) */
[tabindex="-1"]:focus {
  outline: none;
}


/* =========================================================================
   PERFORMANCE HINTS  --  let the browser skip work it doesn't need
   -------------------------------------------------------------------------
   content-visibility: auto on long content sections lets the browser
   skip layout/render of off-screen content until the user scrolls
   close. Real performance benefit on long pages with many cards;
   contain-intrinsic-size gives the browser a height estimate so the
   scrollbar doesn't jump as content paints in.
   
   Used cautiously -- applying to the wrong elements (anything that needs
   to participate in scroll-based animation, anchor jumping, or
   ResizeObserver) can cause subtle bugs. Stuck to known-safe sections
   below the fold.
   ========================================================================= */

@supports (content-visibility: auto) {
  .latest-news,
  .upcoming-events,
  .latest-media,
  .footer-safeguarding,
  .site-footer__bottom {
    content-visibility:    auto;
    contain-intrinsic-size: 1px 600px;   /* rough height estimate */
  }
}


/* =========================================================================
   2. SITE HEADER LAYOUT  --  three rows
   -------------------------------------------------------------------------
   Wrap the header modules in Body Contents (Source mode) like so:

     <header class="site-header">
       <div class="site-header__utility">
         [Status Bar module]
       </div>
       <div class="site-header__main">
         <div class="site-header__brand">
           [Site Logo module]
           [Site Name module]
         </div>
         [Search module]
       </div>
       <div class="site-header__nav">
         [Drop-down menus module]
       </div>
     </header>

   Desktop result:
     +-----------------------------------------------------------------+
     |                                              [ Status Bar   ]  |  <- utility
     |  [Logo] The Town Church                      [ Search       ]  |  <- main
     |                                              [ Main Nav ... ]  |  <- nav
     +-----------------------------------------------------------------+
   ========================================================================= */

.site-header {
  border-bottom: 1px solid var(--rule);

  /* Sticky positioning -- keeps the nav in view while scrolling.
     Background must be opaque so content scrolling underneath
     doesn't bleed through. z-index: 50 lifts the header above
     page content but leaves room for higher-z-index things like
     side panels (19000) and dialogs.

     IMPORTANT -- width strategy: this element is full-viewport-
     width so the bg + border-bottom extend edge to edge. Inner
     .site-header__main carries the max-width and centring so the
     CONTENT stays in the 1600px column. Otherwise, on viewports
     wider than 1600px, the sticky bar would only paint to 1600px
     and any full-bleed content (e.g. the hero) scrolling under it
     would peek out at the sides of the sticky bar -- visible band
     of hero/page content above where the sticky bar ends.

     IMPORTANT -- z-index + stacking context architecture:
     position: sticky creates a stacking context (per spec). The
     mobile menu's panel (z-index 9050, inside .site-header) and
     backdrop (z-index 9000, also inside .site-header thanks to the
     JS in initMobileMenu) BOTH live within this stacking context,
     so the panel correctly renders above the backdrop.

     This replaced an older v0.108-era workaround that only applied
     z-index: 50 above 1401px (the mobile breakpoint), because at
     that time the backdrop sat at body level -- if .site-header had
     z-index 50 in the mobile range, its 50 vs the backdrop's 9000
     would put the backdrop ABOVE the trapped panel and the menu
     would close on every click. Putting the backdrop inside
     .site-header in the JS removed that constraint. */
  position:   sticky;
  top:        0;
  z-index:    50;
  background: var(--bg);
  transition: box-shadow var(--transition);
}

/* Subtle shadow when the user has scrolled past the threshold.
   Floats the header off the page so it reads as "above" the
   content scrolling beneath. Applied at all viewports because
   the visual separation matters whether or not the header is
   compacted. */
.site-header.is-scrolled {
  box-shadow: 0 4px 16px -8px rgba(26, 22, 18, 0.15);
}

/* Row 1 -- utility strip (Status Bar). Visually hidden as of v0.69
   when the account panel was introduced -- the date/greeting/login
   links now live inside the account-panel slide-out instead, giving
   us a cleaner two-row header (brand + nav + account/search).
   
   We keep it in the DOM so JS can still read the statusbar's links
   (login state, web office URL, etc.) and rebuild them in the panel.
   Hubb's status bar module is the source of truth for these URLs. */
.site-header__utility {
  position: absolute;
  width:    1px;
  height:   1px;
  padding:  0;
  margin:   -1px;
  overflow: hidden;
  clip:     rect(0, 0, 0, 0);
  white-space: nowrap;
  border:   0;
}

/* Row 2 -- single row with brand on the left, nav + search-toggle on the right.
   Body Contents must place .site-header__nav INSIDE .site-header__main
   for this to work (see body Contents snippet in BACKLOG).
   On mobile, .site-header__nav is repositioned via position: fixed to
   become the slide-in panel -- the DOM nesting is irrelevant in that
   mode because fixed positioning is viewport-relative.

   This row carries the max-width + centring + horizontal padding that
   used to live on .site-header. Moved here when sticky positioning was
   introduced so the parent could be full-viewport-width (see comment
   on .site-header above for why). */
.site-header__main {
  max-width:       var(--container-width);
  margin-inline:   auto;
  padding-inline:  var(--container-gutter);
  display:         flex;
  align-items:     center;
  gap:             var(--space-6);
  padding-block:   var(--space-6);
}

.site-header__brand {
  display:     flex;
  align-items: center;
  gap:         var(--space-4);
  flex-shrink: 0;                 /* never let brand compress when nav grows */
}

/* Nav lives inside the main row now. margin-left: auto pushes it to
   the right edge so brand stays on the left. The search-toggle button
   (added by JS) sits as a sibling AFTER .site-header__nav, anchored
   to the very far right. */
.site-header__nav {
  margin-left: auto;
  display:     flex;
  align-items: center;
}

/* =========================================================================
   2.4b  HEADER VARIANT: TWO-ROW  --  .site-header--tworow
   -------------------------------------------------------------------------
   First of the site-header--* layout modifiers. Switch the whole header by
   adding ONE class:  <header class="site-header site-header--tworow">  and
   wrapping the utility row's contents in .site-header__utility-inner with an
   optional .header-giving pill (see Body Contents). Everything is scoped to
   the modifier, so the default single-row header is completely untouched.

   What it does: reveals the Status Bar utility row (normally visually hidden)
   as a slim top strip -- greeting + session links on the left, a Giving pill
   on the right -- above the existing brand + nav row. Revealing it does NOT
   affect the JS that mirrors the statusbar links into the account panel (that
   reads the DOM regardless of visibility), and the statusbar's own search box
   stays hidden via its existing rule. The account toggle + search toggle stay
   in the main row exactly where the JS places them. */

/* Top strip: undo the visually-hidden clip the default header applies, and
   make it a full-width slim bar with a faint tint + hairline below. */
.site-header--tworow .site-header__utility {
  position:      static;
  width:         auto;
  height:        auto;
  margin:        0;
  padding:       0;
  overflow:      visible;
  clip:          auto;
  white-space:   normal;
  background:    var(--section-light);
  border-bottom: 1px solid var(--rule);
}
/* Inner keeps the strip content in the same 1600px column as the main row
   (the tint stays full-width on the parent above). */
.site-header--tworow .site-header__utility-inner {
  max-width:       var(--container-width);
  margin-inline:   auto;
  padding-inline:  var(--container-gutter);
  padding-block:   var(--space-2);
  display:         flex;
  align-items:     center;
  justify-content: space-between;
  gap:             var(--space-4);
}
/* The Status Bar module's unclassed wrapper <div> collapses out so .statusbar
   becomes a direct flex child of the inner row. */
.site-header--tworow .site-header__utility-inner > div { display: contents; }
/* .statusbar already ships styled + flex; the default rule justifies it to the
   right, so flip it left and let it take the slack to push Giving far right. */
.site-header--tworow .statusbar { justify-content: flex-start; flex: 1; }

/* Giving pill -- a plain <a> (not a module) styled as the accent pill. Sits
   outside .statusbar, so no statusbar link colour applies; colour set anyway. */
.site-header--tworow .header-giving,
.site-header--split  .header-giving {
  display:         inline-flex;
  align-items:     center;
  flex-shrink:     0;
  padding:         var(--space-2) var(--space-5);
  background:      var(--accent);
  color:           var(--accent-ink);
  font-family:     var(--font-body);
  font-size:       var(--text-xs);
  font-weight:     700;
  letter-spacing:  0.1em;
  text-transform:  uppercase;
  text-decoration: none;
  border-radius:   999px;
  transition:      background var(--transition);
}
.site-header--tworow .header-giving:hover,
.site-header--split  .header-giving:hover { background: var(--accent-dark); }

/* Mobile layout rules for the whole library live in section 4 below. */


/* =========================================================================
   2.4c  HEADER VARIANT: SPLIT  --  .site-header--split
   -------------------------------------------------------------------------
   Second of the site-header--* layout modifiers, and the first aimed at
   non-church sites. Switch the whole header by adding ONE class:
       <header class="site-header site-header--split">
   and placing the optional .header-giving pill inside .site-header__main
   (see Body Contents). Everything is scoped to the modifier, so both the
   default header and the two-row variant are completely untouched.

   What it does: single row with the brand hard left, the main nav sitting
   immediately beside it rather than pushed right the way the default header
   does, and the action cluster (search toggle, account toggle, accent pill)
   anchored far right. The classic shape, and it suits a commercial site
   where the pill is the primary conversion action.

   WHY the order property rather than DOM order: the search toggle, the
   account toggle and the burger are all injected into .site-header__main at
   runtime by three separate init steps. Ordering visually here means the
   layout holds regardless of the sequence those steps run in, and
   regardless of where Body Contents puts the pill. Nothing below depends on
   JS insertion order.

   WHY scoped to min-width 1401px: below that, .site-header__nav becomes a
   position: fixed slide-in panel (section 4). These rules carry two class
   selectors, so they would out-specify the single-class mobile rules and
   leak into the panel layout even though the mobile block sits later in the
   file. This breakpoint and the nav breakpoint must stay in step, 1400 and
   1401, or a dead zone opens up between them. */

@media (min-width: 1401px) {
  /* Tighter than the default --space-6 so the actions read as one cluster.
     The wide brand-to-actions separation comes from the nav's auto margin
     below, not from this gap. */
  .site-header--split .site-header__main { gap: var(--space-4); }

  /* The default header pushes the nav right with margin-left: auto. Undo
     that and flip the auto margin to the right instead, so the nav sits
     beside the brand and all the slack lands between nav and actions.
     margin-left must be set explicitly: leaving the inherited auto in place
     would centre the nav between two auto margins. */
  .site-header--split .site-header__nav {
    margin-left:  0;
    margin-right: auto;
    order:        1;
  }

  .site-header--split .site-header__brand { order: 0; }
  .site-header--split .site-search-toggle { order: 2; }
  .site-header--split .account-toggle     { order: 3; }
  .site-header--split .header-giving      { order: 4; }
  .site-header--split .menu-button        { order: 5; }
}


/* =========================================================================
   2.4d  HEADER UTILITY: no account control  --  .site-header--no-account
   -------------------------------------------------------------------------
   Opt-in and additive, so churches keep the account panel and nothing
   changes unless this class is added to the header element. For an
   outward-facing business site with no member area, the Account button
   invites visitors into a sign-in flow with nothing behind it.

   Hidden rather than never built: the account init step reads the Status
   Bar for session and web-office URLs and builds the panel from them, and
   hiding is the low-risk change. Suppressing the build would need a JS flag
   and buys nothing visible. The panel is hidden too as belt and braces, so
   it cannot be reached even if something else toggles it. */
.site-header--no-account .account-toggle,
.site-header--no-account .account-panel { display: none; }


/* =========================================================================
   2.5  SLICE BASE  --  shared structure for every <main> content block
   -------------------------------------------------------------------------
   See "SLICE CATALOGUE" and "HOW TO BUILD A NEW SLICE" in the file
   header for the full pattern. Quick recap:

     <section class="slice slice--name">                  outer wrapper
       <div class="slice__head slice__head--centred">     optional intro
         <h2 class="slice__heading">...</h2>
       </div>
       <module ... />                                     the Hubb module
     </section>

   The .slice base rule handles vertical rhythm + container width that
   every slice shares. The .slice--name modifier in section 3 handles
   anything specific to that slice (background, internal grid, etc.).
   ------------------------------------------------------------------------- */
.slice {
  /* Every slice contributes the same vertical rhythm by default.
     Specific slices can override (e.g. .slice--hero is full-bleed
     and sets margin: 0). */
  margin-block:  var(--space-16);
  /* Container constraint -- slices that need full-bleed (.slice--hero,
     .slice--announcement, .slice--feature with parallax bg) opt out by
     setting max-width: none and padding-inline: 0 on themselves.
     Default keeps content inside the page's editorial column. */
  max-width:     var(--container-width);
  margin-inline: auto;
  padding-inline: var(--container-gutter);
  /* Position context for slices that need to layer (e.g. announcement
     banners with absolute close buttons). Costs nothing if unused. */
  position:      relative;
}

/* ---- Section themes: light / dark bands -------------------------------------
   Add .slice--light-grey or .slice--dark-grey to any .slice to give it a
   full-bleed coloured band. Sites read better when sections alternate
   background, so this is the one-class switch for that.

   How it works:
     - A 100vw ::before, painted BEHIND the content (z-index:-1), is the band.
       The slice's own content width/padding is untouched, so a themed events
       grid keeps the full column, a themed explore gallery keeps its 1200px
       column, etc.
     - .slice--dark-grey ALSO redefines the colour tokens (--ink, --ink-soft,
       --rule, --surface, --bg, --accent-soft) for its subtree. Because every
       element reads its colour from these tokens, all text, hairlines and
       cards invert to light automatically -- no per-element overrides.
     - Bands use padding-block (not margin) and margin-block:0 so two adjacent
       bands sit flush, giving the clean alternating look.

   Use on standard content slices (events, sermons, news, explore, welcome).
   The hero and the parallax .slice--feature already manage their own
   backgrounds, so don't add a band class to those.

   Note: the accent colour must contrast with the dark grey (the themed
   terracotta does). A few elements use --accent-dark as a *text* colour
   (e.g. the stacked variants' time line); if you put one of those in a dark
   band and it reads low-contrast, ping me and I'll add a targeted override. */
/* Themed bands sit FLUSH (margin-block:0) so two adjacent bands touch for the
   alternating look, while a plain neighbour's own 64px margin supplies the gap
   on that side -- giving consistent rhythm whichever way sections are ordered.
   The .slice.slice--* compound (specificity 0,2,0) is deliberate: the
   per-section rules (.slice--upcoming-events etc., 0,1,0) sit later in the
   file and set margin-block:64px, which would otherwise win and stop the band
   going flush. Internal breathing room is padding-block, INSIDE the colour. */
.slice.slice--light-grey,
.slice.slice--dark-grey {
  margin-block:  0;
  padding-block: var(--section-pad-block);
}
.slice--light-grey::before,
.slice--dark-grey::before {
  content:   "";
  position:  absolute;
  z-index:   -1;            /* behind the slice content */
  top:       0;
  bottom:    0;
  left:      50%;
  width:     100vw;         /* full-bleed band; content stays in its column */
  transform: translateX(-50%);
}
.slice--light-grey::before { background: var(--section-light); }
.slice--dark-grey::before  { background: var(--section-dark); }

/* Dark band: flip the colour tokens for the whole subtree. --surface steps to
   a slightly lighter dark than the band so cards/badges still read as raised;
   --accent / --accent-ink are left alone so buttons keep their colour. */
.slice--dark-grey {
  --ink:         #FFFFFF;
  --ink-soft:    rgba(255, 255, 255, 0.72);
  --rule:        rgba(255, 255, 255, 0.18);
  --surface:     #353535;
  --bg:          #303030;
  --accent-soft: rgba(255, 255, 255, 0.10);
  color:         var(--ink);
}

/* ---- Full-bleed opt-outs ----
   The .slice base rule (above) constrains every slice to the editorial
   container width with side gutters. Some slices need to break out and
   span the full viewport edge-to-edge:

     .slice--hero          The hero slideshow -- needs full-bleed for
                           the cinematic effect; image cropping breaks
                           if constrained to the container width.
     .slice--announcement  The notice banner -- a coloured strip is
                           the entire visual point; gutters would
                           reduce it to a floating pill which reads
                           as decorative rather than functional.

   v0.131 fix: when v0.130 introduced the .slice base rule, it
   accidentally constrained both slices for the first time. Visible
   regression in Adam's screenshot: hero and announcement showing
   white side-margins where they used to bleed.

   Other full-bleed slices (.slice--feature) handle their own
   breakout in their module rules below.

   v0.146: added .slice--partner-logos to this rule. The v0.144
   change made `.umbrella_org_logo` itself full-bleed cream, but
   missed that Hubb's body Contents wraps the logo gallery in
   `<section class="slice slice--partner-logos">` -- and that
   wrapper still inherited the default .slice constraints
   (margin-block: 64px, max-width: 1600px, padding-inline: 40px).
   Adam's DevTools showed the section computed at 1143px wide
   with 64px top + bottom margins, so the cream band sat inside
   a dark frame on three sides.

   v0.148: tried splitting partner-logos out into its own rule
   with .slice's default margin-block restored, intending to give
   it "breathing room consistent with other sections". Adam's
   screenshot showed the result -- 64px dark gap between
   safeguarding (cream) and partner-logos (cream) read as a
   "big black block" between two cream zones, not as breathing
   room. Wrong frame: the two cream zones should flow as ONE
   continuous light band, with breathing room only between that
   band and the dark address block below.

   v0.149: reverted the v0.148 split. Partner-logos is back in
   this zero-margin group so safeguarding + umbrella sit flush
   as one cream zone. The address-block breathing room is now
   on `.site-footer__inner`'s padding-block-start instead, where
   it semantically belongs (it's the dark area's internal spacing
   above its content, not the partner-logos slice's separation
   from its sibling). */
.slice--hero,
.slice--announcement,
.slice--partner-logos {
  max-width:      none;
  padding-inline: 0;
  /* margin-block: 0 here is critical. The .slice base rule (above)
     gives every slice 64px top + bottom margin, which is the right
     vertical rhythm for content slices (Latest News, Upcoming Events,
     etc.). But the hero slideshow, announcement banner, and footer
     partner-logos band are edge-to-edge AND meant to butt directly
     against their neighbours -- the hero against the header above
     and the announcement below it, the announcement against the
     welcome article that follows, the partner-logos against the
     safeguarding band above (one continuous cream zone). Each
     band's internal spacing comes from its own module rules:
     .hero_slideshow, .announcement_banner, .umbrella_org_logo. */
  margin-block:   0;
}

/* ---- Inner head: centred (default for grids) ----
   Heading sits centred above the module's content. Optional accent
   rule beneath via :has() so we don't need an extra DOM element. */
.slice__head--centred {
  text-align:   center;
  margin-block: 0 var(--space-8);
}
.slice__head--centred .slice__heading {
  display:        inline-block;
}

/* ---- Inner head: intro column (carousel variants) ----
   Heading + tagline + CTA stacks in a column on the left, the
   module's content sits on the right. Falls back to stacking
   on narrow viewports (mobile rule in section 4). */
.slice__head--intro {
  display:               grid;
  grid-template-columns: minmax(220px, 1fr) 3fr;
  gap:                   var(--space-12);
  align-items:           center;
  margin-block:          0 var(--space-8);
}
.slice__head--intro .slice__heading {
  text-align: left;
}
.slice__head--intro .slice__tagline {
  font-family: var(--font-body);
  font-size:   var(--text-base);
  color:       var(--ink-soft);
  margin:      var(--space-4) 0 var(--space-6);
}
.slice__head--intro .slice__cta {
  display:         inline-flex;
  align-items:     center;
  gap:             var(--space-2);
  font-family:     var(--font-body);
  font-size:       var(--text-sm);
  font-weight:     600;
  color:           var(--ink);
  text-decoration: none;
  border-bottom:   1px solid currentColor;
  padding-bottom:  2px;
  align-self:      start;
  transition:      color var(--transition);
}
.slice__head--intro .slice__cta:hover { color: var(--accent-dark); }
.slice__head--intro .slice__cta::after {
  content: "->";
  transition: transform var(--transition);
}
.slice__head--intro .slice__cta:hover::after { transform: translateX(3px); }

/* ---- Shared heading style ----
   Tier-3 in the page hierarchy (between welcome H1 and card titles).
   Same scale as the legacy *__heading classes (.latest-news__heading
   etc.) which keep working alongside this for backward compatibility. */
.slice__heading {
  font-family:    var(--font-display);
  font-size:      clamp(2rem, 3.8vw, 3.25rem);
  font-weight:    400;
  letter-spacing: -0.01em;
  line-height:    1.1;
  margin:         0;
  color:          var(--ink);
}


/* =========================================================================
   3. MODULES
   ========================================================================= */


/* -------------------------------------------------------------------------
   3.1  MODULE: Site Logo
   -------------------------------------------------------------------------
   Settings -> CSS Class = "site-logo"
   Hubb output:  <span class="site-logo"><a><img></a></span>
   ------------------------------------------------------------------------- */
.site-logo {
  display:     inline-flex;
  align-items: center;
  line-height: 0;
}
.site-logo a   { display: block; line-height: 0; }
.site-logo img { height: 48px; width: auto; max-width: none; }


/* -------------------------------------------------------------------------
   3.2  MODULE: Site Name
   -------------------------------------------------------------------------
   Settings -> CSS Class = "site-name"
   Hubb output:  <span class="site-name"><a>The Town Church</a></span>
   ------------------------------------------------------------------------- */
.site-name {
  font-family:     var(--font-display);
  font-size:       var(--text-lg);   /* was --text-xl; reduced for streamlined single-row header */
  font-weight:     500;
  line-height:     1;
  letter-spacing: -0.01em;
  white-space:     nowrap;        /* keep "The Town Church" on one line */
}
.site-name a {
  color:           var(--ink);
  text-decoration: none;
  transition:      opacity var(--transition);
}
.site-name a:hover { opacity: 0.7; }


/* -------------------------------------------------------------------------
   Sticky-header compaction -- DESKTOP ONLY
   -------------------------------------------------------------------------
   When the user scrolls past the threshold, .is-scrolled is added to
   .site-header by initStickyHeader(). On desktop we use that to
   compact the header by ~30% (logo shrinks, site name shrinks, padding
   tightens) so the nav takes less vertical space while staying readable.

   Mobile DELIBERATELY skipped per the brief -- the hamburger toggle
   needs comfortable touch-target sizing, and the header is already
   compact enough on phones. Sticky still applies on mobile (handled
   in the .site-header rule above), it's just that the compact STATE
   doesn't trigger any size changes there.

   Transitions go on the elements that change so the compaction
   animates smoothly rather than snapping when crossing the threshold.
   ------------------------------------------------------------------------- */
@media (min-width: 1401px) {
  .site-header__main,
  .site-logo img,
  .site-name {
    transition: padding-block var(--transition),
                height        var(--transition),
                font-size     var(--transition);
  }

  .site-header.is-scrolled .site-header__main {
    /* Original padding is var(--space-6) (24px). Compact halves
       it to var(--space-3) (12px), giving a 24px total reduction
       across the row. */
    padding-block: var(--space-3);
  }
  .site-header.is-scrolled .site-logo img {
    /* Original 48px -> 40px (~83%). Combined with the padding
       reduction the row is ~64px tall vs ~96px expanded -- about
       a 33% squash, matching the brief. */
    height: 40px;
  }
  .site-header.is-scrolled .site-name {
    /* Drop one type-scale tier: --text-lg -> --text-base. Keeps
       the same visual proportion against the 40px logo as the
       original --text-lg sat against the 48px logo. */
    font-size: var(--text-base);
  }
}


/* -------------------------------------------------------------------------
   3.3  MODULE: Status Bar
   -------------------------------------------------------------------------
   No settings class needed -- Hubb wraps this in <div class="statusbar">
   natively. The internal search is hidden because we use the separate
   Search module. To bring it back, delete the .search_div / .sitebutton
   hide rules at the bottom of this block.
   ------------------------------------------------------------------------- */
.statusbar {
  display:         flex;
  align-items:     center;
  justify-content: flex-end;
  gap:             var(--space-4);
  font-family:     var(--font-body);
  font-size:       var(--text-xs);
  color:           var(--ink-soft);
}

/* Greeting -- italic serif for a masthead feel */
.statusbar .greeting_div {
  font-family: var(--font-display);
  font-style:  italic;
  font-weight: 400;
  font-size:   var(--text-sm);
  color:       var(--ink);
}

/* Session links (login/logout/register/web-office) */
.statusbar a {
  color:           var(--ink-soft);
  text-decoration: none;
  font-weight:     700;
  letter-spacing:  0.12em;
  text-transform:  uppercase;
  transition:      color var(--transition);
  opacity:         1;
}
.statusbar a:hover {
  color:   var(--accent);
  opacity: 1;
}

/* Hide the internal search (using the separate Search module instead) */
.statusbar .search_div,
.statusbar .sitebutton { display: none; }


/* -------------------------------------------------------------------------
   3.4  MODULE: Search
   -------------------------------------------------------------------------
   Settings -> CSS Class = "site-search"
   "Site Search" label is visually hidden (still read by screen readers).
   ------------------------------------------------------------------------- */
.site-search {
  display:     flex;
  align-items: stretch;
  max-width:   340px;
  width:       100%;
  margin-left: auto;   /* right-align inside .site-header__main */
}

.site-search .search_title {
  position:    absolute;
  width:       1px;
  height:      1px;
  padding:     0;
  margin:     -1px;
  overflow:    hidden;
  clip:        rect(0, 0, 0, 0);
  white-space: nowrap;
  border:      0;
}

.site-search .search_div { flex: 1; }

.site-search .text {
  width:         100%;
  padding:       var(--space-2) var(--space-3);
  border:        1px solid var(--rule);
  background:    var(--surface);
  font-family:   var(--font-body);
  font-size:     var(--text-sm);
  color:         var(--ink);
  border-radius: var(--radius) 0 0 var(--radius);
  transition:    border-color var(--transition);
}
.site-search .text:focus {
  outline:      none;
  border-color: var(--accent);
}

.site-search .button {
  padding:        var(--space-2) var(--space-4);
  border:         1px solid var(--accent);
  background:     var(--accent);
  color:          var(--accent-ink);
  font-family:    var(--font-body);
  font-size:      var(--text-xs);
  font-weight:    700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  cursor:         pointer;
  border-radius:  0 var(--radius) var(--radius) 0;
  transition:     background var(--transition), border-color var(--transition);
}
.site-search .button:hover {
  background:   var(--ink);
  border-color: var(--ink);
}

/* ---- Inline search visibility -------------------------------------------
   When the page first renders, the .site-search element is wherever the
   <module> tag is -- somewhere inside .site-header__main. We hide it
   immediately so it never flashes; JS (setupSearchOverlay) then moves
   it into the .site-search-overlay element where it becomes visible
   again. The selector is scoped so a .site-search inside the overlay
   stays visible. */
.site-header__main .site-search {
  display: none;
}


/* -------------------------------------------------------------------------
   3.4b  Search Toggle  +  Search Overlay  (custom UI, no Hubb module)
   -------------------------------------------------------------------------
   Pattern modelled on Aspire Church (https://aspire.hubb.church): a
   magnifying-glass icon button in the header opens a full-width band
   with a prominent search input and instructional text. Closing the
   overlay returns the user to the page. The actual search input is
   the Hubb Search module -- JS just relocates it into the overlay
   so we don't have to reinvent the form/submit mechanics.
   ------------------------------------------------------------------------- */

/* The toggle button -- a 44x44 icon button at the right edge of the
   header. Created by JS (setupSearchOverlay), inserted into
   .site-header__main as the last child. */
.site-search-toggle {
  width:           44px;
  height:          44px;
  display:         inline-flex;
  align-items:     center;
  justify-content: center;
  padding:         0;
  background:      transparent;
  border:          0;
  border-radius:   999px;
  color:           var(--ink);
  cursor:          pointer;
  transition:      background var(--transition), color var(--transition);
  flex-shrink:     0;
}
.site-search-toggle:hover {
  background: var(--accent-soft);
  color:      var(--accent);
}
.site-search-toggle:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 2px;
}
.site-search-toggle__icon {
  width:  20px;
  height: 20px;
  stroke: currentColor;
  stroke-width:    2;
  stroke-linecap:  round;
  stroke-linejoin: round;
  fill:   none;
}

/* The overlay -- full-width band fixed to the top of the viewport.
   Starts off-screen (translateY(-100%)) and slides in when .is-open
   is added. visibility coupling avoids it intercepting clicks while
   hidden (transform alone leaves it in the layer tree).
   Light background to match the rest of the site -- feels like a
   flyout drawer rather than a foreign modal. */
.site-search-overlay {
  position:        fixed;
  top:             0;
  left:            0;
  right:           0;
  background:      var(--surface);
  color:           var(--ink);
  z-index:         9200;
  transform:       translateY(-100%);
  transition:      transform 0.35s ease, visibility 0s linear 0.35s;
  visibility:      hidden;
  border-bottom:   1px solid var(--rule);
  box-shadow:      0 8px 24px -8px rgba(26, 22, 18, 0.12);
}
.site-search-overlay.is-open {
  transform:  translateY(0);
  visibility: visible;
  transition: transform 0.35s ease, visibility 0s linear 0s;
}

.site-search-overlay__inner {
  max-width:      var(--container-width);
  margin-inline:  auto;
  /* Top padding composes with safe-area-inset-top so the search
     title clears notch / dynamic island when the overlay drops
     down from the top of the viewport. */
  padding:        calc(var(--space-12) + var(--safe-top))
                  calc(var(--container-gutter) + var(--safe-right))
                  var(--space-12)
                  calc(var(--container-gutter) + var(--safe-left));
  position:       relative;
}

.site-search-overlay__title {
  margin:         0 0 var(--space-2);
  font-family:    var(--font-display);
  font-size:      clamp(1.75rem, 3vw + 1rem, 2.5rem);
  font-weight:    500;
  color:          var(--ink);
  line-height:    1.15;
  letter-spacing: -0.01em;
}

.site-search-overlay__hint {
  margin:      0 0 var(--space-6);
  font-family: var(--font-body);
  font-size:   var(--text-sm);
  color:       var(--ink-soft);
}

/* Close button -- same pill pattern as the mobile menu close button
   so close affordances feel like one family across the site. */
.site-search-overlay__close {
  position:        absolute;
  top:             var(--space-4);
  right:           var(--space-4);
  display:         inline-flex;
  align-items:     center;
  gap:             var(--space-2);
  padding:         var(--space-2) var(--space-4);
  background:      transparent;
  border:          1px solid var(--rule);
  border-radius:   999px;
  cursor:          pointer;
  color:           var(--ink);
  font-family:     var(--font-body);
  font-size:       var(--text-xs);
  font-weight:     700;
  letter-spacing:  0.1em;
  text-transform:  uppercase;
  line-height:     1;
  transition:      background var(--transition), border-color var(--transition);
}
.site-search-overlay__close__icon {
  font-size:   18px;
  font-weight: 300;
  line-height: 1;
}
.site-search-overlay__close:hover {
  background:   var(--accent-soft);
  border-color: var(--accent);
}
.site-search-overlay__close:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 2px;
}

/* Search input + button inside the overlay -- full-width, larger and
   more prominent than the original inline version. Connected as a
   single unit (input + button share the rounded pill silhouette).
   Overrides the basic .site-search rules from section 3.4 above. */
.site-search-overlay .site-search {
  display:    flex;
  max-width:  none;
  width:      100%;
  margin:     0;
}
.site-search-overlay .site-search .text {
  padding:       var(--space-4);
  font-size:     var(--text-base);
  border:        1px solid var(--rule);
  background:    var(--surface);
  border-radius: var(--radius) 0 0 var(--radius);
}
.site-search-overlay .site-search .text:focus {
  border-color: var(--accent);
  outline:      none;
}
.site-search-overlay .site-search .button {
  padding:       var(--space-4) var(--space-8);
  font-size:     var(--text-sm);
  border-radius: 0 var(--radius) var(--radius) 0;
}
.site-search-overlay .site-search .button:hover {
  background:   var(--ink);
  border-color: var(--ink);
}


/* -------------------------------------------------------------------------
   3.5  MODULE: Drop-down Menus (main navigation)
   -------------------------------------------------------------------------
   Settings -> CSS Class = "main-nav"

   HEADS UP: Hubb renders this as nested HTML tables with <div class="t">
   items (not anchors -- clicks are JS-driven). We flatten the tables and
   style the .t items as a modern horizontal nav. Dropdown panels (.tah
   for horizontal parents, .tav for nested vertical ones) are injected
   and positioned by Hubb's JS; we restyle them aggressively.

   Selectors are boosted with #m so they reliably beat Hubb's base CSS.
   ------------------------------------------------------------------------- */

/* --- Hubb config workaround: hide Home's auto-populated dropdown ---
   Hubb auto-populates the first menu item ("Home" via #bm_1, dropdown
   panel #m_1) with Calendar / Forums / Audio-Video / Address Book /
   Homepage links, controlled by tickboxes in the Drop-down Menus
   module settings. There's a Hubb config quirk where unticking those
   boxes makes the WHOLE menu vanish (not just the Home dropdown).
   So we leave the tickboxes alone in Hubb and just hide the Home
   dropdown panel from rendering. The Home text itself stays clickable
   and routes to the homepage as normal. */
.main-nav #m_1 { display: none !important; }

/* --- Single-item dropdown collapse (desktop).
   When a top-level menu item has only one entry in its dropdown,
   forcing the user to hover-then-click is unnecessary friction. JS
   (collapseSingleItemDropdowns in script.js) detects these cases and
   tags the dropdown panel with this class, then forwards parent click
   to the single child. !important is required because Hubb's hover JS
   sets `display: block` as an inline style -- class rules can't beat
   inline styles without !important. Mobile gets the same UX via
   buildCleanMobileNav's directRows.length === 1 branch. */
.main-nav .main-nav__dropdown--collapsed { display: none !important; }

/* --- Flatten Hubb's outer table-based layout --- */
.main-nav,
.main-nav .menubardiv,
.main-nav table.menubardiv,
.main-nav table.menubardiv > tbody,
.main-nav table.menubardiv > tbody > tr {
  display:         block;
  width:           auto;
  height:          auto;
  border:          0;
  padding:         0;
  margin:          0;
  border-collapse: collapse;
}

.main-nav .menubar > td:first-child { display: block; }

/* Hide the "Low Graphics" link that rides along -- we'll surface it in
   the footer later. Delete this rule to bring it back. */
.main-nav .menubar > td:last-child { display: none; }

/* --- Inner menu table becomes a right-aligned flex row.
   nowrap is critical: with 7 verbose menu items the row would
   otherwise break onto two lines at narrow desktop widths. The
   mobile breakpoint (<=1400px) catches anything that wouldn't fit;
   between there and the container max, the layout is guaranteed
   wide enough. Gap shrunk from --space-6 to --space-4 to give
   the nav a bit more breathing room before it hits the breakpoint. */
.main-nav table#m,
.main-nav table#m > tbody,
.main-nav table#m > tbody > tr {
  display:         flex;
  align-items:     center;
  justify-content: flex-end;
  flex-wrap:       nowrap;
  gap:             var(--space-4);
  width:           auto;
  border-collapse: collapse;
}

.main-nav #m td[class*="t_horizontal"] {
  display:     flex;
  align-items: center;
  padding:     0;
  border:      0;
  position:    relative;  /* positioning context for child .tah */
}

/* --- Top-level menu item  --  scoped to direct children of the horizontal
   cells. Using [class*="t_horizontal"] catches BOTH the base class and
   the "t_horizontal_hi" hover state that Hubb's JS swaps to. Without
   this, styles vanished on hover and the text jumped position. --- */
.main-nav #m td[class*="t_horizontal"] > div.t {
  font-family:     var(--font-body);
  font-size:       var(--text-sm);
  font-weight:     500;
  color:           var(--ink);
  padding:         var(--space-2) 0;
  cursor:          pointer;
  transition:      color var(--transition);
  letter-spacing:  0.02em;
}
.main-nav #m td[class*="t_horizontal"] > div.t:hover { color: var(--accent); }

/* Hide the "|" separators between top-level items */
.main-nav .t_separator { display: none; }


/* --- Dropdown panels ---
   .tah = dropdown off a horizontal (top-level) item
   .tav = nested vertical submenu (opens off a dropdown item)
   Both are positioned absolutely and toggled display by Hubb's JS.

   Each dropdown contains a <table class="t"> whose rows have 4 cells:
     1. td.t_indentcol  --  indent whitespace (hidden)
     2. td.t            --  contains <div class="t">Label</div>
     3. td.t            --  ">>" submenu indicator (only for parent items)
     4. td.t            --  hosts nested .tav (when item has children)
   Separator <tr>s (without class="t") are hidden entirely.             */

.main-nav .tah,
.main-nav .tav {
  background:    var(--surface);
  border:        1px solid var(--rule);
  box-shadow:    0 12px 28px -12px rgba(26, 22, 18, 0.18);
  padding:       var(--space-2);
  border-radius: var(--radius);
  min-width:     220px;
  font-family:   var(--font-body);
}

/* CSS-driven position override.
   Hubb's JS calculates pixel-based top/left for dropdowns by walking
   the offsetParent chain. We've converted the menu's tables to flex,
   which breaks the chain it assumes -- so its numbers land far from
   the parent menu item. We override those inline styles entirely:
   .tah pins to bottom-left of its parent menu cell.
   .tav pins to top-right of its parent dropdown row.
   Hubb's JS is still in charge of show/hide via display:none/block. */
.main-nav .tah {
  top:    100% !important;
  left:   0    !important;
  bottom: auto !important;
  right:  auto !important;
}

/* Each clickable dropdown row becomes the positioning context for
   any nested .tav submenu inside it. The .tab-terms-row class is
   matched alongside [onclick] because our injected Terms & Conditions
   row uses addEventListener for its click (not an inline onclick),
   so the attribute selector wouldn't otherwise hit it. (v0.224) */
.main-nav .tah tr[onclick],
.main-nav .tav tr[onclick],
.main-nav .tah tr.tab-terms-row {
  position: relative;
}

.main-nav .tav {
  top:    0    !important;
  left:   100% !important;
  bottom: auto !important;
  right:  auto !important;
}

/* Nested flyout -- pull up so the first item aligns with its parent row.
   Hubb's JS positions .tav level with the parent; our own padding-top
   and border would otherwise push the first item 9px lower. */
.main-nav .tav {
  margin-top: calc(-1 * var(--space-2) - 1px);
}

/* Empty dropdown panels (items with no children, e.g. "My Account") --
   strip the visual treatment so no ghost box appears on hover. Using
   :not(:has(tr[onclick])) means "this .tah has zero clickable rows". */
.main-nav .tah:not(:has(tr[onclick])),
.main-nav .tav:not(:has(tr[onclick])) {
  padding:    0;
  border:     0;
  background: transparent;
  box-shadow: none;
  min-width:  0;
}

/* Flatten the inner table structure */
.main-nav .tah > table,
.main-nav .tah > table > tbody,
.main-nav .tav > table,
.main-nav .tav > table > tbody {
  display:         block;
  width:           100%;
  border-collapse: collapse;
  border-spacing:  0;
}

/* Hide separator rows -- they're the ones that contain a
   td.t_separator_horizontal. Do NOT use :not(.t) here, because Hubb's
   dropdown JS temporarily swaps the tr's class from "t" to "t_hi" on
   hover -- that caused the hovered row to briefly disappear. */
.main-nav .tah tr:has(td.t_separator_horizontal),
.main-nav .tav tr:has(td.t_separator_horizontal) { display: none; }

/* Each clickable item row -- identified by the onclick attribute, which
   is stable (unlike class, which toggles during hover). The
   .tab-terms-row class is matched alongside it because our injected
   Terms & Conditions row has no onclick attribute (v0.224). */
.main-nav .tah tr[onclick],
.main-nav .tav tr[onclick],
.main-nav .tah tr.tab-terms-row {
  display:       flex;
  align-items:   center;
  width:         100%;
  cursor:        pointer;
  border-radius: var(--radius);
  transition:    background var(--transition);
}
.main-nav .tah tr[onclick]:hover,
.main-nav .tav tr[onclick]:hover,
.main-nav .tah tr.tab-terms-row:hover { background: var(--accent-soft); }

/* 1st td: indent whitespace -- hide */
.main-nav .tah tr[onclick] > td:nth-child(1),
.main-nav .tav tr[onclick] > td:nth-child(1),
.main-nav .tah tr.tab-terms-row > td:nth-child(1) { display: none; }

/* 2nd td: the label -- expand to fill row */
.main-nav .tah tr[onclick] > td:nth-child(2),
.main-nav .tav tr[onclick] > td:nth-child(2),
.main-nav .tah tr.tab-terms-row > td:nth-child(2) {
  flex:        1;
  padding:     var(--space-2) var(--space-3);
  font-size:   var(--text-sm);
  color:       var(--ink);
  font-weight: 500;
  line-height: 1.2;
  white-space: nowrap;
}

/* Inner <div class="t"> inside label cell -- pass through parent styles */
.main-nav .tah td > div.t,
.main-nav .tav td > div.t {
  padding:     0;
  color:       inherit;
  font:        inherit;
  line-height: inherit;
}

/* 3rd td: ">>" submenu indicator (visible only on parent items) */
.main-nav .tah tr[onclick] > td:nth-child(3),
.main-nav .tav tr[onclick] > td:nth-child(3),
.main-nav .tah tr.tab-terms-row > td:nth-child(3) {
  padding:   var(--space-2) var(--space-3);
  color:     var(--ink-soft);
  font-size: var(--text-xs);
}

/* 4th td: hosts the nested .tav -- zero padding so its absolutely-
   positioned child doesn't create flow weirdness */
.main-nav .tah tr[onclick] > td:nth-child(4),
.main-nav .tav tr[onclick] > td:nth-child(4),
.main-nav .tah tr.tab-terms-row > td:nth-child(4) { padding: 0; }

/* v0.226  Force the parent dropdown panel to stay visible while our injected
   Terms & Conditions row is hovered.

   Why: Hubb's DropDownMenu.js fires a ~1s close timer after the cursor leaves
   the previous row's mo() handler. Our row's addEventListener-bound mouseover
   doesn't cleanly cancel that timer (mi() is being called with an id Hubb
   doesn't recognise so its timer-clear path probably bails before running) --
   the result is the dropdown disappearing from under the cursor while it's
   still on the row, which Adam correctly flagged as bad UX.

   This rule sidesteps the whole question of Hubb's internals: while the row
   is :hover, we hold the panel in display: block regardless of any inline
   display: none Hubb's JS applies. Inline styles beat normal stylesheet rules
   but lose to !important, so this wins the cascade.

   The :has() selector is widely supported (Chrome/Edge 105+, Safari 15.4+,
   Firefox 121+) and used in 160+ other places in this file already. */
.main-nav .tah:has(.tab-terms-row:hover) {
  display: block !important;
}


/* -------------------------------------------------------------------------
   3.5b  Priority+ overflow nav ("More" menu)  --  desktop only
   -------------------------------------------------------------------------
   Problem this solves: the horizontal nav (table#m, a nowrap flex row) has
   no idea how many items will fit. When a church adds many items or long
   labels, the cells shrink and their labels wrap to two lines (looks broken)
   in the band between "everything fits" and the 1400px hamburger breakpoint.

   initPriorityNav() in script.js measures what fits on one line and moves
   the overflow into an auto "More" dropdown it builds and controls. It runs
   ONLY above 1400px (below that the mobile drawer takes over, so the ranges
   align with no dead zone). Every rule here is gated on
   .main-nav.nav--priority, which the JS adds only when the feature is live --
   so if the JS ever fails the nav falls back to its old behaviour instead of
   overflowing. Overflowed items are not removed (Hubb's JS and the mobile
   builder still need them); they are hidden with .nav--hidden and re-created
   as plain links inside the More panel.                                    */

/* When live, items keep their natural width (no shrink) and never wrap their
   label -- this is what makes one-line measurement valid. Specificity is one
   class above the base nav rules so it wins only while .nav--priority is on. */
.main-nav.nav--priority #m td[class*="t_horizontal"] { flex-shrink: 0; }
.main-nav.nav--priority #m td[class*="t_horizontal"] > div.t { white-space: nowrap; }

/* Items the JS has pushed into the More panel. !important beats the flex
   display rule on the t_horizontal cells (that rule carries an id). Hidden
   cells cannot be hovered, so Hubb's hover JS never swaps their class. */
.main-nav #m td.nav--hidden { display: none !important; }

/* The More control -- a plain cell appended as the last item in the row.
   Deliberately NOT given the t_horizontal class: that rule is id-scoped
   (#m ...) and would override the show/hide below. */
.main-nav .nav-more {
  display:     none;            /* shown by JS only when there is overflow */
  position:    relative;        /* positioning context for the panel */
  align-items: center;
}
.main-nav .nav-more.is-active { display: flex; }
.main-nav .nav-more__btn {
  display:        inline-flex;
  align-items:    center;
  gap:            var(--space-1);
  font-family:    var(--font-body);
  font-size:      var(--text-sm);
  font-weight:    500;
  letter-spacing: 0.02em;
  color:          var(--ink);
  background:     transparent;
  border:         0;
  padding:        var(--space-2) 0;
  cursor:         pointer;
  transition:     color var(--transition);
}
.main-nav .nav-more__btn:hover,
.main-nav .nav-more.is-open .nav-more__btn { color: var(--accent); }
/* Chevron caret that flips when the panel is open. */
.main-nav .nav-more__chevron {
  width:         0.4em;
  height:        0.4em;
  border-right:  1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform:     rotate(45deg) translateY(-1px);
  transition:    transform var(--transition);
}
.main-nav .nav-more.is-open .nav-more__chevron { transform: rotate(225deg) translateY(2px); }

/* Panel -- same visual treatment as a Hubb .tah dropdown, pinned to the
   bottom-right of the More button (it sits at the right end of the row). */
.main-nav .nav-more__panel {
  position:      absolute;
  top:           100%;
  right:         0;
  left:          auto;
  min-width:     240px;
  max-width:     min(92vw, 340px);
  margin-top:    var(--space-2);
  background:    var(--surface);
  border:        1px solid var(--rule);
  box-shadow:    0 12px 28px -12px rgba(26, 22, 18, 0.18);
  padding:       var(--space-2);
  border-radius: var(--radius);
  font-family:   var(--font-body);
  opacity:       0;
  visibility:    hidden;
  transform:     translateY(6px);
  transition:    opacity var(--transition), transform var(--transition), visibility var(--transition);
  z-index:       60;
  max-height:    min(70vh, 560px);
  overflow-y:    auto;          /* long overflow lists stay reachable */
}
.main-nav .nav-more.is-open .nav-more__panel { opacity: 1; visibility: visible; transform: translateY(0); }
.main-nav .nav-more__panel ul { list-style: none; margin: 0; padding: 0; }
.main-nav .nav-more__link {
  display:         block;
  padding:         var(--space-2) var(--space-3);
  border-radius:   var(--radius);
  font-size:       var(--text-sm);
  color:           var(--ink);
  text-decoration: none;
  transition:      background var(--transition), color var(--transition);
}
.main-nav .nav-more__link:hover { background: var(--rule); color: var(--accent); }
/* Grouped overflow item: a non-link heading with its children listed under it
   (the "flat grouped" presentation -- no nested hover flyouts). */
.main-nav .nav-more__group + .nav-more__group { margin-top: var(--space-2); }
.main-nav .nav-more__group-title {
  display:        block;
  padding:        var(--space-2) var(--space-3) var(--space-1);
  font-size:      var(--text-xs);
  font-weight:    700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color:          var(--ink-soft);
}
.main-nav .nav-more__group .nav-more__link { padding-left: var(--space-5); }
/* Visible keyboard focus. */
.main-nav .nav-more__btn:focus-visible,
.main-nav .nav-more__link:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 2px;
  border-radius:  var(--radius);
}



/* -------------------------------------------------------------------------
   3.6  MODULE: Articles List  --  variant .al-cards
   -------------------------------------------------------------------------
   Settings -> CSS Class = "al-cards"
   Targets the "Universal Article List" mini-layout.

   USAGE in Body Contents -- wrap the module so it gets the heading and
   constrained width:

     <section class="latest-news">
       <h2 class="latest-news__heading">Latest News</h2>
       <module alt="Latest Articles" module_id="..." type_id="1" />
     </section>

   GRID BEHAVIOUR
   - Default (>=1025px): 4 columns
   - 541-1024px:        2 columns
   - <=540px:            1 column
   Breakpoints chosen so we only ever get 4-of-4, 2-of-2, or 1-of-1
   (never 3 + 1 orphan rows). Mobile-only adjustments live in section 4.

   "Read more" is plain text in Hubb's HTML (not a link). We make the
   whole card clickable using a stretched-link pseudo-element on the
   .articleListTitle <a>, so taps anywhere on the card route to the
   article URL the title already points to.
   ------------------------------------------------------------------------- */

/* ---- Section wrapper (heading + max-width container)
   .slice--latest-news is the v0.130 canonical class; .latest-news
   kept as alias for backward compatibility with pre-v0.130 Body
   Contents HTML. The .slice base rule (section 2.5) provides the
   same max-width + margin-block already, so this rule is mostly
   defensive belt-and-braces in case authors omit the .slice base
   class when copying old patterns. Alias removable in v0.131. */
.slice--latest-news,
.latest-news {
  max-width:      var(--container-width);
  margin-inline:  auto;
  padding-inline: var(--container-gutter);
  margin-block:   var(--space-16);
}

.latest-news__heading,
.upcoming-events__heading,
.latest-media__heading,
.explore-church__heading,
.shop-section__heading {
  font-family:    var(--font-display);
  /* Section headings sized to hold the page hierarchy: above the -2
     intro variants (text-2xl, max 48px) and below tier-2 headings
     (welcome H1, feature title -- both max 60px). 32-52px range
     gives section breaks real presence at desktop without crowding
     content. Italic serif at 400 weight stays light visually even
     at this size, so the bump reads as confident rather than shouty. */
  font-size:      clamp(2rem, 3.8vw, 3.25rem);
  font-weight:    400;
  letter-spacing: -0.01em;
  line-height:    1.1;
  margin:         0 0 var(--space-8);
  text-align:     center;
  color:          var(--ink);
}

/* Subtle straight line beneath each section heading. Adds visual
   rhythm between major sections without competing with the
   typography. Earlier version used a hand-drawn brush-stroke SVG
   which read as too playful for the brand -- a clean horizontal
   line in the accent colour is more refined. Width scales modestly
   with viewport via clamp() so the rule looks proportionate at any
   heading size. */
.latest-news__heading::after,
.upcoming-events__heading::after,
.latest-media__heading::after,
.explore-church__heading::after,
.shop-section__heading::after {
  content:          "";
  display:          block;
  width:            clamp(60px, 7vw, 88px);
  height:           2px;
  margin:           var(--space-4) auto 0;
  background-color: var(--accent);
  opacity:          0.85;
  border-radius:    1px;
}

/* ---- Card grid ---- */
.al-cards {
  display:               grid;
  grid-template-columns: repeat(4, 1fr);
  gap:                   var(--space-8);
}

.al-cards > div {
  display:        flex;
  flex-direction: column;
  gap:            var(--space-3);
  position:       relative;   /* anchor for the stretched-link overlay */
}

/* --- Image --- */
.al-cards .articleListImage {
  overflow:      hidden;
  border-radius: var(--radius);
  background:    var(--accent-soft);
  aspect-ratio:  4 / 3;
}
.al-cards .articleListImage a { display: block; height: 100%; }
.al-cards .articleListImage img {
  width:      100%;
  height:     100%;
  object-fit: cover;
  transition: transform 600ms ease;
}
@media (hover: hover) {
  .al-cards > div:hover .articleListImage img { transform: scale(1.04); }
}

/* --- Title (also the stretched-link host) --- */
.al-cards .articleListTitle {
  font-family:    var(--font-display);
  font-size:      var(--text-lg);
  font-weight:    500;
  line-height:    1.2;
  letter-spacing: -0.01em;
}
:is(.al-cards, .al-cards-feature, .al-cards-rail) .articleListTitle a {
  color:               var(--ink);
  text-decoration:     none;
  background-image:    linear-gradient(currentColor, currentColor);
  background-size:     0% 1px;
  background-repeat:   no-repeat;
  background-position: 0 100%;
  /* small gap so the hover underline clears the descenders. The smaller news
     card titles (text-base) read cramped without it; events/media titles are
     larger and already have room, so this is scoped to news only. */
  padding-bottom:      2px;
  transition:          background-size 300ms ease;
}
/* Stretched-link overlay -- invisible, covers the whole card */
.al-cards .articleListTitle a::before {
  content:   "";
  position:  absolute;
  inset:     0;
  z-index:   1;
}
/* Hovering anywhere on the card is treated as hovering the title link */
@media (hover: hover) {
  :is(.al-cards, .al-cards-feature, .al-cards-rail) > div:hover .articleListTitle a { background-size: 100% 1px; }
}

/* --- Summary --- */
.al-cards .articleListSummary {
  color:       var(--ink-soft);
  font-size:   var(--text-sm);
  line-height: 1.55;
}

/* --- "Read more" -- text in HTML, styled as a CTA tag with hover state --- */
.al-cards .articleListLink {
  margin-top:     auto;
  padding-top:    var(--space-3);
  font-size:      var(--text-xs);
  font-weight:    700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color:          var(--accent);
  display:        flex;
  align-items:    center;
  gap:            var(--space-3);
  transition:     color 200ms ease;
}
.al-cards .articleListLink::before {
  content:    "";
  width:      var(--space-6);
  height:     1px;
  background: var(--accent);
  transition: width 200ms ease, background-color 200ms ease;
}
@media (hover: hover) {
  .al-cards > div:hover .articleListLink::before { width: var(--space-12); }
}

/* =========================================================================
   3.6c  MODULE: Articles List -- Feature + List variant  --  .al-cards-feature
   -------------------------------------------------------------------------
   Settings -> CSS Class = "al-cards-feature"
   Editorial front-page layout for the SAME Hubb Articles List module as
   3.6 (type_id 1, same .articleList* anchors -- see MODULES.md). The first
   article becomes a large lead story (big image, headline, excerpt) in the
   left column; the rest stack as compact image-left rows on the right.

   WHY grid-row: 1 / span 50 on the lead: Hubb renders every article as a
   flat sibling .al-cards-feature > div with no grouping wrapper, and the
   article count is unknown at author time. Spanning the lead across far
   more rows than will ever exist (with grid-auto-rows: min-content so empty
   rows collapse to 0) stretches the lead column to exactly the height of
   the stacked list, whatever the count. grid-row: 1 / -1 does NOT work
   here: with no explicit grid-template-rows, -1 resolves to line 2 and the
   lead would span a single row only.
   ------------------------------------------------------------------------- */

.al-cards-feature {
  display:               grid;
  grid-template-columns: 1.6fr 1fr;
  column-gap:            var(--space-8);
  /* row-gap MUST be 0: the lead spans 50 rows (see WHY below) and any
     row-gap is applied between ALL 50 tracks, stacking ~46 phantom gaps
     of empty space below the content. Spacing between the secondary rows
     is handled by padding-block on the cards instead. */
  row-gap:               0;
  grid-auto-rows:        min-content;
  align-items:           start;
  margin-bottom:         var(--space-12);  /* breathing room before section-link */
}

/* Title link + whole-card stretched link (same behaviour as 3.6, re-declared
   because the variant uses a different wrapper class). */
.al-cards-feature .articleListTitle a {
  color:           var(--ink);
  text-decoration: none;
}
.al-cards-feature .articleListTitle a::before {
  content:  "";
  position: absolute;
  inset:    0;
  z-index:  1;            /* taps anywhere on the card open the article */
}
@media (hover: hover) {
  .al-cards-feature > div:hover .articleListImage img { transform: scale(1.04); }
}

/* ---- Lead story (first article) ---- */
.al-cards-feature > div:first-child {
  grid-column:    1;
  grid-row:       1 / span 50;   /* see WHY above */
  display:        flex;
  flex-direction: column;
  gap:            var(--space-4);
  position:       relative;      /* stretched-link anchor */
}
.al-cards-feature > div:first-child .articleListImage {
  overflow:      hidden;
  border-radius: var(--radius);
  background:    var(--accent-soft);
  aspect-ratio:  16 / 10;
  flex:          1 1 auto;       /* grow to fill the lead column height */
  min-height:    260px;
}
.al-cards-feature > div:first-child .articleListImage a { display: block; height: 100%; }
.al-cards-feature > div:first-child .articleListImage img {
  width:      100%;
  height:     100%;
  object-fit: cover;
  transition: transform 600ms ease;
}
.al-cards-feature > div:first-child .articleListTitle {
  font-family:    var(--font-display);
  font-size:      var(--text-2xl);
  font-weight:    500;
  line-height:    1.15;
  letter-spacing: -0.02em;
}
.al-cards-feature > div:first-child .articleListSummary {
  color:              var(--ink-soft);
  font-size:          var(--text-base);
  line-height:        1.55;
  display:            -webkit-box;
  -webkit-line-clamp: 4;         /* keep the lead excerpt tidy */
  line-clamp:         4;
  -webkit-box-orient: vertical;
  overflow:           hidden;
}

/* Lead read-more: same accent CTA tag as the base 3.6 grid (re-declared
   for this wrapper class). The card stretched-link still does the click;
   this is the visible affordance. */
.al-cards-feature > div:first-child .articleListLink {
  margin-top:     var(--space-2);
  font-size:      var(--text-xs);
  font-weight:    700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color:          var(--accent);
  display:        flex;
  align-items:    center;
  gap:            var(--space-3);
}
.al-cards-feature > div:first-child .articleListLink::before {
  content:    "";
  width:      var(--space-6);
  height:     1px;
  background: var(--accent);
  transition: width 200ms ease;
}
@media (hover: hover) {
  .al-cards-feature > div:first-child:hover .articleListLink::before { width: var(--space-12); }
}

/* ---- Secondary stories (remaining articles) -- compact image-left rows.
   Articles List emits image, title, summary, read-more as flat siblings, so
   each is placed explicitly: the image spans the left column, title / summary
   / read-more stack on the right. A plain 2-col grid would scatter the
   summary under the image. ---- */
.al-cards-feature > div:not(:first-child) {
  grid-column:           2;
  display:               grid;
  grid-template-columns: 104px minmax(0, 1fr);
  column-gap:            var(--space-4);
  row-gap:               var(--space-1);
  align-items:           start;
  position:              relative;
  /* padding-block (not row-gap) carries the inter-row spacing -- see the
     row-gap: 0 note on .al-cards-feature. */
  padding-block:         var(--space-5);
  border-bottom:         1px solid var(--rule);
}
/* First secondary aligns its top edge with the lead; last drops its rule. */
.al-cards-feature > div:nth-child(2) { padding-top: 0; }
.al-cards-feature > div:not(:first-child):last-child { border-bottom: none; }
.al-cards-feature > div:not(:first-child) .articleListImage {
  grid-column:   1;
  grid-row:      1 / 4;        /* span the title / summary / read-more stack */
  align-self:    start;
  /* v0.277: was a hardcoded 104px, matching the DESKTOP track. The mobile
     rule narrows that track to 84px without touching this, so the image
     overhung its own column by 20px into the text. Filling the track
     removes the coupling entirely. */
  width:         100%;
  aspect-ratio:  1 / 1;
  overflow:      hidden;
  border-radius: var(--radius);
  background:    var(--accent-soft);
}
.al-cards-feature > div:not(:first-child) .articleListImage a { display: block; height: 100%; }
.al-cards-feature > div:not(:first-child) .articleListImage img {
  width:      100%;
  height:     100%;
  object-fit: cover;
}
.al-cards-feature > div:not(:first-child) .articleListTitle {
  grid-column: 2;
  grid-row:    1;
  min-width:   0;   /* v0.277: a grid item defaults to min-width:auto, so the
                       clamped summary could force this track past its share */
  font-family: var(--font-display);
  font-size:   var(--text-base);
  font-weight: 500;
  line-height: 1.25;
}
/* Secondary excerpt: short 2-line summary under the title (restored). */
.al-cards-feature > div:not(:first-child) .articleListSummary {
  grid-column:        2;
  grid-row:           2;
  min-width:   0;   /* v0.277: a grid item defaults to min-width:auto, so the
                       clamped summary could force this track past its share */
  color:              var(--ink-soft);
  font-size:          var(--text-sm);
  line-height:        1.5;
  display:            -webkit-box;
  -webkit-line-clamp: 2;
  line-clamp:         2;
  -webkit-box-orient: vertical;
  overflow:           hidden;
}
/* Secondary read-more: accent tag with the same leading rule + hover as the
   lead and base grid, so link treatment is consistent across the library. */
.al-cards-feature > div:not(:first-child) .articleListLink {
  grid-column:    2;
  grid-row:       3;
  min-width:   0;   /* v0.277: a grid item defaults to min-width:auto, so the
                       clamped summary could force this track past its share */
  margin-top:     var(--space-1);
  font-size:      var(--text-xs);
  font-weight:    700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color:          var(--accent);
  display:        inline-flex;
  align-items:    center;
  gap:            var(--space-2);
}
.al-cards-feature > div:not(:first-child) .articleListLink::before {
  content:    "";
  width:      var(--space-4);
  height:     1px;
  background: var(--accent);
  transition: width 200ms ease;
}
@media (hover: hover) {
  .al-cards-feature > div:not(:first-child):hover .articleListLink::before { width: var(--space-8); }
}

/* --- Synthesised "Read more" affordance (al-cards-feature) ---------------
   Hubb's Articles List does NOT emit a read-more element here: the rendered
   DOM is image + title + summary only, no .articleListLink, so the rules
   above never match. The cards are already whole-card stretched links (via
   .al-cards-feature .articleListTitle a::before), so the click already works
   -- only the visible cue was missing. Synthesise it with a ::after placed in
   the slots the absent .articleListLink would have used. The
   :not(:has(.articleListLink)) guard means that if a future Hubb config ever
   DOES emit the real link, we step aside and let the rules above style it --
   no double affordance. pointer-events:none so the cue can't intercept the
   stretched link beneath it. ASCII "->" matches the .feature__cta arrow. */
.al-cards-feature > div:first-child:not(:has(.articleListLink))::after,
.al-cards-feature > div:not(:first-child):not(:has(.articleListLink))::after {
  content:        "Read more ->";
  font-size:      var(--text-xs);
  font-weight:    700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color:          var(--accent);
  pointer-events: none;
}
/* Lead: last item in the flex column, under the excerpt. */
.al-cards-feature > div:first-child:not(:has(.articleListLink))::after {
  margin-top: var(--space-2);
}
/* Secondary: into the reserved col-2 / row-3 slot beside the thumbnail. */
.al-cards-feature > div:not(:first-child):not(:has(.articleListLink))::after {
  grid-column: 2;
  grid-row:    3;
  margin-top:  var(--space-1);
}


/* =========================================================================
   3.6d  MODULE: Articles List -- Horizontal Rail variant  --  .al-cards-rail
   -------------------------------------------------------------------------
   Settings -> CSS Class = "al-cards-rail"
   Swipeable horizontal rail of compact image-left cards, for the SAME Hubb
   Articles List module as 3.6 (type_id 1). Reuses the carousel MECHANIC
   shared by Events 2 / Media 2: setupCarousels() (script.js) wraps the
   track in .carousel-viewport and injects the prev/next buttons + edge
   fades (all generic, defined in 3.10c/3.10d), so this section only adds
   the news-specific grid, intro and image-left card.

   Author pattern in Body Contents (the .latest-news-2__intro block is what
   triggers the rail layout via :has(), exactly like Events 2 / Media 2):

     <section class="slice slice--latest-news">
       <div class="slice__head slice__head--intro latest-news-2__intro">
         <h2 class="slice__heading latest-news-2__heading">Latest News</h2>
         <p  class="slice__tagline latest-news-2__tagline">...</p>
         <a  class="slice__cta latest-news-2__cta" href="...">Read more news</a>
       </div>
       <module alt="Latest News" module_id="..." type_id="1"
               cssClass="al-cards-rail" />
     </section>

   Progressive enhancement: with no JS the track is still a native horizontal
   scroller (swipe / scrollbar / keyboard) -- only the arrow buttons absent.
   ------------------------------------------------------------------------- */

/* Outer 2-column grid: intro | rail viewport. Higher specificity than the
   base .slice--latest-news block rule, so only the rail variant (intro
   present) becomes a grid; the plain 4-col news grid is untouched. */
.slice--latest-news:has(> .latest-news-2__intro),
.latest-news:has(> .latest-news-2__intro) {
  display:               grid;
  grid-template-columns: minmax(220px, 24%) minmax(0, 1fr);
  column-gap:            var(--space-8);
  row-gap:               var(--space-6);
  align-items:           center;
}

/* Intro block (left column). */
.latest-news-2__intro {
  display:        flex;
  flex-direction: column;
  gap:            var(--space-3);
  /* align-items: flex-start REQUIRED: this column inherits align-items:
     center from .slice__head--intro, which shrink-wraps and centres the
     heading (the CTA escapes via its own align-self:start; the tagline is
     masked because it wraps to fill the column). v0.170's text-align:left
     was the wrong layer -- it aligns text inside the box, not the box. */
  align-items:    flex-start;
}
.latest-news-2__heading {
  font-family:    var(--font-display);
  font-size:      var(--text-2xl);
  font-weight:    500;
  line-height:    1.1;
  letter-spacing: -0.02em;
  color:          var(--ink);
  margin:         0;
}
/* Sidebar heading, not a centred section banner: suppress the underline
   ::after ornament carried by the centred-heading group. */
.latest-news-2__heading::after { display: none; }
.latest-news-2__tagline {
  font-family: var(--font-body);
  font-size:   var(--text-base);
  color:       var(--ink-soft);
  line-height: 1.5;
  margin:      0;
}
.latest-news-2__cta {
  font-family:     var(--font-body);
  font-size:       var(--text-sm);
  font-weight:     600;
  color:           var(--ink);
  text-decoration: none;
  margin-top:      var(--space-2);
  align-self:      flex-start;
  display:         inline-flex;
  align-items:     center;
  gap:             var(--space-2);
  transition:      color var(--transition);
}
.latest-news-2__cta::after {
  content:    "\2192";   /* right arrow, escaped for pure-ASCII file */
  font-size:  1.1em;
  transition: transform 200ms ease;
}
@media (hover: hover) {
  .latest-news-2__cta:hover { color: var(--accent); }
  .latest-news-2__cta:hover::after { transform: translateX(4px); }
}

/* Card track -- horizontal scroll-snap rail (mirrors the 3.10c track so the
   scroll feel is identical across all carousels). min-width: 0 lets the
   flex track overflow horizontally instead of stretching the grid column. */
.al-cards-rail {
  display:          flex;
  gap:              var(--space-5);
  overflow-x:       auto;
  scroll-snap-type: x mandatory;
  scroll-behavior:  smooth;
  scrollbar-width:  none;
  min-width:        0;
  padding-bottom:   var(--space-1);
}
.al-cards-rail::-webkit-scrollbar { display: none; }

/* Each card -- image-top, matching the Events 2 / Media 2 rail cards so all
   three rails read as one family. The Articles List children (image, title,
   summary, read-more) are flat siblings, so a flex column stacks them in
   source order and the summary spans the full card width. */
.al-cards-rail > div {
  flex:              0 0 280px;     /* same card width as events/media rails */
  scroll-snap-align: start;
  display:           flex;
  flex-direction:    column;
  gap:               var(--space-3);
  position:          relative;      /* stretched-link anchor */
}
.al-cards-rail .articleListImage {
  overflow:      hidden;
  border-radius: var(--radius);
  background:    var(--accent-soft);
  aspect-ratio:  4 / 3;             /* image on top, full card width */
}
.al-cards-rail .articleListImage a { display: block; height: 100%; }
.al-cards-rail .articleListImage img {
  width:      100%;
  height:     100%;
  object-fit: cover;
  transition: transform 600ms ease;
}
@media (hover: hover) {
  .al-cards-rail > div:hover .articleListImage img { transform: scale(1.04); }
}
.al-cards-rail .articleListTitle {
  font-family:    var(--font-display);
  font-size:      var(--text-base);
  font-weight:    500;
  line-height:    1.25;
  letter-spacing: -0.01em;
}
.al-cards-rail .articleListTitle a {
  color:           var(--ink);
  text-decoration: none;
}
.al-cards-rail .articleListTitle a::before {
  content:  "";
  position: absolute;
  inset:    0;
  z-index:  1;            /* whole card opens the article */
}
.al-cards-rail .articleListSummary {
  color:              var(--ink-soft);
  font-size:          var(--text-sm);
  line-height:        1.5;
  margin-top:         var(--space-2);
  display:            -webkit-box;
  -webkit-line-clamp: 3;
  line-clamp:         3;
  -webkit-box-orient: vertical;
  overflow:           hidden;
}
/* Read-more tag is redundant in the compact rail card. */
.al-cards-rail .articleListLink { display: none; }


/* =========================================================================
   3.6e  MODULE: Articles List -- stacked rows  --  .al-cards-stacked
   -------------------------------------------------------------------------
   Settings -> CSS Class = "al-cards-stacked"
   News version of the stacked "Featured Events" layout (3.10f/3.10g), in
   Tabernacle tokens. Same Articles List module as 3.6 (type_id 1, same
   .articleList* anchors -- see MODULES.md), but a standalone wrapper class,
   so none of the .al-cards base rules apply here. Articles carry no date, so
   there is no calendar badge -- the card is image | title + summary | Read more.

   The whole card opens the article via a stretched-link on the title. The
   "Read more" pill is Hubb's own .articleListLink (plain text, not a link),
   restyled and parked in the third column.

   Width: the stacked layouts share a 75% centred width -- see the shared
   rule at the end of this section.

   Author pattern in Body Contents:
     <section class="slice slice--latest-news">
       <div class="slice__head slice__head--centred">
         <h2 class="slice__heading latest-news__heading">Latest News</h2>
       </div>
       <module alt="Latest Articles" module_id="..." type_id="1"
               cssClass="al-cards-stacked" />
       <a class="section-link" href="/Groups/.../Blog.aspx">Read more news</a>
     </section>
   ------------------------------------------------------------------------- */

/* Vertical stack of cards */
.al-cards-stacked {
  display:        flex;
  flex-direction: column;
  gap:            var(--space-5);
}

/* Each card -- horizontal: image | content | read-more */
.al-cards-stacked > div {
  display:               grid;
  grid-template-columns: minmax(0, 240px) 1fr auto;
  grid-template-rows:    auto auto;
  align-items:           center;
  column-gap:            var(--space-8);
  row-gap:               var(--space-2);
  position:              relative;   /* stretched-link anchor */
  background:            var(--surface);
  border:                1px solid var(--rule);
  border-radius:         var(--radius-lg);
  padding:               var(--space-5);
  transition:            border-color var(--transition);
}
@media (hover: hover) {
  .al-cards-stacked > div:hover { border-color: var(--ink-soft); }
}

/* --- Image (spans the content rows on the left) --- */
.al-cards-stacked .articleListImage {
  grid-column:   1;
  grid-row:      1 / -1;
  align-self:    stretch;
  min-height:    150px;
  overflow:      hidden;
  border-radius: var(--radius);
  background:    var(--accent-soft);
}
.al-cards-stacked .articleListImage a { display: block; height: 100%; }
.al-cards-stacked .articleListImage img {
  width:      100%;
  height:     100%;
  object-fit: cover;
  transition: transform 600ms ease;
}
@media (hover: hover) {
  .al-cards-stacked > div:hover .articleListImage img { transform: scale(1.04); }
}

/* --- Title (row 1) with a short accent rule beneath --- */
.al-cards-stacked .articleListTitle {
  grid-column:    2;
  grid-row:       1;
  font-family:    var(--font-display);
  font-size:      var(--text-xl);
  font-weight:    500;
  line-height:    1.2;
  letter-spacing: -0.01em;
}
.al-cards-stacked .articleListTitle::after {
  content:    "";
  display:    block;
  width:      var(--space-12);
  height:     2px;
  margin-top: var(--space-3);
  background: var(--accent);
}
.al-cards-stacked .articleListTitle a {
  color:           var(--ink);
  text-decoration: none;
}
.al-cards-stacked .articleListTitle a::before {
  content:  "";
  position: absolute;
  inset:    0;
  z-index:  1;            /* whole card opens the article */
}

/* --- Summary (row 2), clamped to two lines --- */
.al-cards-stacked .articleListSummary {
  grid-column:        2;
  grid-row:           2;
  color:              var(--ink-soft);
  font-size:          var(--text-sm);
  line-height:        1.55;
  display:            -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow:           hidden;
}

/* --- "Read more" pill (col 3) -- Hubb's .articleListLink text, restyled as
   a button. The card click does the work, so this is a decorative affordance. */
.al-cards-stacked .articleListLink {
  grid-column:    3;
  grid-row:       1 / -1;
  align-self:     center;
  justify-self:   start;
  white-space:    nowrap;
  font-family:    var(--font-body);
  font-size:      var(--text-xs);
  font-weight:    700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color:          var(--ink);
  padding:        var(--space-3) var(--space-5);
  border:         1px solid var(--rule);
  border-radius:  999px;
  transition:     background var(--transition), border-color var(--transition);
}
@media (hover: hover) {
  .al-cards-stacked > div:hover .articleListLink {
    background:   var(--accent-soft);
    border-color: var(--accent);
  }
}

/* -------------------------------------------------------------------------
   Stacked layouts -- shared width
   The three stacked row layouts (.al-cards-stacked, .upcoming_events_3 and
   .latest_media_4) run the full editorial column by default, which reads too
   wide on desktop. Constrain them to 75% and centre. Reset to full width on
   mobile (where the cards stack vertically anyway -- see Mobile section).
   ------------------------------------------------------------------------- */
.al-cards-stacked,
.upcoming_events_3,
.latest_media_4 .MediaList {
  width:         75%;
  margin-inline: auto;
}


/* =========================================================================
   3.6f  MODULE: Articles List -- 3-up card grid  --  .al-cards-3up
   -------------------------------------------------------------------------
   Settings -> CSS Class = "al-cards al-cards-3up"   (BOTH classes)
   Targets the "Universal Article List" mini-layout, same as .al-cards.

   WHY it exists: .al-cards is a deliberate 4-column grid, and its 4 -> 2 -> 1
   collapse was chosen so a feed of unknown length never leaves an orphan row.
   That is right for news. A curated set of exactly three things (three course
   levels, three service tiers, three venues) sits in it with a quarter-width
   hole. Three across is the most common marketing layout there is, so it
   earns a variant rather than a per-site override.

   USED WITH .al-cards, not instead of it. Every card style -- image ratio,
   title, summary, stretched link, hover -- is inherited, and this variant
   restates only the column count. That keeps it to two rules and means any
   future change to card styling lands here automatically.

   WHY two class selectors: .al-cards.al-cards-3up (0,2,0) outranks the shared
   grid rules in section 4c (0,1,0) wherever they sit in the file, so the
   column count cannot be silently reverted by a later breakpoint.

   The collapse is deliberately 3 -> 1 with NO 2-column step: three items in
   two columns is precisely the 2 + 1 orphan that section 4c exists to avoid.
   It happens at 720px, the library's established phone breakpoint, so no new
   breakpoint is introduced. The rule itself lives in section 4c with the
   other card grid breakpoints. */
.al-cards.al-cards-3up { grid-template-columns: repeat(3, 1fr); }


/* =========================================================================
   3.6b  MODULE: Announcement Banner  --  .announcement_banner  (Article module)
   -------------------------------------------------------------------------
   Settings -> CSS Class = "announcement_banner"

   A slim full-width bar for short-lived announcements (e.g. "Important
   notice -- service moved to 11am this Sunday"). Authored as a Hubb
   Article module so the text + link can be edited in the Web Office
   without touching the template HTML. The cssClass on the module
   determines visual treatment; without it, the article would render
   as a regular block of body text.

   Body Contents pattern -- typical placement is right at the top of
   <main>, between </header> and the hero gallery, so the banner is
   the first thing visitors see:

     </header>
     <main>
       <module alt="Announcement" module_id="..." type_id="10"
               cssClass="announcement_banner" />
       <gallery><module alt="Hero" .../></gallery>
       ...

   It also works lower in the page (e.g. between the hero and welcome
   article) if you want the banner to sit below the visual headline
   rather than above it. The styling doesn't care where it lives.

   Authoring rules in the Web Office:
     - Keep it ONE LINE on desktop (one short sentence + a "click here"
       link). It can wrap to two lines on phones, but a multi-paragraph
       announcement isn't this component -- it's the welcome article.
     - <strong> for emphasis and <a> for the link both work.
     - Don't paste images into the article body; this is a text bar.

   Hubb cruft handled here:
     - .pagebox table inside .ArticleBody (already display:none via
       inline style, but defended in CSS belt-and-braces).
     - .ArticleRotaSignupLink div (sibling of .ArticleBody, empty in
       announcement context but Hubb renders it anyway).
     - Inline style="font-size:NNpx" spans the editor sometimes injects
       around pasted content -- these mess up the bar's typography
       rhythm, so we override them.
   ========================================================================= */

.announcement_banner {
  background:     var(--accent);
  color:          var(--accent-ink);
  font-family:    var(--font-body);
  /* Sized at body copy (--text-base) not small text. The banner
     carries time-sensitive functional information ("service moved
     to 11am this Sunday") -- it's the kind of message users need
     to actually READ, not glance at. Adam flagged that --text-sm
     (clamps to ~15px max) was reading as fine-print, undermining
     the urgency the terracotta background is meant to signal.
     --text-base puts it at body-copy size (~17-18px on desktop)
     so the announcement reads with the weight it deserves. */
  font-size:      var(--text-base);
  font-weight:    500;
  letter-spacing: 0.01em;
  line-height:    1.4;
  padding:        var(--space-3) var(--container-gutter);
  text-align:     center;
}

/* Inner constraint -- text content respects the same max-width as
   the rest of the page so wide announcements stay readable on big
   monitors instead of stretching to 1900px. */
.announcement_banner .ArticleBody {
  max-width:     var(--container-width);
  margin-inline: auto;
}
.announcement_banner .ArticleBody p {
  margin: 0;
}

/* Defeat inline font-size spans that Hubb's article editor sometimes
   inserts around pasted text (e.g. <span style="font-size:18px">).
   The banner's typography rhythm is set on the wrapper; per-character
   overrides break that. */
.announcement_banner .ArticleBody [style*="font-size"] {
  font-size: inherit !important;
}

/* Hide images that might end up in the article body -- this is a
   text bar, not a feature block. Defensive in case an author pastes
   in a rich-formatted snippet. */
.announcement_banner .ArticleBody img {
  display: none;
}

/* Link inside the announcement -- white text with underline. Hover
   gives a subtle opacity fade. v0.152: switched from a
   `text-decoration-thickness: 1px -> 2px` hover change with
   transition on text-decoration-thickness. That property's
   transition support is flaky -- Chrome often paints the change
   instantly despite the `transition` declaration, and the 1px
   delta is too small to perceive even when the transition does
   fire. The result was a hover that snapped instead of easing,
   which Adam flagged as inconsistent with every other hover on
   the site. Opacity transitions reliably everywhere, matches the
   umbrella logo hover (v0.148), and gives a clearly-perceived
   ease-in/out on the terracotta background. */
.announcement_banner .ArticleBody a {
  color:                     inherit;
  text-decoration:           underline;
  text-underline-offset:     3px;
  text-decoration-thickness: 1px;
  font-weight:               600;
  transition:                opacity var(--transition);
}
.announcement_banner .ArticleBody a:hover {
  opacity: 0.75;
}
.announcement_banner .ArticleBody a:focus-visible {
  outline:        2px solid var(--accent-ink);
  outline-offset: 2px;
}

/* Hide Hubb's article cruft */
.announcement_banner .pagebox,
.announcement_banner .ArticleRotaSignupLink {
  display: none;
}


/* =========================================================================
   3.7  MODULE: Hero Slideshow  --  .hero_slideshow  (Gallery module)
   -------------------------------------------------------------------------
   Hubb renders the Gallery module using bxSlider. The markup we're
   styling looks like:

     .hero_slideshow
       .bx-wrapper
         .bx-viewport
           ul.gallery_ul
             li.gallery_li          <- each slide
               img
         .bx-controls
           .bx-pager
             .bx-pager-item
               a.bx-pager-link      <- numbered dots (1, 2, ...)
           .bx-controls-direction
             a.bx-prev              <- text "Prev"
             a.bx-next              <- text "Next"

   The image <img> comes out with alt="" and title="" unless the
   underlying images in Hubb's media library have descriptions set.
   Once they do, the accompanying JS snippet (documented below the
   CSS) reads the alt/title and builds a .hero-caption overlay.

   Caption format supported:
     "Headline*Sub-headline"  ->  split on asterisks (Aspire convention)
     "Headline"               ->  single title line
   ========================================================================= */

/* ---- FOUC prevention -----------------------------------------------------
   bxSlider initializes on $(window).load(), so for a brief moment between
   HTML parse and JS execution the raw <ul><li><img>...</li></ul> renders
   as a stacked list of full-size images -- looks janky as the slider then
   reorganizes everything into place.

   The fix has two parts:
     1. Reserve the slideshow space (background + min-height) so the
        layout doesn't jump when bxSlider takes over.
     2. Hide the raw <ul> until bxSlider has wrapped it with .bx-wrapper.
        We use the :has() selector -- when .hero_slideshow contains
        .bx-wrapper, the wrapper fades in. Until then, the unwrapped
        contents stay invisible against the cream placeholder.
   --------------------------------------------------------------------- */
.hero_slideshow {
  position:   relative;
  background: var(--bg);
  min-height: 520px;
  overflow:   hidden;
}

/* Raw UL -- hidden before bxSlider wraps it. Once .bx-wrapper exists,
   the gallery_ul is no longer a direct child of .hero_slideshow
   (bxSlider nests it inside .bx-wrapper > .bx-viewport), so this
   selector stops matching and content shows. */
.hero_slideshow > .gallery_ul {
  opacity: 0;
}

/* The wrapped slider fades in once present */
.hero_slideshow .bx-wrapper {
  animation: hubb-slider-in 600ms var(--ease-out-soft) both;
}
@keyframes hubb-slider-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Reset bxSlider's default chrome */
.hero_slideshow .bx-wrapper {
  background:   transparent;
  border:       0;
  box-shadow:   none;
  margin:       0;
  max-width:    100% !important;
}

.hero_slideshow .bx-viewport {
  height:       520px !important;
  border:       0;
  box-shadow:   none;
  background:   var(--bg);
  left:         0;
}

/* Slides */
.hero_slideshow .gallery_li {
  height:       520px !important;
  position:     relative;
  overflow:     hidden;
}

.hero_slideshow .gallery_li img {
  width:          100%;
  height:         100%;
  object-fit:     cover;
  object-position: center;
  display:        block;
}

/* Static dark tint overlay -- improves caption legibility against
   bright photos. Always-on (not a hover effect); positioned beneath
   the caption (z-index 1) but above the image. */
.hero_slideshow .gallery_li::after {
  content:        "";
  position:       absolute;
  inset:          0;
  background:     rgba(26, 22, 18, 0.32);
  pointer-events: none;
  z-index:        1;
}

/* Caption -- built by JS from img.alt or img.title. Centred both
   horizontally and vertically over the slide.
   If alt/title are empty, no .hero-caption is created and this
   section renders nothing. Safe to ship. */
.hero_slideshow .hero-caption {
  position:       absolute;
  top:            50%;
  left:           50%;
  transform:      translate(-50%, -50%);
  width:          100%;
  max-width:      720px;
  padding:        0 var(--space-6);
  z-index:        2;
  color:          #FFFFFF;
  text-align:     center;
}

.hero_slideshow .hero-caption__title {
  font-family:    var(--font-display);
  /* Hero is tier-1 in the page hierarchy -- it should own the
     viewport, not read like a section heading. Bumped from a 56px
     max to 80px so "WELCOME" carries real weight on desktop. Cap
     stays under 90px because italic serif strokes get visually
     fragile beyond that and start reading as thin rather than
     confident. The 6.5vw middle term means the text scales smoothly
     across tablet sizes without sudden jumps. */
  font-size:      clamp(2.5rem, 6.5vw, 5rem);
  font-weight:    400;
  line-height:    1.08;
  letter-spacing: -0.01em;
  margin:         0 0 var(--space-3);
  text-shadow:    0 2px 24px rgba(0, 0, 0, 0.35);
}

.hero_slideshow .hero-caption__subtitle {
  font-family:    var(--font-body);
  font-size:      1.125rem;
  font-weight:    400;
  line-height:    1.45;
  margin:         0;
  opacity:        0.95;
  text-shadow:    0 1px 12px rgba(0, 0, 0, 0.4);
}

/* ---- Hero CTA button --------------------------------------------------
   Same pill-with-arrow pattern as the welcome article (.ArrowToRight)
   and the feature section (.feature__cta), so all three primary CTAs
   on the homepage feel like one family of button. JS adds this only
   when the third asterisk-separated part of the alt text is present
   (e.g. "Welcome*Subtitle text*Visitor Information"). */
.hero_slideshow .hero-caption__cta {
  display:         inline-flex;
  align-items:     center;
  gap:             var(--space-3);
  margin-top:      var(--space-6);
  padding:         var(--space-3) var(--space-6);
  background:      var(--accent);
  color:           var(--accent-ink);
  font-family:     var(--font-body);
  font-size:       var(--text-xs);
  font-weight:     700;
  letter-spacing:  0.1em;
  text-transform:  uppercase;
  text-decoration: none;
  border-radius:   999px;
  transition:      background 0.2s ease, opacity 0.2s ease;
  /* relative + z-index so the button sits above the .gallery_li::after
     gradient overlay (which is at z-index: 1 / 2) */
  position:        relative;
  z-index:         3;
}
.hero_slideshow .hero-caption__cta::after {
  content:    "->";
  font-size:  1.15em;
  /* v0.143: see .welcome_article .ArrowToRight::after for rationale. */
  transform:  translateY(-0.08em);
  transition: transform 0.2s ease;
}
.hero_slideshow .hero-caption__cta:focus-visible {
  background: var(--accent-dark);
  color:      var(--accent-ink);
  opacity:    1;
}
.hero_slideshow .hero-caption__cta:active {
  background: var(--accent-dark);
  transform:  translateY(1px);   /* tactile press feedback on touch + click */
}
@media (hover: hover) {
  .hero_slideshow .hero-caption__cta:hover {
    background: var(--accent-dark);
    color:      var(--accent-ink);
    opacity:    1;
  }
  .hero_slideshow .hero-caption__cta:hover::after,
  .hero_slideshow .hero-caption__cta:focus-visible::after {
    transform: translate(3px, -0.08em);   /* X slide + preserves baseline Y nudge */
  }
}

/* ---- Hero caption staggered entrance ---------------------------------------
   When bxSlider finishes wrapping the slideshow (i.e. .bx-wrapper exists),
   the caption parts cascade in -- title first, subtitle 150ms later, CTA
   another 150ms after that. Adds polish to the most prominent element of
   the page without needing any JS hook.

   Trigger reuses :has(.bx-wrapper). The existing FOUC-prevention rules
   above already gate slider visibility on bxSlider initialising; using
   the same signal keeps the entrance logic consistent and avoids a
   second mechanism for "is the slider ready yet".

   Why animation rather than transition: the CTA already has transitions
   defined for hover (background colour change). Using a transition for
   the entrance with transition-delay would also delay other state
   changes, so the press-down feedback (transform: translateY(1px) on
   :active) would lag noticeably. Animation runs once and gets out of
   the way.

   Why opacity-only -- no upward CTA translate: animation-fill-mode: both
   would hold whatever transform value the keyframe ends with, which
   conflicts with the existing :active press-down. Opacity is interaction-
   safe -- no pseudo-class state on the caption parts touches it after
   entrance, so there's nothing to fight.

   Animation runs once at slider init. Subsequent slide transitions use
   bxSlider's image fade -- the next slide's caption is already at its
   end state by the time it scrolls into view. Mobile uses the same
   timing; the cascade reads well on phone-sized hero captions and the
   tighter mobile reveal stagger applies to scrolled-in cards, not to
   this above-the-fold hero element.
   ------------------------------------------------------------------------- */
@media (prefers-reduced-motion: no-preference) {
  .hero_slideshow:has(.bx-wrapper) .hero-caption__title {
    animation: hubb-hero-cap-fade-in 600ms var(--ease-out-soft) 200ms both;
  }
  .hero_slideshow:has(.bx-wrapper) .hero-caption__subtitle {
    /* Soft variant ends at 0.95 to match the static rule's slight dim
       against the title (hierarchy cue). animation-fill-mode: both
       holds the end opacity, so the keyframe end must match what the
       static rule would otherwise leave it at. */
    animation: hubb-hero-cap-fade-in-soft 600ms var(--ease-out-soft) 350ms both;
  }
  .hero_slideshow:has(.bx-wrapper) .hero-caption__cta {
    animation: hubb-hero-cap-fade-in 600ms var(--ease-out-soft) 500ms both;
  }
}
@keyframes hubb-hero-cap-fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes hubb-hero-cap-fade-in-soft {
  from { opacity: 0;    }
  to   { opacity: 0.95; }
}

/* ---- Controls container ------------------------------------------------- */
.hero_slideshow .bx-controls {
  /* bxSlider positions this after the viewport; we overlay it on top */
  position:       absolute;
  inset:          0;
  pointer-events: none;
  z-index:        3;
}

/* ---- Pager dots --------------------------------------------------------- */
.hero_slideshow .bx-pager {
  position:       absolute;
  left:           50%;
  bottom:         var(--space-4);
  transform:      translateX(-50%);
  display:        flex;
  gap:            var(--space-2);
  width:          auto;
  pointer-events: auto;
  text-align:     center;
}

.hero_slideshow .bx-pager-item {
  display:        inline-block;
  line-height:    0;
  margin:         0 var(--space-2);  /* spacing between hit targets */
}

/* Pager link is the actual click target -- sized 24x24 to satisfy
   WCAG 2.5.8 minimum tap target. The visible dot is rendered via
   ::before so it stays a small accent regardless of the hit-area
   size around it. Earlier version used the link itself as a 10x10
   visual + hit target which failed Lighthouse target-size audit
   (mobile users couldn't reliably tap a 10px target). */
.hero_slideshow .bx-pager-link {
  display:         inline-flex;
  align-items:     center;
  justify-content: center;
  width:           24px;
  height:          24px;
  padding:         0;
  background:      transparent;
  border:          0;
  cursor:          pointer;
  font-size:       0;
  text-indent:     -9999px;
  position:        relative;
}
.hero_slideshow .bx-pager-link::before {
  content:        "";
  display:        block;
  width:          10px;
  height:         10px;
  border-radius:  50%;
  background:     rgba(255, 255, 255, 0.5);
  transition:     background 0.25s ease, width 0.25s ease, border-radius 0.25s ease;
}

.hero_slideshow .bx-pager-link:hover::before {
  background:     rgba(255, 255, 255, 0.85);
}

.hero_slideshow .bx-pager-link.active::before {
  background:     #FFFFFF;
  width:          28px;
  border-radius:  5px;
}

/* ---- Prev / Next arrows ------------------------------------------------- */
.hero_slideshow .bx-controls-direction {
  pointer-events: none;
}

.hero_slideshow .bx-prev,
.hero_slideshow .bx-next {
  position:         absolute;
  top:              50%;
  width:            48px;
  height:           48px;
  border-radius:    50%;
  background:       rgba(255, 255, 255, 0.9);
  color:            var(--ink);
  text-decoration:  none;
  text-indent:      -9999px;
  font-size:        0;
  pointer-events:   auto;
  transform:        translateY(-50%);
  transition:       background 0.2s ease, transform 0.2s ease;
  z-index:          4;
}

.hero_slideshow .bx-prev { left:  var(--space-4); }
.hero_slideshow .bx-next { right: var(--space-4); }

.hero_slideshow .bx-prev:hover,
.hero_slideshow .bx-next:hover {
  background:       #FFFFFF;
  transform:        translateY(-50%) scale(1.05);
}

/* Chevrons drawn with borders -- no icon font, no SVG needed */
.hero_slideshow .bx-prev::before,
.hero_slideshow .bx-next::before {
  content:          "";
  position:         absolute;
  top:              50%;
  left:             50%;
  width:            10px;
  height:           10px;
  border-top:       2px solid currentColor;
  border-right:     2px solid currentColor;
}

.hero_slideshow .bx-prev::before {
  transform:        translate(-35%, -50%) rotate(-135deg);
}

.hero_slideshow .bx-next::before {
  transform:        translate(-65%, -50%) rotate(45deg);
}


/* =========================================================================
   3.7c  MODULE: Hero Banner  --  .slice--hero-banner
   -------------------------------------------------------------------------
   A full-bleed cover hero: a dark background image runs edge to edge with a
   left-aligned overlay (headline, short intro, two buttons). Built the
   Giving-Advert way -- the client edits content in the site manager only:

     ONE Gallery module (type 84) -> the background, bound to a media folder
       (e.g. "hero 2"). Keep HasTitleBar/HasBackground OFF and set NO CSS
       Class -- this slice is scoped by the section + .hero-banner__bg wrapper,
       not a module class, so it never picks up the .hero_slideshow styling.
       SlideShowMode OFF renders a single static image (a plain backdrop, no
       controls); turn it ON to run several images -- the dots/arrows auto-hide
       whenever there is only one image (see .bx-controls rule below).
     ONE Article module (type 10) -> the copy: a Heading (headline), a
       paragraph (intro), then a paragraph holding two links (the buttons).
       First link = primary (filled), second = secondary (outline). Editing
       text/links in the GUI can't break the layout.

   A left-weighted scrim keeps white text legible over any image. Buttons use
   the global --accent tokens, so a branded site re-skins with no structural
   edit.
   ------------------------------------------------------------------------- */
.slice--hero-banner {
  max-width:      none;
  padding-inline: 0;
  margin-inline:  calc(50% - 50vw);    /* full-bleed band */
  margin-block:   0;                   /* opt out of the base .slice vertical
                                          rhythm -- that margin sits OUTSIDE the
                                          section, so the page white shows through
                                          above/below the dark band otherwise */
  position:       relative;
  isolation:      isolate;             /* own stacking context: bg < scrim < copy */
  overflow:       hidden;
  display:        flex;
  align-items:    center;              /* vertically centre the copy */
  min-height:     clamp(420px, 62vh, 640px);
  background:     var(--section-dark); /* fallback before the image paints */

  /* Re-skin surface: the primary button defaults to WHITE so it contrasts on
     the dark image; a branded site points --hero-btn-bg at its accent instead. */
  --hero-btn-bg:      #ffffff;
  --hero-btn-ink:     var(--ink);
  --hero-btn2-border: rgba(255, 255, 255, 0.6);
}

/* ---- Background layer: the Gallery, stretched to cover the band ---- */
.slice--hero-banner .hero-banner__bg {
  position: absolute;
  inset:    0;
  z-index:  0;
  overflow: hidden;
}
/* The Gallery module's own wrapper div must carry full height, or the bxSlider
   chain (module div > .bx-wrapper > .bx-viewport > ul > li) can't resolve
   height:100% down to the image -- bxSlider sets the viewport to height:0
   inline, so without a resolved height at every level the cover image collapses
   and the band ends up sized by the image's aspect ratio instead. */
.slice--hero-banner .hero-banner__bg > div { height: 100%; }
.slice--hero-banner .hero-banner__bg .gallery_ul {
  margin: 0; padding: 0; list-style: none; height: 100%;
}
.slice--hero-banner .hero-banner__bg .gallery_li {
  height: 100%; margin: 0; position: relative;
}
.slice--hero-banner .hero-banner__bg img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; display: block;
}
/* bxSlider (SlideShowMode ON) wraps the ul -- make the slider fill the band. */
.slice--hero-banner .hero-banner__bg .bx-wrapper,
.slice--hero-banner .hero-banner__bg .bx-viewport {
  height: 100% !important;
  margin: 0; border: 0; box-shadow: none;
  background: transparent; max-width: 100% !important;
}
/* Single image -> drop the slideshow chrome. bxSlider builds one .bx-pager-item
   per slide; with no 2nd item, hide all controls so it reads as a plain image.
   With 2+ images the controls appear. */
.slice--hero-banner .bx-controls { display: none; }
.slice--hero-banner .bx-controls:has(.bx-pager-item:nth-child(2)) { display: block; }

/* Left-weighted scrim (above the image, below the copy). Absolute, so it does
   not become a flex item. */
.slice--hero-banner::before {
  content: "";
  position: absolute; inset: 0;
  z-index: 1;
  pointer-events: none;
  background: linear-gradient(90deg,
                rgba(0, 0, 0, 0.72) 0%,
                rgba(0, 0, 0, 0.46) 45%,
                rgba(0, 0, 0, 0.12) 100%);
}

/* ---- Copy overlay = the Article module -------------------------------- */
/* The article module's wrapper is the flex child that is NOT the bg div. */
.slice--hero-banner > div:not(.hero-banner__bg) {
  position: relative;
  z-index:  2;
  width:    100%;
}
.slice--hero-banner .ArticleRotaSignupLink { display: none; }

/* The Article module prints the article's Title as the first <h1> in the body,
   ahead of the author's content. In the hero we want the Title to stay a plain
   internal label (e.g. "Homepage: Hero Banner") and the headline to be the
   content Heading, so hide that first, module-injected heading. The client's
   own headline is the SECOND element and is untouched. Keep the article Title
   distinct from the on-screen headline for this to read correctly. */
.slice--hero-banner .ArticleBody > h1:first-child { display: none; }

.slice--hero-banner .ArticleBody {
  max-width:      var(--container-width);
  margin-inline:  auto;
  padding-inline: var(--container-gutter);
  color:          #ffffff;
}
/* Keep the copy to a readable left-hand column. */
.slice--hero-banner .ArticleBody > * { max-width: 620px; }

.slice--hero-banner .ArticleBody :is(h1, h2) {
  margin:         0 0 var(--space-5);
  font-family:    var(--font-display);
  font-size:      clamp(2.4rem, 4.5vw + 0.5rem, 4rem);
  font-weight:    400;
  line-height:    1.05;
  letter-spacing: -0.01em;
  color:          #ffffff;
  text-shadow:    0 2px 24px rgba(0, 0, 0, 0.35);
}
/* Intro paragraph (any paragraph that is NOT the buttons row). */
.slice--hero-banner .ArticleBody p {
  margin:      0 0 var(--space-6);
  max-width:   540px;
  font-family: var(--font-body);
  font-size:   clamp(1.05rem, 1vw + 0.9rem, 1.25rem);
  line-height: 1.5;
  color:       rgba(255, 255, 255, 0.9);
}

/* Buttons row: the paragraph that contains links. */
.slice--hero-banner .ArticleBody p:has(a) {
  display:   flex;
  flex-wrap: wrap;
  gap:       var(--space-4);
  margin:    var(--space-6) 0 0;
  max-width: none;
}
.slice--hero-banner .ArticleBody p:has(a) a {
  display:         inline-flex;
  align-items:     center;
  padding:         var(--space-4) var(--space-8);
  border-radius:   999px;
  font-family:     var(--font-body);
  font-weight:     700;
  font-size:       var(--text-sm);
  letter-spacing:  0.01em;
  text-decoration: none;
  transition:      background var(--transition), color var(--transition), border-color var(--transition);
}
/* Primary = first link (filled accent). */
.slice--hero-banner .ArticleBody p:has(a) a:first-child {
  background: var(--hero-btn-bg);
  color:      var(--hero-btn-ink);
  border:     2px solid var(--hero-btn-bg);
}
/* Secondary = any following link (outline on the dark image). */
.slice--hero-banner .ArticleBody p:has(a) a:not(:first-child) {
  background: transparent;
  color:      #ffffff;
  border:     2px solid var(--hero-btn2-border);
}
.slice--hero-banner .ArticleBody p:has(a) a:focus-visible {
  outline:        3px solid #ffffff;
  outline-offset: 3px;
}
@media (hover: hover) {
  .slice--hero-banner .ArticleBody p:has(a) a:first-child:hover {
    background: rgba(255, 255, 255, 0.88); border-color: rgba(255, 255, 255, 0.88);
  }
  .slice--hero-banner .ArticleBody p:has(a) a:not(:first-child):hover {
    background: #ffffff; color: var(--ink); border-color: #ffffff;
  }
}

/* =========================================================================
   ICON LIBRARY  --  --icon-*  (Lucide v1.24.0, ISC)
   =========================================================================
   Shared repository of line-icons as CSS mask data-URIs. Point any component
   icon token at one, e.g. on an Impact Stats section:
       style="--stat-icon-1: var(--icon-utensils)"
   The glyph is a MASK -- its colour comes from the element background-color,
   so one token recolours it. Browse the full ~1600 set at lucide.dev/icons;
   ask to add any name and it becomes --icon-<name> here.
   ========================================================================= */
:root {
  --icon-users: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M16%2021v-2a4%204%200%200%200-4-4H6a4%204%200%200%200-4%204v2%22%20%2F%3E%20%3Cpath%20d%3D%22M16%203.128a4%204%200%200%201%200%207.744%22%20%2F%3E%20%3Cpath%20d%3D%22M22%2021v-2a4%204%200%200%200-3-3.87%22%20%2F%3E%20%3Ccircle%20cx%3D%229%22%20cy%3D%227%22%20r%3D%224%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-user: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M19%2021v-2a4%204%200%200%200-4-4H9a4%204%200%200%200-4%204v2%22%20%2F%3E%20%3Ccircle%20cx%3D%2212%22%20cy%3D%227%22%20r%3D%224%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-globe: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Ccircle%20cx%3D%2212%22%20cy%3D%2212%22%20r%3D%2210%22%20%2F%3E%20%3Cpath%20d%3D%22M12%202a14.5%2014.5%200%200%200%200%2020%2014.5%2014.5%200%200%200%200-20%22%20%2F%3E%20%3Cpath%20d%3D%22M2%2012h20%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-church: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M10%209h4%22%20%2F%3E%20%3Cpath%20d%3D%22M12%207v5%22%20%2F%3E%20%3Cpath%20d%3D%22M14%2021v-3a2%202%200%200%200-4%200v3%22%20%2F%3E%20%3Cpath%20d%3D%22m18%209%203.52%202.147a1%201%200%200%201%20.48.854V19a2%202%200%200%201-2%202H4a2%202%200%200%201-2-2v-6.999a1%201%200%200%201%20.48-.854L6%209%22%20%2F%3E%20%3Cpath%20d%3D%22M6%2021V7a1%201%200%200%201%20.376-.782l5-3.999a1%201%200%200%201%201.249.001l5%204A1%201%200%200%201%2018%207v14%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-heart: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M2%209.5a5.5%205.5%200%200%201%209.591-3.676.56.56%200%200%200%20.818%200A5.49%205.49%200%200%201%2022%209.5c0%202.29-1.5%204-3%205.5l-5.492%205.313a2%202%200%200%201-3%20.019L5%2015c-1.5-1.5-3-3.2-3-5.5%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-hand-heart: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M11%2014h2a2%202%200%200%200%200-4h-3c-.6%200-1.1.2-1.4.6L3%2016%22%20%2F%3E%20%3Cpath%20d%3D%22m14.45%2013.39%205.05-4.694C20.196%208%2021%206.85%2021%205.75a2.75%202.75%200%200%200-4.797-1.837.276.276%200%200%201-.406%200A2.75%202.75%200%200%200%2011%205.75c0%201.2.802%202.248%201.5%202.946L16%2011.95%22%20%2F%3E%20%3Cpath%20d%3D%22m2%2015%206%206%22%20%2F%3E%20%3Cpath%20d%3D%22m7%2020%201.6-1.4c.3-.4.8-.6%201.4-.6h4c1.1%200%202.1-.4%202.8-1.2l4.6-4.4a1%201%200%200%200-2.75-2.91%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-heart-handshake: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M19.414%2014.414C21%2012.828%2022%2011.5%2022%209.5a5.5%205.5%200%200%200-9.591-3.676.6.6%200%200%201-.818.001A5.5%205.5%200%200%200%202%209.5c0%202.3%201.5%204%203%205.5l5.535%205.362a2%202%200%200%200%202.879.052%202.12%202.12%200%200%200-.004-3%202.124%202.124%200%201%200%203-3%202.124%202.124%200%200%200%203.004%200%202%202%200%200%200%200-2.828l-1.881-1.882a2.41%202.41%200%200%200-3.409%200l-1.71%201.71a2%202%200%200%201-2.828%200%202%202%200%200%201%200-2.828l2.823-2.762%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-handshake: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22m11%2017%202%202a1%201%200%201%200%203-3%22%20%2F%3E%20%3Cpath%20d%3D%22m14%2014%202.5%202.5a1%201%200%201%200%203-3l-3.88-3.88a3%203%200%200%200-4.24%200l-.88.88a1%201%200%201%201-3-3l2.81-2.81a5.79%205.79%200%200%201%207.06-.87l.47.28a2%202%200%200%200%201.42.25L21%204%22%20%2F%3E%20%3Cpath%20d%3D%22m21%203%201%2011h-2%22%20%2F%3E%20%3Cpath%20d%3D%22M3%203%202%2014l6.5%206.5a1%201%200%201%200%203-3%22%20%2F%3E%20%3Cpath%20d%3D%22M3%204h8%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-utensils: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M3%202v7c0%201.1.9%202%202%202h4a2%202%200%200%200%202-2V2%22%20%2F%3E%20%3Cpath%20d%3D%22M7%202v20%22%20%2F%3E%20%3Cpath%20d%3D%22M21%2015V2a5%205%200%200%200-5%205v6c0%201.1.9%202%202%202h3Zm0%200v7%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-coffee: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M10%202v2%22%20%2F%3E%20%3Cpath%20d%3D%22M14%202v2%22%20%2F%3E%20%3Cpath%20d%3D%22M16%208a1%201%200%200%201%201%201v8a4%204%200%200%201-4%204H7a4%204%200%200%201-4-4V9a1%201%200%200%201%201-1h14a4%204%200%201%201%200%208h-1%22%20%2F%3E%20%3Cpath%20d%3D%22M6%202v2%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-clock: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Ccircle%20cx%3D%2212%22%20cy%3D%2212%22%20r%3D%2210%22%20%2F%3E%20%3Cpath%20d%3D%22M12%206v6l4%202%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-calendar: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M8%202v4%22%20%2F%3E%20%3Cpath%20d%3D%22M16%202v4%22%20%2F%3E%20%3Crect%20width%3D%2218%22%20height%3D%2218%22%20x%3D%223%22%20y%3D%224%22%20rx%3D%222%22%20%2F%3E%20%3Cpath%20d%3D%22M3%2010h18%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-book-open: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M12%207v14%22%20%2F%3E%20%3Cpath%20d%3D%22M3%2018a1%201%200%200%201-1-1V4a1%201%200%200%201%201-1h5a4%204%200%200%201%204%204%204%204%200%200%201%204-4h5a1%201%200%200%201%201%201v13a1%201%200%200%201-1%201h-6a3%203%200%200%200-3%203%203%203%200%200%200-3-3z%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-music: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M9%2018V5l12-2v13%22%20%2F%3E%20%3Ccircle%20cx%3D%226%22%20cy%3D%2218%22%20r%3D%223%22%20%2F%3E%20%3Ccircle%20cx%3D%2218%22%20cy%3D%2216%22%20r%3D%223%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-baby: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M10%2016c.5.3%201.2.5%202%20.5s1.5-.2%202-.5%22%20%2F%3E%20%3Cpath%20d%3D%22M15%2012h.01%22%20%2F%3E%20%3Cpath%20d%3D%22M19.38%206.813A9%209%200%200%201%2020.8%2010.2a2%202%200%200%201%200%203.6%209%209%200%200%201-17.6%200%202%202%200%200%201%200-3.6A9%209%200%200%201%2012%203c2%200%203.5%201.1%203.5%202.5s-.9%202.5-2%202.5c-.8%200-1.5-.4-1.5-1%22%20%2F%3E%20%3Cpath%20d%3D%22M9%2012h.01%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-house: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M15%2021v-8a1%201%200%200%200-1-1h-4a1%201%200%200%200-1%201v8%22%20%2F%3E%20%3Cpath%20d%3D%22M3%2010a2%202%200%200%201%20.709-1.528l7-6a2%202%200%200%201%202.582%200l7%206A2%202%200%200%201%2021%2010v9a2%202%200%200%201-2%202H5a2%202%200%200%201-2-2z%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-home: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M15%2021v-8a1%201%200%200%200-1-1h-4a1%201%200%200%200-1%201v8%22%20%2F%3E%20%3Cpath%20d%3D%22M3%2010a2%202%200%200%201%20.709-1.528l7-6a2%202%200%200%201%202.582%200l7%206A2%202%200%200%201%2021%2010v9a2%202%200%200%201-2%202H5a2%202%200%200%201-2-2z%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-gift: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M12%207v14%22%20%2F%3E%20%3Cpath%20d%3D%22M20%2011v8a2%202%200%200%201-2%202H6a2%202%200%200%201-2-2v-8%22%20%2F%3E%20%3Cpath%20d%3D%22M7.5%207a1%201%200%200%201%200-5A4.8%208%200%200%201%2012%207a4.8%208%200%200%201%204.5-5%201%201%200%200%201%200%205%22%20%2F%3E%20%3Crect%20x%3D%223%22%20y%3D%227%22%20width%3D%2218%22%20height%3D%224%22%20rx%3D%221%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-hand-coins: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M11%2015h2a2%202%200%201%200%200-4h-3c-.6%200-1.1.2-1.4.6L3%2017%22%20%2F%3E%20%3Cpath%20d%3D%22m7%2021%201.6-1.4c.3-.4.8-.6%201.4-.6h4c1.1%200%202.1-.4%202.8-1.2l4.6-4.4a2%202%200%200%200-2.75-2.91l-4.2%203.9%22%20%2F%3E%20%3Cpath%20d%3D%22m2%2016%206%206%22%20%2F%3E%20%3Ccircle%20cx%3D%2216%22%20cy%3D%229%22%20r%3D%222.9%22%20%2F%3E%20%3Ccircle%20cx%3D%226%22%20cy%3D%225%22%20r%3D%223%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-sprout: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M14%209.536V7a4%204%200%200%201%204-4h1.5a.5.5%200%200%201%20.5.5V5a4%204%200%200%201-4%204%204%204%200%200%200-4%204c0%202%201%203%201%205a5%205%200%200%201-1%203%22%20%2F%3E%20%3Cpath%20d%3D%22M4%209a5%205%200%200%201%208%204%205%205%200%200%201-8-4%22%20%2F%3E%20%3Cpath%20d%3D%22M5%2021h14%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-graduation-cap: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M21.42%2010.922a1%201%200%200%200-.019-1.838L12.83%205.18a2%202%200%200%200-1.66%200L2.6%209.08a1%201%200%200%200%200%201.832l8.57%203.908a2%202%200%200%200%201.66%200z%22%20%2F%3E%20%3Cpath%20d%3D%22M22%2010v6%22%20%2F%3E%20%3Cpath%20d%3D%22M6%2012.5V16a6%203%200%200%200%2012%200v-3.5%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-map-pin: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M20%2010c0%204.993-5.539%2010.193-7.399%2011.799a1%201%200%200%201-1.202%200C9.539%2020.193%204%2014.993%204%2010a8%208%200%200%201%2016%200%22%20%2F%3E%20%3Ccircle%20cx%3D%2212%22%20cy%3D%2210%22%20r%3D%223%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-sparkles: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M11.017%202.814a1%201%200%200%201%201.966%200l1.051%205.558a2%202%200%200%200%201.594%201.594l5.558%201.051a1%201%200%200%201%200%201.966l-5.558%201.051a2%202%200%200%200-1.594%201.594l-1.051%205.558a1%201%200%200%201-1.966%200l-1.051-5.558a2%202%200%200%200-1.594-1.594l-5.558-1.051a1%201%200%200%201%200-1.966l5.558-1.051a2%202%200%200%200%201.594-1.594z%22%20%2F%3E%20%3Cpath%20d%3D%22M20%202v4%22%20%2F%3E%20%3Cpath%20d%3D%22M22%204h-4%22%20%2F%3E%20%3Ccircle%20cx%3D%224%22%20cy%3D%2220%22%20r%3D%222%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-star: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M11.525%202.295a.53.53%200%200%201%20.95%200l2.31%204.679a2.123%202.123%200%200%200%201.595%201.16l5.166.756a.53.53%200%200%201%20.294.904l-3.736%203.638a2.123%202.123%200%200%200-.611%201.878l.882%205.14a.53.53%200%200%201-.771.56l-4.618-2.428a2.122%202.122%200%200%200-1.973%200L6.396%2021.01a.53.53%200%200%201-.77-.56l.881-5.139a2.122%202.122%200%200%200-.611-1.879L2.16%209.795a.53.53%200%200%201%20.294-.906l5.165-.755a2.122%202.122%200%200%200%201.597-1.16z%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-flame: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M12%203q1%204%204%206.5t3%205.5a1%201%200%200%201-14%200%205%205%200%200%201%201-3%201%201%200%200%200%205%200c0-2-1.5-3-1.5-5q0-2%202.5-4%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-cross: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M4%209a2%202%200%200%200-2%202v2a2%202%200%200%200%202%202h4a1%201%200%200%201%201%201v4a2%202%200%200%200%202%202h2a2%202%200%200%200%202-2v-4a1%201%200%200%201%201-1h4a2%202%200%200%200%202-2v-2a2%202%200%200%200-2-2h-4a1%201%200%200%201-1-1V4a2%202%200%200%200-2-2h-2a2%202%200%200%200-2%202v4a1%201%200%200%201-1%201z%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-mail: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22m22%207-8.991%205.727a2%202%200%200%201-2.009%200L2%207%22%20%2F%3E%20%3Crect%20x%3D%222%22%20y%3D%224%22%20width%3D%2220%22%20height%3D%2216%22%20rx%3D%222%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-send: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M14.536%2021.686a.5.5%200%200%200%20.937-.024l6.5-19a.496.496%200%200%200-.635-.635l-19%206.5a.5.5%200%200%200-.024.937l7.93%203.18a2%202%200%200%201%201.112%201.11z%22%20%2F%3E%20%3Cpath%20d%3D%22m21.854%202.147-10.94%2010.939%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-chevron-right: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22m9%2018%206-6-6-6%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-arrow-right: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M5%2012h14%22%20%2F%3E%20%3Cpath%20d%3D%22m12%205%207%207-7%207%22%20%2F%3E%3C%2Fsvg%3E");
  --icon-bell: url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2024%2024%27%20fill%3D%27none%27%20stroke%3D%27%23000%27%20stroke-width%3D%272%27%20stroke-linecap%3D%27round%27%20stroke-linejoin%3D%27round%27%3E%3Cpath%20d%3D%22M10.268%2021a2%202%200%200%200%203.464%200%22%20%2F%3E%20%3Cpath%20d%3D%22M3.262%2015.326A1%201%200%200%200%204%2017h16a1%201%200%200%200%20.74-1.673C19.41%2013.956%2018%2012.499%2018%208A6%206%200%200%200%206%208c0%204.499-1.411%205.956-2.738%207.326%22%20%2F%3E%3C%2Fsvg%3E");
}

/* -------------------------------------------------------------------------
   3.7d  MODULE: Impact Stats  --  .slice--stats  (Article module, type 10)
   -------------------------------------------------------------------------
   A row of headline statistics: small label, big number, optional one-line
   description, a Lucide line-icon above each, hairline dividers between
   columns. ONE CKEditor article, so the client edits it in the site manager;
   the homepage Body Contents is just:

       <section class="slice slice--stats">
           <module alt="Article" module_id="..." type_id="10" />
       </section>

   Article structure (paste in Source mode -- CKEditor strips classes/styles):
   an optional Heading (the section heading) then a <ul>, one <li> per stat:
     <em>     -> the small label      (e.g. "Total people groups")
     <strong> -> the big number       (e.g. "17,443")
     trailing text after a <br> -> the optional one-line description
   The <br>s only give the client visible line breaks in the editor; they are
   hidden here (the column is a block stack). Add/remove an <li> and the row
   rebalances: grid-auto-flow:column sizes the columns to the item count, so 3
   stats give 3 columns and 4 give 4 -- never an empty cell.

   Icons are CSS masks (Lucide v1.24.0, ISC), one per column by position and
   tinted by --stat-icon-color (defaults to --accent, so a branded site
   recolours with a single token). Swap an icon by overriding --stat-icon-N.
   Composes with band theming: add .slice--light-grey / .slice--dark-grey and
   the text, dividers and icons re-tone automatically (they use --ink /
   --ink-soft / --rule, which the dark band flips).
   ------------------------------------------------------------------------- */
.slice--stats {
  /* Icons per column. Point any --stat-icon-N at a library glyph, e.g.
     var(--icon-utensils); or override per section with an inline style on the
     <section>. Columns beyond 4 fall back to --stat-icon-fallback. */
  --stat-icon-1: var(--icon-users);
  --stat-icon-2: var(--icon-globe);
  --stat-icon-3: var(--icon-hand-heart);
  --stat-icon-4: var(--icon-church);
  --stat-icon-fallback: var(--icon-sparkles);
  --stat-icon-color: var(--accent);     /* one token to re-skin every icon */
  --stat-icon-size:  2.5rem;
}
/* On a dark band the default charcoal --accent would vanish; use the (now
   white) --ink. A branded site whose accent reads on dark can re-point this. */
.slice--stats.slice--dark-grey { --stat-icon-color: var(--ink); }

/* Optional section heading: the client's first heading in the article.
   Centred; collapses when empty so "no heading" leaves no gap. */
.slice--stats .ArticleBody > :is(h1, h2, h3) {
  margin:      0 0 var(--space-8);
  font-family: var(--font-display);
  font-size:   var(--text-2xl);
  font-weight: 700;
  line-height: 1.1;
  text-align:  center;
  color:       var(--ink);
}
.slice--stats .ArticleBody > :is(h1, h2, h3):empty { display: none; }

/* Empty rota-signup artefact the Article module always emits. */
.slice--stats .ArticleRotaSignupLink { display: none; }

/* The row. grid-auto-flow:column keeps every stat on ONE row and sizes the
   columns to the item count -- no empty cells for 3 vs 4. */
.slice--stats .ArticleBody ul {
  list-style: none;
  margin:     0;
  padding:    0;
  display:            grid;
  grid-auto-flow:     column;
  grid-auto-columns:  1fr;
  align-items:        stretch;   /* equal-height cells -> uniform full-height
                                    dividers even when only some columns have a
                                    description; content stays top-aligned */
}

/* One stat. Block flow (not flex) so a long description wraps to the column
   width and stays centred; the base text style here IS the description. */
.slice--stats .ArticleBody li {
  display:     block;
  text-align:  center;
  padding:     var(--space-2) var(--space-5);
  color:       var(--ink-soft);
  font-size:   var(--text-sm);
  font-weight: 500;
  line-height: 1.5;
}

/* Hairline divider BETWEEN columns only (never before the first). */
.slice--stats .ArticleBody li + li { border-inline-start: 1px solid var(--rule); }

/* Masked Lucide glyph above each stat, tinted by --stat-icon-color. */
.slice--stats .ArticleBody li::before {
  content: "";
  display: block;
  width:   var(--stat-icon-size);
  height:  var(--stat-icon-size);
  margin:  0 auto var(--space-4);
  background-color: var(--stat-icon-color);
  -webkit-mask: var(--stat-icon-fallback) no-repeat center / contain;
          mask: var(--stat-icon-fallback) no-repeat center / contain;
}
.slice--stats .ArticleBody li:nth-child(1)::before { -webkit-mask-image: var(--stat-icon-1); mask-image: var(--stat-icon-1); }
.slice--stats .ArticleBody li:nth-child(2)::before { -webkit-mask-image: var(--stat-icon-2); mask-image: var(--stat-icon-2); }
.slice--stats .ArticleBody li:nth-child(3)::before { -webkit-mask-image: var(--stat-icon-3); mask-image: var(--stat-icon-3); }
.slice--stats .ArticleBody li:nth-child(4)::before { -webkit-mask-image: var(--stat-icon-4); mask-image: var(--stat-icon-4); }

/* Label (small, above the number). <em> is only a hook -- de-italicise it. */
/* Label + description share the base .li style above; the label just happens
   to sit before the number. <em>/<strong> from earlier drafts still render
   fine if present, but only <strong> (the number) is required. */
/* The headline number. */
.slice--stats .ArticleBody li strong {
  display:        inline-block;      /* on its own line via the <br>s, but
                                        NOT the only structural hook -- if the
                                        client unbolds it, the number just
                                        drops to normal size, nothing breaks */
  font-family:    var(--font-display);
  font-weight:    700;
  font-size:      clamp(2rem, 1.2rem + 2.4vw, 3.25rem);
  line-height:    1.05;
  letter-spacing: -0.02em;
  color:          var(--ink);
  margin-block:   var(--space-2);    /* breathing space above + below */
}


/* -------------------------------------------------------------------------
   3.7d Impact Stats -- phone: stack to one column, dividers go horizontal.
   ------------------------------------------------------------------------- */
@media (max-width: 720px) {
  .slice--stats .ArticleBody ul {
    grid-auto-flow: row;
    grid-template-columns: 1fr;
  }
  .slice--stats .ArticleBody li + li {
    border-inline-start: 0;
    border-top: 1px solid var(--rule);
  }
  .slice--stats .ArticleBody li { padding-block: var(--space-6); }
}

/* -------------------------------------------------------------------------
   3.7e  STATS VARIANT: reasons  --  .slice--stats-reasons
   -------------------------------------------------------------------------
   Used WITH .slice--stats:
       <section class="slice slice--stats slice--stats-reasons">

   Identical machinery to Impact Stats: one client-edited article, a list
   whose item count sets the column count, a masked Lucide icon per column,
   hairline dividers between columns, and the phone stack. Retuned for a row
   of REASONS rather than numbers, so it suits "Why train with us", "How we
   help", or any short row of things a visitor should know before they buy.

   WHY a variant rather than a new slice: the only thing that genuinely
   differs is the strong-tag treatment. In Impact Stats that is the headline
   number, sized clamp(2rem, ..., 3.25rem), which is right for "17,443" and
   far too big for a multi-word title such as "Expert Instructors" -- at
   52px it would wrap mid-phrase in a quarter-width column. Everything else
   about the block is already correct, so this restates one declaration block
   instead of duplicating a whole section.

   Article structure is the same as Impact Stats minus the number: an optional
   heading, then a list with one item per reason. Put the reason title in
   bold, then a line break, then the supporting sentence. The italic label
   Impact Stats uses for its caption is simply not needed here.

   Icons: override the per-column icon custom properties with an inline style
   on the section element, exactly as Impact Stats documents. */
.slice--stats.slice--stats-reasons .ArticleBody li strong {
  /* A title, not a statistic: down to the type scale's large step so it reads
     as a clear heading above its description without dominating the column.
     Inherits the display family, weight and margins from Impact Stats. */
  font-size:      var(--text-lg);
  line-height:    1.25;
  letter-spacing: -0.01em;
}


/* -------------------------------------------------------------------------
   3.8  MODULE: Mobile Menu  --  hamburger button + slide-in panel
   -------------------------------------------------------------------------
   Hidden on desktop (display: none). At <=1400px -- see section 4 -- the
   menu button appears INSIDE the header (right-aligned next to the
   search icon), and the existing .site-header__nav becomes a slide-in
   panel anchored to the right edge with a backdrop dimming the rest
   of the page.

   Toggling is driven by a small JS snippet (provided alongside style.css)
   that injects the button + backdrop into the DOM and adds/removes a
   `menu-open` class on <body> when the user taps. Search lives in its
   own overlay (.site-search-overlay) at all viewport sizes -- see 3.4b.
   ------------------------------------------------------------------------- */

.mobile-menu-toggle,
.mobile-menu-backdrop,
.mobile-menu-close,
.mobile-menu-header,
.mobile-account-links,
.mobile-nav-clean,
.mobile-search {
  display: none;            /* desktop default -- all hidden */
}

/* Menu button -- borderless, lives inside .site-header__main on mobile.
   Three stacked bars on the left, "MENU" / "CLOSE" label on the right
   (the text helps less-tech-y visitors recognise it as a menu). */
.mobile-menu-toggle {
  align-items:     center;
  gap:             var(--space-3);
  padding:         var(--space-2) var(--space-3);
  background:      transparent;
  border:          0;
  border-radius:   var(--radius);
  cursor:          pointer;
  font-family:     var(--font-body);
  color:           var(--ink);
  transition:      background 0.2s ease;
}
.mobile-menu-toggle:hover { background: var(--accent-soft); }

.mobile-menu-toggle__icon {
  display:        flex;
  flex-direction: column;
  gap:            4px;
  width:          22px;
}
.mobile-menu-toggle__icon span {
  display:       block;
  height:        2px;
  background:    currentColor;
  border-radius: 1px;
  transition:    transform 0.3s ease, opacity 0.2s ease;
}

.mobile-menu-toggle__label {
  font-size:      var(--text-xs);
  font-weight:    700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  line-height:    1;
}

/* Bars morph into an X when the menu is open */
.menu-open .mobile-menu-toggle__icon span:nth-child(1) {
  transform: translateY(6px) rotate(45deg);
}
.menu-open .mobile-menu-toggle__icon span:nth-child(2) {
  opacity: 0;
}
.menu-open .mobile-menu-toggle__icon span:nth-child(3) {
  transform: translateY(-6px) rotate(-45deg);
}

/* Backdrop -- dims the page behind the slide-in panel */
/* The "backdrop" is intentionally invisible. It exists solely as a
   click-catcher so tapping outside the drawer closes the menu. We
   tried both a dark tint and a backdrop-filter blur -- both made the
   open-drawer state feel like a heavy modal takeover, especially at
   the wider drawer breakpoint (<=1400px) where the drawer is really
   just a sidebar over plenty of visible page. The drawer's own
   background and shadow already establish enough visual hierarchy
   to read as "above" the page. */
.mobile-menu-backdrop {
  position:   fixed;
  inset:      0;
  /* v0.277: was transparent, so no site dimmed behind its mobile menu.
     color-mix keeps the scrim on whatever ink the skin sets, so it stays
     on-palette without each site declaring its own. The flat rgba above
     is the fallback for engines without color-mix. */
  background: rgba(0, 0, 0, 0.45);
  background: color-mix(in srgb, var(--ink) 55%, transparent);
  z-index:    9000;
  opacity:    0;
  transition: opacity 0.3s ease;
}
.menu-open .mobile-menu-backdrop {
  display: block;
  opacity: 1;
}

/* Lock scrolling behind the menu panel. Using position: fixed on body
   (with `top: -{scrollY}px` set inline by setOpen) is the iOS-safe
   scroll-lock pattern. The previous version used `overflow: hidden`
   alone, which is famously unreliable on iOS Safari where touch-scroll
   can still propagate to the document and carry the sticky header
   along with it.

   The inline `top: -{scrollY}px` set by setOpen preserves the user's
   visual scroll position; setOpen restores window.scrollTo on close. */
body.menu-open {
  position: fixed;
  left:     0;
  right:    0;
  width:    100%;
  overflow: hidden;       /* belt-and-braces -- catches edge cases on
                             some Android browsers where position: fixed
                             alone leaves a hairline of scroll bleed */
}

/* When body becomes position: fixed during menu-open, .site-header's
   sticky-positioning loses its scroll container and devolves to
   relative -- which would render the header at body's top edge,
   which is now at viewport-top minus the saved scroll offset
   (i.e. offscreen). Force the header to viewport-fixed for the
   duration of menu-open so it stays visible behind the slide-in
   panel.

   The panel itself is z-index 9050 inside .site-header, so it still
   renders above the (now also fixed) header content. The header's
   z-index 50 also still establishes the local stacking context that
   panel and backdrop are scoped to. */
body.menu-open .site-header {
  position: fixed;
  top:      0;
  left:     0;
  right:    0;
}


/* -------------------------------------------------------------------------
   3.9  MODULE: Welcome Article  --  .welcome_article  (Article module)
   -------------------------------------------------------------------------
   Settings -> CSS Class = "welcome_article"
   Used for short editorial-style blocks like "A very warm welcome".
   Hubb wraps the rendered article in:

     .welcome_article
       .ArticleBody
         h1                       <- article title
         .title3                  <- lead paragraph (Hubb's class for it)
           ... text ...
           <br><br>
           a.ArrowToRight         <- Hubb's "arrow link" CTA
       .ArticleRotaSignupLink     <- empty container, hidden via :empty

   Layout: centred text, generous whitespace, large italic Fraunces
   headline. Treats the .ArrowToRight link as a styled CTA pill.
   ------------------------------------------------------------------------- */

.welcome_article {
  max-width:      var(--container-width);
  margin-inline:  auto;
  padding-inline: var(--container-gutter);
  margin-block:   var(--space-16);
}

.welcome_article .ArticleBody {
  max-width:     760px;
  margin-inline: auto;
  text-align:    center;
}

.welcome_article h1 {
  font-family:    var(--font-display);
  /* Page H1 sits between hero (tier 1, 80px max) and section
     headings (tier 3, 52px max). 36-60px range keeps it clearly
     above section breaks without competing with the hero. */
  font-size:      clamp(2.25rem, 5vw, 3.75rem);
  font-weight:    400;
  font-style:     italic;
  letter-spacing: -0.02em;
  line-height:    1.1;
  margin:         0 0 var(--space-6);
  color:          var(--ink);
}

.welcome_article .title3 {
  font-family: var(--font-body);
  font-size:   var(--text-lg);
  line-height: 1.65;
  color:       var(--ink-soft);
}

/* Inline links inside the lead paragraph (e.g. "Sunday Services").
   Uses --accent-dark (not --accent) for WCAG AA contrast -- accent
   on cream is 4.24:1 which fails the 4.5:1 normal-text minimum.
   accent-dark = 6.86:1 which passes AAA. Decorative uses of accent
   (buttons, hover backgrounds, the accent rule under headings) are
   fine because they're not body text. */
.welcome_article .title3 a:not(.ArrowToRight) {
  color:                 var(--accent-dark);
  text-decoration:       underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition:            opacity var(--transition);
}

/* Arrow CTA -- pill button with a chevron that nudges right on hover */
.welcome_article .ArrowToRight {
  display:        inline-flex;
  align-items:    center;
  gap:            var(--space-3);
  margin-top:     var(--space-4);
  padding:        var(--space-3) var(--space-6);
  background:     var(--accent);
  color:          var(--accent-ink);
  text-decoration: none;
  border-radius:  999px;
  font-family:    var(--font-body);
  font-size:      var(--text-xs);
  font-weight:    700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  transition:     background 0.2s ease, opacity 0.2s ease;
}
.welcome_article .ArrowToRight::after {
  content:    "->";
  font-size:  1.15em;
  /* v0.143: small upward nudge so the "->" glyphs (which sit at
     glyph-middle / x-height level) align visually with the
     surrounding UPPERCASE text (which sits at cap-height level).
     Without this, the arrow reads as sitting low against caps
     because the larger 1.15em line-box centres the arrow's middle
     to the same point as the caps' middle, but caps OCCUPY the
     upper half of their line-box while the arrow glyph sits in
     the centre of its line-box. -0.08em moves the arrow up just
     enough to bring the visible glyph into the same band as the
     caps. translateX(3px) on hover (below) needs to be expanded
     to translate(3px, -0.08em) so the Y nudge survives the hover
     transform replacement. */
  transform:  translateY(-0.08em);
  transition: transform 0.2s ease;
}
/* Hover/active states. All three primary CTAs (.ArrowToRight,
   .feature__cta, .hero-caption__cta) now share the same hover treatment:
   accent -> accent-dark. The arrow nudge is transform-based so it lives
   inside @media (hover: hover) to avoid sticking after a tap. The
   :active state gives touch users a tactile press confirmation that
   :hover never can. */
.welcome_article .ArrowToRight:focus-visible {
  background: var(--accent-dark);
  opacity:    1;
}
.welcome_article .ArrowToRight:active {
  background: var(--accent-dark);
  transform:  translateY(1px);
}
@media (hover: hover) {
  .welcome_article .ArrowToRight:hover {
    background: var(--accent-dark);
    opacity:    1;
  }
  .welcome_article .ArrowToRight:hover::after {
    transform: translate(3px, -0.08em);   /* X slide + preserves baseline Y nudge */
  }
}

/* Hide the empty rota-signup container Hubb appends to every article */
.welcome_article .ArticleRotaSignupLink:empty { display: none; }


/* =========================================================================
   3.9b  MODULE: Where & When  --  .where_when  (Article module)
   -------------------------------------------------------------------------
   Settings -> CSS Class = "where_when"

   A two-column info card with the church's service times (left) and
   location (right), each marked by an icon and a heading. Authored
   as a Hubb Article module so the content is editable in the Web
   Office. Card sits as its own visual unit -- no section heading
   wrapping it.

   Article authoring -- the design works with EITHER of two structures
   the Web Office editor tends to produce:

     Option A (each section in its own <p>):
       <p><strong>When:</strong><br>...details...</p>
       <p><strong>Where:</strong><br>...details...</p>

     Option B (both sections in one <p>, separated by <br><br>):
       <p>
         <strong>When:</strong><br>...details...<br>
         <br>
         <strong>Where:</strong><br>...details...
       </p>

   The CSS targets <strong> elements directly (rather than each <p>),
   so the icon assignment by <strong>:nth-of-type works the same way
   in both structures. First <strong> gets the clock icon (Service
   Times); second <strong> gets the map-pin icon (Location).

   The article's <h1> is hidden -- this card has no section heading
   in the design. If the author leaves welcome text in the H1 it
   doesn't render here (the welcome message belongs in the welcome
   article module elsewhere on the page).

   The two-column layout is achieved with CSS `column-count: 2` on
   the paragraph, plus `break-before: column` on the second <strong>
   to force "Where" into the right column. This works with either
   article structure.

   Body Contents pattern -- typically placed below the welcome article
   or between the hero and welcome article:

     <module alt="Where and When" module_id="..." type_id="10"
             cssClass="where_when" />
   ========================================================================= */

.where_when {
  /* Card-style container -- subtle warm tint sits between the page
     background (--bg) and a stark white surface. Uses --accent-soft
     at low alpha to thread the brand colour through the whole card
     without overwhelming. */
  background:    color-mix(in srgb, var(--accent-soft) 35%, var(--surface));
  border:        1px solid var(--rule);
  border-radius: 16px;            /* larger than --radius (4px) -- this
                                     is a panel, not a small chip */
  padding:       var(--space-8);
  margin:        var(--space-12) auto;
  max-width:     1200px;
}

/* Hide the article's <h1> -- this card has no section heading in
   the design, and authors sometimes leave welcome text in the H1
   that belongs in the welcome article elsewhere on the page. */
.where_when .ArticleBody h1 {
  display: none;
}

/* Defeat any inline font-size spans the editor sometimes injects --
   the card's typography is set on the <strong> and <p> rules, and
   per-character overrides break the visual rhythm. */
.where_when .ArticleBody [style*="font-size"] {
  font-size: inherit !important;
}

/* The article's paragraph(s) become the column-flow container.
   column-count: 2 splits content into two columns; break-before on
   the second <strong> below forces "Where" to start in the right
   column, so When ends up in column 1 and Where in column 2. The
   column-rule is the vertical divider between them. This works
   whether the article uses one <p> or two -- both authoring
   structures end up with the same visual result. */
.where_when .ArticleBody p {
  margin:       0;
  font-family:  var(--font-body);
  font-size:    var(--text-base);
  color:        var(--ink);
  line-height:  1.6;
  column-count: 2;
  column-gap:   var(--space-12);   /* 48px -- the right column's icon
                                      sits at left:0 of its column, so a
                                      tighter gap (32px) put it close
                                      against the rule and the rule
                                      *read* as offset toward the right.
                                      48px gives 24px of breathing room
                                      on each side, which balances
                                      against the natural empty space
                                      at column 1's right edge. */
  column-rule:  1px solid var(--rule);
  /* column-fill: balance is the default and best for this layout --
     when one column is shorter than the other (e.g. one-line When
     vs three-line Where) the rule still spans the full container
     height so the divider doesn't truncate awkwardly. */
}

/* If the article happens to have multiple <p> tags (option A above),
   we need each <p> to NOT individually become a column container --
   they should sit in flow as siblings inside the column-formatting
   context. Apply the column rules to the wrapping ArticleBody
   instead in that case. */
.where_when .ArticleBody:has(p + p) {
  column-count: 2;
  column-gap:   var(--space-12);
  column-rule:  1px solid var(--rule);
}
.where_when .ArticleBody:has(p + p) p {
  column-count: 1;            /* nested columns are not what we want */
  column-rule:  none;
  break-inside: avoid-column; /* keep each <p> together in its column */
}

/* Tighten heading-to-body gap. The article author types
   <strong>X:</strong><br>... but since strong is display:block
   it already moves the next text to a new line by itself, so
   the <br> right after it is redundant and adds an unwanted
   ~26px line break. Hidden here. With the <br> gone, the gap
   between heading and body is just margin-bottom (12px), which
   reads as tight without feeling cramped. */
.where_when .ArticleBody p strong + br,
.where_when .ArticleBody strong + br {
  display: none;
}

/* Section separator collapse: previously a `.where_when br + br
   { display: none }` rule lived here to hide the empty-line <br><br>
   the article author types between When and Where sections. v0.154
   removed it -- CSS adjacent sibling combinator skips text nodes
   (HTML whitespace between BRs counts as a text node), so the rule
   matched every <br> whose previous ELEMENT sibling was also a
   <br>, regardless of real text between them in the DOM. In Adam's
   article HTML that meant essentially every line-break <br> got
   hidden too, so "...10.00am" and "1st Sunday..." ran together.
   Replacement is collapseTrueConsecutiveBrs() in script.js, which
   walks each <br>'s previousSibling skipping whitespace-only text
   nodes, and only hides the <br> if its actual neighbour is another
   <br>. Real text between two <br>s leaves both visible. */

/* Each <strong> becomes a section heading with its own icon. Uses
   :nth-of-type so it works regardless of how many <p> the article
   has -- both sections will have first/second strongs by the
   typical article structure. */
.where_when .ArticleBody p strong,
.where_when .ArticleBody strong {
  display:        block;
  position:       relative;
  padding-left:   calc(32px + var(--space-3));
  font-family:    var(--font-display);
  font-size:      var(--text-xl);
  font-weight:    500;
  color:          var(--accent);
  letter-spacing: -0.01em;
  line-height:    1.2;
  margin-bottom:  var(--space-3);   /* 12px -- a touch of breathing room
                                       between heading and body, now that
                                       the redundant <br> after strong is
                                       gone (otherwise this gap was ~34px
                                       from margin + line-break). */
  /* Avoid awkward column splits where the icon ends up in one column
     and the body in the next. */
  break-after:    avoid-column;
}

/* Force the second <strong> to start in column 2. Pairs with
   column-count: 2 above. */
.where_when .ArticleBody p strong:nth-of-type(2),
.where_when .ArticleBody strong:nth-of-type(2) {
  break-before: column;
}

/* Icon -- sized to match the heading's optical baseline, mask-image
   coloured by background-color so it inherits --accent and rethemes
   correctly when --accent changes for a different church. */
.where_when .ArticleBody p strong::before,
.where_when .ArticleBody strong::before {
  content:               '';
  position:              absolute;
  left:                  0;
  top:                   50%;
  transform:             translateY(-50%);
  width:                 32px;
  height:                32px;
  background-color:      var(--accent);
  -webkit-mask-size:     contain;
          mask-size:     contain;
  -webkit-mask-repeat:   no-repeat;
          mask-repeat:   no-repeat;
  -webkit-mask-position: center;
          mask-position: center;
}

/* Clock icon for the first <strong> (Service Times / When) */
.where_when .ArticleBody p strong:nth-of-type(1)::before,
.where_when .ArticleBody strong:nth-of-type(1)::before {
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='10'/><polyline points='12 6 12 12 16 14'/></svg>");
          mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='10'/><polyline points='12 6 12 12 16 14'/></svg>");
}

/* Map-pin icon for the second <strong> (Location / Where) */
.where_when .ArticleBody p strong:nth-of-type(2)::before,
.where_when .ArticleBody strong:nth-of-type(2)::before {
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z'/><circle cx='12' cy='10' r='3'/></svg>");
          mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z'/><circle cx='12' cy='10' r='3'/></svg>");
}

/* Optional link inside either section.
   --accent-dark not --accent: --accent on the where_when bg
   (color-mix of accent-soft + surface ~ #f7f0ea) gives only 4.02:1
   which fails AA for body-sized text. --accent-dark gives 6.14:1.
   Hover affordance is the underline thickening below -- removing
   the colour shift on hover is fine because the underline change
   is the more reliable signal anyway. */
.where_when .ArticleBody a {
  color:                     var(--accent-dark);
  text-decoration:           underline;
  text-underline-offset:     3px;
  text-decoration-thickness: 1px;
  font-weight:               500;
  display:                   inline-block;
  margin-top:                var(--space-2);
  /* v0.152: opacity hover instead of text-decoration-thickness
     change -- same fix as .announcement_banner above. See that
     rule for the reasoning. */
  transition:                opacity var(--transition);
}
.where_when .ArticleBody a:hover {
  opacity: 0.75;
}
.where_when .ArticleBody a:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 3px;
}

/* Hide Hubb's article cruft */
.where_when .ArticleRotaSignupLink {
  display: none;
}


/* =========================================================================
   3.9c  MODULE: Where & When (variant 2) -- .where_when_2  (Article module)
   -------------------------------------------------------------------------
   Settings -> CSS Class = "where_when_2"
   A slimmer, full-width-background take on the Where & When card. Instead of
   a centred tinted panel, this is a compact horizontal strip (When | Where)
   sitting on a full-bleed band. Same Article content + the same
   "<strong>When:</strong>...<strong>Where:</strong>..." structure and icon
   assignment as .where_when, so the identical article works in either variant.

   Self-contained: it paints its OWN full-width band (a 100vw ::before, like
   the section themes) and sets its own slim padding, so ONE class delivers
   both "slimmer" and "full-width background". The icon logic is duplicated
   from .where_when (rather than coupled to it) so .where_when stays untouched.
   Band tint is the same warm accent-soft mix as the card so the two variants
   read as a family; it rethemes automatically when --accent-soft changes.

   Body Contents pattern:
     <section class="slice slice--where-when">
       <module alt="Where and When" module_id="..." type_id="10" cssClass="where_when_2" />
     </section>
   ========================================================================= */
.where_when_2 {
  position:      relative;
  max-width:     1000px;          /* narrower centred content -- it is a strip */
  margin-inline: auto;
  padding-block: var(--space-6);  /* slim -- the card uses space-8 + a big margin */
}
/* Full-width band painted behind the slim content; content stays in its
   centred column (same technique as the .slice--*-grey section bands). */
.where_when_2::before {
  content:    "";
  position:   absolute;
  z-index:    -1;
  top:        0;
  bottom:     0;
  left:       50%;
  width:      100vw;
  transform:  translateX(-50%);
  background: color-mix(in srgb, var(--accent-soft) 35%, var(--surface));
}

/* Hide the article's <h1> + Hubb cruft, defeat inline font-size spans, and
   drop the redundant <br> after each <strong> (same handling as .where_when). */
.where_when_2 .ArticleBody h1,
.where_when_2 .ArticleRotaSignupLink { display: none; }
.where_when_2 .ArticleBody [style*="font-size"] { font-size: inherit !important; }
.where_when_2 .ArticleBody p strong + br,
.where_when_2 .ArticleBody strong + br { display: none; }

/* Two compact columns (When | Where) with a slim divider; smaller type than
   the card keeps the strip slim. Works with one-<p> or two-<p> articles. */
.where_when_2 .ArticleBody p,
.where_when_2 .ArticleBody:has(p + p) {
  margin:       0;
  font-family:  var(--font-body);
  font-size:    var(--text-sm);
  color:        var(--ink);
  line-height:  1.5;
  column-count: 2;
  column-gap:   var(--space-12);
  column-rule:  1px solid var(--rule);
}
.where_when_2 .ArticleBody:has(p + p) p {
  column-count: 1;
  column-rule:  none;
  break-inside: avoid-column;
}

/* Each <strong> -> compact section heading with a smaller icon. */
.where_when_2 .ArticleBody p strong,
.where_when_2 .ArticleBody strong {
  display:       block;
  position:      relative;
  padding-left:  calc(24px + var(--space-2));
  font-family:   var(--font-display);
  font-size:     var(--text-base);
  font-weight:   500;
  color:         var(--accent);
  line-height:   1.2;
  margin-bottom: var(--space-1);
  break-after:   avoid-column;
}
.where_when_2 .ArticleBody p strong:nth-of-type(2),
.where_when_2 .ArticleBody strong:nth-of-type(2) { break-before: column; }

.where_when_2 .ArticleBody p strong::before,
.where_when_2 .ArticleBody strong::before {
  content:               "";
  position:              absolute;
  left:                  0;
  top:                   50%;
  transform:             translateY(-50%);
  width:                 24px;
  height:                24px;
  background-color:      var(--accent);
  -webkit-mask-size:     contain;
          mask-size:     contain;
  -webkit-mask-repeat:   no-repeat;
          mask-repeat:   no-repeat;
  -webkit-mask-position: center;
          mask-position: center;
}
.where_when_2 .ArticleBody p strong:nth-of-type(1)::before,
.where_when_2 .ArticleBody strong:nth-of-type(1)::before {
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='10'/><polyline points='12 6 12 12 16 14'/></svg>");
          mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><circle cx='12' cy='12' r='10'/><polyline points='12 6 12 12 16 14'/></svg>");
}
.where_when_2 .ArticleBody p strong:nth-of-type(2)::before,
.where_when_2 .ArticleBody strong:nth-of-type(2)::before {
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z'/><circle cx='12' cy='10' r='3'/></svg>");
          mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z'/><circle cx='12' cy='10' r='3'/></svg>");
}

/* Optional link -- accent-dark for AA contrast on the tint (as .where_when). */
.where_when_2 .ArticleBody a {
  color:                     var(--accent-dark);
  text-decoration:           underline;
  text-underline-offset:     3px;
  text-decoration-thickness: 1px;
  font-weight:               500;
  transition:                opacity var(--transition);
}
.where_when_2 .ArticleBody a:hover { opacity: 0.75; }
.where_when_2 .ArticleBody a:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }


/* -------------------------------------------------------------------------
   3.10  MODULE: Upcoming Events  --  .upcoming_events  (Calendar module)
   -------------------------------------------------------------------------
   Settings -> CSS Class = "upcoming_events"
   Calendar module set to "List the next N events" with the standard
   listing layout. Hubb wraps the rendered events in:

     section.upcoming-events
       h2.upcoming-events__heading
       .upcoming_events                    <- Hubb's class (underscore)
         div[class^="event-group-"]        <- one per event
           .eventListImage > a > img
           .eventListDate                  <- e.g. "24 Apr 11:30am - 2:30pm"
           .eventListTitle  > a            <- opens event in popup
           .eventListLocation > a          <- opens Google Maps in new tab

   Layout matches the Articles List grid (4 / 2 / 1 breakpoints) for
   visual consistency. Whole card is clickable to the event popup via
   a stretched-link pseudo-element on the title; the location link is
   lifted above that overlay so map clicks still go to Google Maps.
   ------------------------------------------------------------------------- */

.slice--upcoming-events,
.upcoming-events {
  max-width:      var(--container-width);
  margin-inline:  auto;
  padding-inline: var(--container-gutter);
  margin-block:   var(--space-16);
}

/* Card grid -- same shape as .al-cards */
.upcoming_events {
  display:               grid;
  grid-template-columns: repeat(4, 1fr);
  gap:                   var(--space-8);
}

.upcoming_events > div {
  display:        flex;
  flex-direction: column;
  gap:            var(--space-3);
  position:       relative;   /* anchor for the stretched-link overlay */
}

/* --- Image --- */
.upcoming_events .eventListImage {
  overflow:      hidden;
  border-radius: var(--radius);
  background:    var(--accent-soft);
  aspect-ratio:  4 / 3;
}
.upcoming_events .eventListImage a { display: block; height: 100%; }
.upcoming_events .eventListImage img {
  width:      100%;
  height:     100%;
  object-fit: cover;
  transition: transform 600ms ease;
}
@media (hover: hover) {
  .upcoming_events > div:hover .eventListImage img { transform: scale(1.04); }
}

/* --- Placeholder when no image exists ---
   JS (addEventImagePlaceholders) injects this div when the event has
   no <img> or the img has no src. Designed to look intentional rather
   than apologetic -- uses the brand accent color against the existing
   accent-soft background fill, with a subtle calendar icon centered.
   The aspect-ratio: 4/3 set on .eventListImage above keeps the card
   shape consistent with image-bearing events, so the grid never has
   ragged heights. */
.upcoming_events .eventListImage__placeholder,
.upcoming_events_3 .eventListImage__placeholder {
  width:           100%;
  height:          100%;
  display:         flex;
  align-items:     center;
  justify-content: center;
  color:           var(--accent);
  opacity:         0.6;
}
.upcoming_events .eventListImage__placeholder svg,
.upcoming_events_3 .eventListImage__placeholder svg {
  width:  35%;
  height: auto;
  max-width: 80px;
}

/* --- Date -- calendar tear-off block. Hubb outputs the raw date as a
   single text node (e.g. "24 Apr 11:30am - 2:30pm"); a JS snippet in
   Head Contents parses it and rebuilds it as structured spans:
     <div class="eventListDate">
       <span class="day">24</span>
       <span class="month">Apr</span>
       <span class="time">11:30am - 2:30pm</span>
     </div>
   If the JS doesn't run for any reason, the original text shows
   normally -- graceful fallback. --- */
.upcoming_events .eventListDate {
  display:        flex;
  align-items:    center;
  gap:            var(--space-3);
  font-family:    var(--font-body);
  margin-top:     var(--space-1);
  color:          var(--ink-soft);
}
.upcoming_events .eventListDate .day,
.upcoming_events .eventListDate .month {
  display:        flex;
  flex-direction: column;
  align-items:    center;
  line-height:    1;
}
.upcoming_events .eventListDate .day {
  font-family:    var(--font-display);
  font-size:      1.75rem;
  font-weight:    500;
  color:          var(--ink);
  letter-spacing: -0.02em;
}
.upcoming_events .eventListDate .month {
  font-size:      0.7rem;
  font-weight:    700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color:          var(--accent);
  margin-top:     2px;
}
.upcoming_events .eventListDate .day-month {
  display:        flex;
  flex-direction: column;
  align-items:    center;
  padding:        var(--space-2) var(--space-3);
  border:         1px solid var(--rule);
  border-radius:  var(--radius);
  background:     var(--surface);
  min-width:      52px;
}
.upcoming_events .eventListDate .time {
  font-size: var(--text-sm);
  flex:      1;
}

/* --- Title (also the stretched-link host) --- */
.upcoming_events .eventListTitle {
  font-family:    var(--font-display);
  font-size:      var(--text-lg);
  font-weight:    500;
  line-height:    1.2;
  letter-spacing: -0.01em;
}
.upcoming_events .eventListTitle a {
  color:               var(--ink);
  text-decoration:     none;
  background-image:    linear-gradient(currentColor, currentColor);
  background-size:     0% 1px;
  background-repeat:   no-repeat;
  background-position: 0 100%;
  transition:          background-size 300ms ease;
}
/* Stretched-link overlay -- invisible, covers the whole card */
.upcoming_events .eventListTitle a::before {
  content:  "";
  position: absolute;
  inset:    0;
  z-index:  1;
}
@media (hover: hover) {
  .upcoming_events > div:hover .eventListTitle a { background-size: 100% 1px; }
}

/* --- Location -- pinned to bottom of card; lifted above the stretched
   link so the map link stays independently clickable --- */
.upcoming_events .eventListLocation {
  margin-top: auto;
  padding-top: var(--space-2);
  font-size:  var(--text-sm);
  color:      var(--ink-soft);
  position:   relative;
  z-index:    2;
}
.upcoming_events .eventListLocation a {
  color:           var(--ink-soft);
  text-decoration: none;
  transition:      color var(--transition);
}
.upcoming_events .eventListLocation a:hover {
  color:   var(--accent);
  opacity: 1;
}

/* Empty state -- JS injects this when Hubb renders no events. */
.upcoming-events__empty {
  grid-column: 1 / -1;
  text-align:  center;
  padding:     var(--space-12) var(--space-4);
  color:       var(--ink-soft);
  font-style:  italic;
  font-family: var(--font-display);
  font-size:   var(--text-lg);
}

/* "View all sermons / View full calendar" links below each feed.
   
   Treatment: a horizontal-rule-prefix link sitting on its own line,
   left-aligned, in the body ink colour rather than the brand accent --
   the goal is "feature link" not "button". The leading rule extends
   on hover and the arrow nudges right, so the link feels alive without
   shouting. Same pattern works under News, Events, Media, anywhere the
   <a class="section-link"> is used.
   
   Layout note: display: flex + width: max-content + margin auto:0
   forces left alignment regardless of any text-align: center on the
   parent section (which the headings rely on). */
.section-link {
  /* Block-level flex so margin-inline:auto centres the pill in
     its parent section. width:max-content shrinks to fit content
     so auto margins have something to balance against. */
  display:        flex;
  width:          max-content;
  align-items:    center;
  gap:            var(--space-3);
  margin:         var(--space-8) auto 0;
  padding:        var(--space-3) var(--space-6);

  font-family:    var(--font-body);
  font-size:      var(--text-xs);
  font-weight:    700;
  letter-spacing: 0.12em;
  text-transform: uppercase;

  /* Subtle ghost-pill: transparent fill, hairline border, ink text.
     Earlier version used --accent (terracotta) on hover which felt
     too brand-loud for a secondary "see more" affordance -- kept
     entirely neutral now so the link reads as supporting nav, not
     a primary call-to-action. */
  background:      transparent;
  color:           var(--ink);
  text-decoration: none;
  border:          1px solid var(--rule);
  border-radius:   999px;
  transition:      background var(--transition), border-color var(--transition), gap var(--transition);
}

/* Earlier version had a leading underline pseudo -- removed for the
   pill design (the rounded shape itself is the affordance). */
.section-link::before {
  display: none;
}

/* Trailing arrow -- slides right slightly on hover */
.section-link::after {
  content:    "->";
  display:    inline-block;
  font-weight: 400;
  transition: transform var(--transition);
}

@media (hover: hover) {
  .section-link:hover {
    /* Wash background slightly + strengthen border. Keep text
       colour as --ink (no accent shift) for the neutral feel. */
    background:    var(--bg);
    border-color:  var(--ink-soft);
    gap:           var(--space-4);
  }
  .section-link:hover::after {
    transform: translateX(2px);
  }
}
.section-link:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 2px;
}
/* Touch feedback: 100ms-quick darker fill on press. Mirrors the
   behaviour Apple HIG / Material both expect: visible change
   within ~100ms of touch-down so the device feels responsive. */
.section-link:active {
  background:   var(--rule);
  border-color: var(--ink);
  /* Override the default transition so :active fires INSTANTLY on
     touch-down rather than animating in over the standard duration. */
  transition: background 80ms ease, border-color 80ms ease;
}


/* =========================================================================
   3.10b  MODULE: Latest Media (Sermons)  --  .latest_media  (Media List)
   -------------------------------------------------------------------------
   Settings -> CSS Class = "latest_media"
   Targets the Media List module configured to show recent recordings
   (e.g. sermon recordings -- audio or video).

   USAGE in Body Contents:

     <section class="latest-media">
       <h2 class="latest-media__heading">Latest Sermons</h2>
       <module alt="Latest Media" module_id="..." type_id="11" />
     </section>

   Hubb's rendered structure differs slightly from Articles List in
   one important way: the items aren't direct children of .latest_media,
   they're wrapped in <span class="MediaList">. So the grid container
   is .latest_media .MediaList, not .latest_media itself. (The span
   element is happy to be a grid container -- display: grid overrides
   the default inline layout.)

   Each card has, in source order:
     .mediaListImage > a > img       (poster art)
     .mediaListDate > span           (e.g. "19 April")
     .mediaListTitle > a             (e.g. "Faith Heroes: Moses")
                                     -- opens player popup via javascript:playMedia(...)
     .mediaListSpeaker > span        (e.g. "Glenys Bayliss")

   We render the visual hierarchy as: image with play overlay, then
   small uppercase date tag, then the prominent title, then the
   speaker as an italic "by Glenys Bayliss" byline. Whole card is
   clickable (stretched-link on the title <a>). Same 4 / 2 / 1 grid
   breakpoints as Articles + Events for visual consistency.
   ------------------------------------------------------------------------- */

/* Section wrapper -- same shape as .latest-news / .upcoming-events */
.slice--latest-media,
.latest-media {
  max-width:      var(--container-width);
  margin-inline:  auto;
  padding-inline: var(--container-gutter);
  margin-block:   var(--space-16);
}

/* Grid container -- see comment above; we target the inner MediaList span */
.latest_media .MediaList {
  display:               grid;
  grid-template-columns: repeat(4, 1fr);
  gap:                   var(--space-8);
}

/* Each item card. position:relative anchors the stretched-link overlay
   on the title <a>. */
:is(.latest_media, .latest_media_3) .MediaList > div {
  display:        flex;
  flex-direction: column;
  gap:            var(--space-3);
  position:       relative;
}

/* --- Image with play overlay --- */
:is(.latest_media, .latest_media_3) .mediaListImage {
  overflow:       hidden;
  border-radius:  var(--radius);
  background:     var(--accent-soft);
  aspect-ratio:   4 / 3;
  position:       relative;
}
:is(.latest_media, .latest_media_3) .mediaListImage a { display: block; height: 100%; }
:is(.latest_media, .latest_media_3) .mediaListImage img {
  width:      100%;
  height:     100%;
  object-fit: cover;
  transition: transform 600ms ease;
}
@media (hover: hover) {
  :is(.latest_media, .latest_media_3) .MediaList > div:hover .mediaListImage img { transform: scale(1.04); }
}

/* Play button overlay -- circular accent badge in the bottom-right
   corner of the image. Inline SVG data URI so no extra HTTP request,
   and the icon will never 404. The triangle is intentionally offset
   slightly right of geometric center (background-position: 56%) so
   it visually centers -- the eye reads triangles as pulled toward
   their flat side, so dead-centering looks weighted left. */
/* Play icon overlay -- circular accent button at bottom-right of the
   thumbnail. Triangle SVG is designed so its geometric centroid lands
   exactly at the viewBox centre (vertices at 8/6, 8/18, 20/12 -> centroid
   at 12,12 in a 24x24 viewBox). Combined with background-position:
   center, the triangle is optically as well as geometrically centred --
   the previous version used a different SVG plus a 56% horizontal nudge
   which over-corrected and visibly pushed the triangle right of the
   circle's centre. */
:is(.latest_media, .latest_media_3, .latest_media_4) .mediaListImage::after {
  content:             "";
  position:            absolute;
  bottom:              var(--space-3);
  right:               var(--space-3);
  width:               44px;
  height:              44px;
  border-radius:       50%;
  background-color:    var(--accent);
  background-image:    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'><path d='M8 6v12l12-6z'/></svg>");
  background-position: center;
  background-size:     14px;
  background-repeat:   no-repeat;
  box-shadow:          0 2px 8px rgba(26, 22, 18, 0.25);
  transition:          transform 200ms ease, background-color 200ms ease;
  z-index:             2;
  pointer-events:      none;   /* let click pass through to image link */
}
@media (hover: hover) {
  :is(.latest_media, .latest_media_3) .MediaList > div:hover .mediaListImage::after {
    transform:        scale(1.1);
    background-color: var(--accent-dark);
  }
}

/* --- Date -- small uppercase tag, sits above the title.
   Same typographic treatment as the .articleListLink "Read more"
   tag so the metadata feels like one family across modules.
   Uses --accent-dark not --accent for body-text contrast: --accent
   on the cream page bg gives 4.24:1 which fails WCAG AA's 4.5:1
   minimum for body-sized text. --accent-dark gives 6.48:1, passing
   AA comfortably and only just shy of AAA. */
:is(.latest_media, .latest_media_3) .mediaListDate {
  font-family:    var(--font-body);
  font-size:      var(--text-xs);
  font-weight:    700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color:          var(--accent-dark);
  line-height:    1;
}

/* --- Title (also the stretched-link host) --- */
:is(.latest_media, .latest_media_3) .mediaListTitle {
  font-family:    var(--font-display);
  font-size:      var(--text-lg);
  font-weight:    500;
  line-height:    1.2;
  letter-spacing: -0.01em;
}
:is(.latest_media, .latest_media_3) .mediaListTitle a {
  color:               var(--ink);
  text-decoration:     none;
  background-image:    linear-gradient(currentColor, currentColor);
  background-size:     0% 1px;
  background-repeat:   no-repeat;
  background-position: 0 100%;
  transition:          background-size 300ms ease;
}
/* Stretched-link overlay -- invisible, covers the whole card */
:is(.latest_media, .latest_media_3) .mediaListTitle a::before {
  content:   "";
  position:  absolute;
  inset:     0;
  z-index:   1;
}
/* Hovering anywhere on the card is treated as hovering the title link */
@media (hover: hover) {
  :is(.latest_media, .latest_media_3) .MediaList > div:hover .mediaListTitle a { background-size: 100% 1px; }
}

/* --- Speaker -- italic byline below title.
   The "by " prefix is added via ::before so the rendered HTML stays
   clean (just the speaker name). If a future sermon has no speaker,
   Hubb omits the entire .mediaListSpeaker div, so the prefix never
   appears stranded. --- */
:is(.latest_media, .latest_media_3) .mediaListSpeaker {
  font-family: var(--font-body);
  font-size:   var(--text-sm);
  font-style:  italic;
  color:       var(--ink-soft);
  line-height: 1.4;
}
:is(.latest_media, .latest_media_3) .mediaListSpeaker::before {
  content:     "by ";
  font-style:  italic;
}


/* -------------------------------------------------------------------------
   3.10c  MODULE: Upcoming Events 2  --  .upcoming_events_2  (Calendar module)
   -------------------------------------------------------------------------
   Settings -> CSS Class = "upcoming_events_2"

   Horizontal carousel layout. Section becomes a 3-column grid:
     [intro block]  [scrollable card track]  [next button]

   The card track uses CSS scroll-snap so touch swipe, scrollbar drag,
   and keyboard scrolling all work natively. The next button (injected
   by setupCarousels in script.js) is a discoverability affordance for
   non-touch users -- it auto-disables when the user has scrolled to
   the end. Mobile drops the button entirely and stacks intro on top
   of a full-bleed scroller (swipe is the natural interaction).

   HTML pattern Adam adds to the template body:

     <section class="upcoming-events">
       <div class="upcoming-events-2__intro">
         <h2 class="upcoming-events-2__heading">Upcoming Events</h2>
         <p  class="upcoming-events-2__tagline">Keep up to date with
            what's on at the church.</p>
         <a  class="upcoming-events-2__cta"
             href="/Calendar/Month.aspx">View the full calendar</a>
       </div>
       <module alt="Upcoming Events" module_id="..." type_id="2"
               cssClass="upcoming_events_2" />
     </section>

   The :has() detection means a non-carousel .upcoming-events section
   on the same page (the original 4-col grid layout) keeps its old
   behaviour -- only sections containing the __intro block become
   carousels. :has() is supported in Safari 15.4+, Chrome 105+,
   Firefox 121+; older browsers fall back to the default vertical
   stack which is still readable, just not carousel-shaped.

   Reuses structureEventDates() and addEventImagePlaceholders() from
   script.js (selectors updated to include .upcoming_events_2).
   ------------------------------------------------------------------------- */


/* === Outer 2-column grid (applies to both events and media carousels) ===
   Column 1: intro block.
   Column 2: carousel viewport -- a position:relative wrapper that
             contains the scroller + the prev / next overlay buttons +
             the edge-fade pseudo-elements. The viewport is added at
             runtime by setupCarousels (script.js) which moves the
             scroller into it and injects both buttons. */

.slice--upcoming-events:has(> .upcoming-events-2__intro),
.upcoming-events:has(> .upcoming-events-2__intro),
.slice--latest-media:has(> .latest-media-2__intro),
.latest-media:has(> .latest-media-2__intro) {
  display:               grid;
  grid-template-columns: minmax(220px, 24%) minmax(0, 1fr);
  column-gap:            var(--space-8);
  row-gap:               var(--space-6);
  align-items:           center;
}


/* === Intro block (left column) -- shared by both modules === */

.upcoming-events-2__intro,
.latest-media-2__intro {
  display:        flex;
  flex-direction: column;
  gap:            var(--space-3);
  align-items:    flex-start;  /* see note on .latest-news-2__intro */
}
.upcoming-events-2__heading,
.latest-media-2__heading {
  font-family:    var(--font-display);
  font-size:      var(--text-2xl);
  font-weight:    500;
  line-height:    1.1;
  letter-spacing: -0.02em;
  color:          var(--ink);
  margin:         0;
}
/* Defensive: defeat the orange underline accent that .upcoming-events__
   heading carries via ::after. The carousel heading is a sidebar
   element, not a centred section banner, so it shouldn't carry that
   ornament -- and a customer who happens to use the old class name
   here by mistake should still see a clean heading. */
.upcoming-events-2__heading::after,
.latest-media-2__heading::after {
  display: none;
}

/* v0.170: force left alignment on every carousel intro heading. The
   .slice__head--intro .slice__heading rule (section 2.5) only lands when
   the heading carries BOTH .slice__heading and a .slice__head--intro
   ancestor; when that chain doesn't hold, the legacy centred-heading group
   (.latest-news__heading etc.) wins and centres the text. Targeting the
   -2__intro container's heading directly guarantees left regardless of the
   author's class choice. The bug only showed on short one-line titles
   (Latest News / Latest Sermons) -- "Upcoming Events" wraps and fills the
   column, hiding the centre. */
.upcoming-events-2__intro :is(h2, .slice__heading),
.latest-media-2__intro   :is(h2, .slice__heading),
.latest-news-2__intro    :is(h2, .slice__heading) {
  text-align: left;
}
.upcoming-events-2__tagline,
.latest-media-2__tagline {
  font-family: var(--font-body);
  font-size:   var(--text-base);
  color:       var(--ink-soft);
  line-height: 1.5;
  margin:      0;
}
.upcoming-events-2__cta,
.latest-media-2__cta {
  font-family:     var(--font-body);
  font-size:       var(--text-sm);
  font-weight:     600;
  color:           var(--ink);
  text-decoration: none;
  margin-top:      var(--space-2);
  align-self:      flex-start;
  display:         inline-flex;
  align-items:     center;
  gap:             var(--space-2);
  transition:      color var(--transition);
}
.upcoming-events-2__cta::after,
.latest-media-2__cta::after {
  content:    "\2192";  /* right arrow, escaped for pure-ASCII file (Anchises lesson) */
  font-size:  1.1em;
  transition: transform 200ms ease;
}
@media (hover: hover) {
  .upcoming-events-2__cta:hover,
  .latest-media-2__cta:hover {
    color: var(--accent);
  }
  .upcoming-events-2__cta:hover::after,
  .latest-media-2__cta:hover::after {
    transform: translateX(4px);
  }
}


/* === Card track (middle column) -- horizontally scrolling, snap-aligned === */

/* For events the .upcoming_events_2 element IS the flex container.
   For media the flex container is the inner .MediaList span (Hubb
   wraps items in a <span class="MediaList">). Both paths handled. */
.upcoming_events_2,
.latest_media_2 .MediaList {
  display:           flex;
  gap:               var(--space-5);
  overflow-x:        auto;
  scroll-snap-type:  x mandatory;
  scroll-behavior:   smooth;
  /* Hide native scrollbar -- the next button + touch / keyboard
     navigation cover the interaction, and a visible scrollbar
     fights the clean carousel look. */
  scrollbar-width:   none;
  /* min-width: 0 so flex children can overflow horizontally rather
     than expanding the grid column to fit them. Without this, an
     8-card track at 280px each would push the column to 2240px+
     and break the layout. */
  min-width:         0;
  /* Tiny breathing room below the cards in case a browser keeps a
     thin scrollbar despite the rules above. */
  padding-bottom:    var(--space-1);
}
.upcoming_events_2::-webkit-scrollbar,
.latest_media_2 .MediaList::-webkit-scrollbar {
  display: none;
}

/* Wrapper around .MediaList for media (.latest_media_2 div). Just a
   passthrough so the flex container (.MediaList) sits in the grid
   column directly. min-width: 0 mirrors the rule on the flex
   container itself for the same overflow reason. */
.slice--latest-media:has(> .latest-media-2__intro) > .latest_media_2,
.latest-media:has(> .latest-media-2__intro) > .latest_media_2 {
  min-width: 0;
}

/* Each card -- fixed width with scroll-snap. Image on top, content
   stacked below (calendar tear-off / date pill, title, location /
   speaker). The card shape is intentionally consistent with the
   .upcoming_events / .latest_media 4-col grid cards so the card
   visual reads as one design family across all four layouts; only
   the container differs. */
.upcoming_events_2 > div,
.latest_media_2 .MediaList > div {
  flex:              0 0 280px;
  scroll-snap-align: start;
  display:           flex;
  flex-direction:    column;
  gap:               var(--space-3);
  position:          relative;     /* anchor for stretched-link */
}


/* === Event card content === */

.upcoming_events_2 .eventListImage {
  overflow:      hidden;
  border-radius: var(--radius);
  background:    var(--accent-soft);
  aspect-ratio:  4 / 3;
}
.upcoming_events_2 .eventListImage a {
  display: block;
  height:  100%;
}
.upcoming_events_2 .eventListImage img {
  width:      100%;
  height:     100%;
  object-fit: cover;
  transition: transform 600ms ease;
}
@media (hover: hover) {
  .upcoming_events_2 > div:hover .eventListImage img {
    transform: scale(1.04);
  }
}

/* Placeholder when no image -- inherits the styling from the
   .upcoming_events placeholder injection. */
.upcoming_events_2 .eventListImage__placeholder {
  width:           100%;
  height:          100%;
  display:         flex;
  align-items:     center;
  justify-content: center;
  color:           var(--accent);
  opacity:         0.6;
}
.upcoming_events_2 .eventListImage__placeholder svg {
  width:     35%;
  height:    auto;
  max-width: 80px;
}

/* Date -- calendar tear-off block (parsed by structureEventDates) */
.upcoming_events_2 .eventListDate {
  display:     flex;
  align-items: center;
  gap:         var(--space-2);
  font-family: var(--font-body);
  margin-top:  var(--space-1);
  color:       var(--ink-soft);
}
.upcoming_events_2 .eventListDate .day-month {
  display:        flex;
  flex-direction: column;
  align-items:    center;
  padding:        var(--space-1) var(--space-2);
  border:         1px solid var(--rule);
  border-radius:  var(--radius);
  background:     var(--surface);
  min-width:      44px;
}
.upcoming_events_2 .eventListDate .day {
  font-family:    var(--font-display);
  font-size:      1.25rem;
  font-weight:    500;
  color:          var(--ink);
  line-height:    1;
  letter-spacing: -0.02em;
}
.upcoming_events_2 .eventListDate .month {
  font-family:    var(--font-body);
  font-size:      0.65rem;
  font-weight:    700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color:          var(--accent);
  margin-top:     2px;
  line-height:    1;
}
.upcoming_events_2 .eventListDate .time {
  font-size: var(--text-xs);
  flex:      1;
}

.upcoming_events_2 .eventListTitle {
  font-family:    var(--font-display);
  font-size:      var(--text-lg);
  font-weight:    500;
  line-height:    1.2;
  letter-spacing: -0.01em;
}
.upcoming_events_2 .eventListTitle a {
  color:               var(--ink);
  text-decoration:     none;
  background-image:    linear-gradient(currentColor, currentColor);
  background-size:     0% 1px;
  background-repeat:   no-repeat;
  background-position: 0 100%;
  transition:          background-size 300ms ease;
}
/* Stretched-link overlay -- whole card is clickable to the event popup */
.upcoming_events_2 .eventListTitle a::before {
  content:  "";
  position: absolute;
  inset:    0;
  z-index:  1;
}
@media (hover: hover) {
  .upcoming_events_2 > div:hover .eventListTitle a {
    background-size: 100% 1px;
  }
}

.upcoming_events_2 .eventListLocation {
  margin-top:  auto;     /* pin to bottom of card */
  padding-top: var(--space-1);
  font-size:   var(--text-sm);
  color:       var(--ink-soft);
  position:    relative;
  z-index:     2;        /* above stretched-link so map link works */
}
.upcoming_events_2 .eventListLocation a {
  color:           var(--ink-soft);
  text-decoration: none;
  transition:      color var(--transition);
}
.upcoming_events_2 .eventListLocation a:hover {
  color: var(--accent);
}


/* === Media card content === */

.latest_media_2 .mediaListImage {
  overflow:      hidden;
  border-radius: var(--radius);
  background:    var(--accent-soft);
  aspect-ratio:  4 / 3;
  position:      relative;
}
.latest_media_2 .mediaListImage a {
  display: block;
  height:  100%;
}
.latest_media_2 .mediaListImage img {
  width:      100%;
  height:     100%;
  object-fit: cover;
  transition: transform 600ms ease;
}
@media (hover: hover) {
  .latest_media_2 .MediaList > div:hover .mediaListImage img {
    transform: scale(1.04);
  }
}

/* Play badge -- 44px, same as the 4-col grid version. */
.latest_media_2 .mediaListImage::after {
  content:             "";
  position:            absolute;
  bottom:              var(--space-3);
  right:               var(--space-3);
  width:               44px;
  height:              44px;
  border-radius:       50%;
  background-color:    var(--accent);
  background-image:    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='white'><path d='M8 6v12l12-6z'/></svg>");
  background-position: center;
  background-size:     14px;
  background-repeat:   no-repeat;
  box-shadow:          0 2px 8px rgba(26, 22, 18, 0.25);
  transition:          transform 200ms ease, background-color 200ms ease;
  z-index:             2;
  pointer-events:      none;
}
@media (hover: hover) {
  .latest_media_2 .MediaList > div:hover .mediaListImage::after {
    transform:        scale(1.1);
    background-color: var(--accent-dark);
  }
}

.latest_media_2 .mediaListDate {
  font-family:    var(--font-body);
  font-size:      var(--text-xs);
  font-weight:    700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  /* --accent-dark not --accent: see contrast note on .mediaListDate above */
  color:          var(--accent-dark);
  line-height:    1;
}

.latest_media_2 .mediaListTitle {
  font-family:    var(--font-display);
  font-size:      var(--text-lg);
  font-weight:    500;
  line-height:    1.2;
  letter-spacing: -0.01em;
}
.latest_media_2 .mediaListTitle a {
  color:               var(--ink);
  text-decoration:     none;
  background-image:    linear-gradient(currentColor, currentColor);
  background-size:     0% 1px;
  background-repeat:   no-repeat;
  background-position: 0 100%;
  transition:          background-size 300ms ease;
}
.latest_media_2 .mediaListTitle a::before {
  content:  "";
  position: absolute;
  inset:    0;
  z-index:  1;
}
@media (hover: hover) {
  .latest_media_2 .MediaList > div:hover .mediaListTitle a {
    background-size: 100% 1px;
  }
}

.latest_media_2 .mediaListSpeaker {
  font-family: var(--font-body);
  font-size:   var(--text-sm);
  font-style:  italic;
  color:       var(--ink-soft);
  line-height: 1.4;
}
.latest_media_2 .mediaListSpeaker::before {
  content:    "by ";
  font-style: italic;
}


/* === Carousel viewport === */
/* Wraps the scrolling track + both nav buttons + the edge-fade overlays.
   position: relative anchors the absolute children (buttons, fades).
   overflow: hidden clips the fade-gradient pseudo-elements at the
   viewport edges (without it, the gradients would extend beyond the
   viewport and bleed into the intro column). */
.carousel-viewport {
  position:  relative;
  min-width: 0;     /* allow the inner scroller's flex children to
                       overflow horizontally rather than expanding the
                       grid column to fit them. */
  overflow:  hidden;
}

/* === Edge fade gradients ===
   Pseudo-elements on the viewport, fading from the page background
   (--bg) to transparent. Visible only when there's more content to
   scroll in that direction, controlled by data-can-scroll-* attrs
   that JS toggles based on scroll position. The fades are wide enough
   (60px) to be a clear "more content this way" signal but narrow
   enough not to obscure card content. pointer-events: none lets
   clicks pass through to whatever's underneath (the cards). */
.carousel-viewport::before,
.carousel-viewport::after {
  content:        "";
  position:       absolute;
  top:            0;
  bottom:         0;
  width:          60px;
  pointer-events: none;
  z-index:        3;
  opacity:        0;
  transition:     opacity 200ms ease;
}
.carousel-viewport::before {
  left:       0;
  background: linear-gradient(to right, var(--bg) 0%, rgba(var(--bg-rgb), 0) 100%);
}
.carousel-viewport::after {
  right:      0;
  background: linear-gradient(to left,  var(--bg) 0%, rgba(var(--bg-rgb), 0) 100%);
}
.carousel-viewport[data-can-scroll-left]::before  { opacity: 1; }
.carousel-viewport[data-can-scroll-right]::after  { opacity: 1; }

/* === Carousel nav buttons (prev / next) ===
   Both absolute-positioned, vertically centred, overlaying the
   scroller's edges. Prev hides entirely at the start of scroll;
   next dims to disabled state at the end. */
.carousel__prev,
.carousel__next {
  position:        absolute;
  top:             50%;
  transform:       translateY(-50%);
  z-index:         4;          /* above fades (3) and cards */
  width:           48px;
  height:          48px;
  border-radius:   50%;
  border:          0;
  background:      var(--surface);
  color:           var(--ink);
  cursor:          pointer;
  display:         inline-flex;
  align-items:     center;
  justify-content: center;
  box-shadow:      0 2px 8px rgba(26, 22, 18, 0.15);
  transition:      background var(--transition),
                   color      var(--transition),
                   box-shadow var(--transition),
                   opacity    200ms ease;
}
.carousel__prev { left:  var(--space-3); }    /* 12px in from viewport edges */
.carousel__next { right: var(--space-3); }

.carousel__prev:hover,
.carousel__next:hover {
  background: var(--accent);
  color:      var(--accent-ink);
  box-shadow: 0 4px 12px rgba(26, 22, 18, 0.20);
}
.carousel__prev:focus-visible,
.carousel__next:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 3px;
}
.carousel__next[disabled] {
  opacity:        0.3;
  cursor:         not-allowed;
  pointer-events: none;
}

/* Prev hides entirely when at start (no need to nudge back further).
   Visibility / opacity transition rather than display: none so the
   button transitions in/out smoothly when the user starts/stops
   scrolling. */
.carousel-viewport:not([data-can-scroll-left]) .carousel__prev {
  opacity:        0;
  visibility:     hidden;
  pointer-events: none;
}

.carousel__prev svg,
.carousel__next svg {
  width:           20px;
  height:          20px;
  stroke:          currentColor;
  stroke-width:    2.5;
  fill:            none;
  stroke-linecap:  round;
  stroke-linejoin: round;
}


/* =========================================================================
   3.7b  MODULE: About Carousel  --  .slice--about-carousel  (Gallery module)
   -------------------------------------------------------------------------
   A horizontal rail of tall portrait cards, each an image with an overlaid
   title, accent bar, description and pill CTA. Built for a Gallery module in
   STATIC UL mode (Slideshow Mode OFF) -- the module outputs a plain
   ul.gallery_ul > li.gallery_li list, and setupAboutCarousel() (script.js)
   turns each li into an overlay card by reading the image caption in the
   three-part format:  Title*Description*Button Label.

   The rail itself reuses the shared carousel engine: setupCarousels() adds
   this section's gallery_ul to its scroller list, so the arrows, edge-fades
   and .carousel-viewport wrapper all come for free (see 3.10c infra above).
   The track mirrors .al-cards-rail so the scroll feel matches every other
   rail in the library.

   Card content is always visible (no hover-to-reveal) so it works on touch
   and for AT; hover only scales the image, matching the al-cards-rail family
   -- no content translate (house rule). Overlay text is white on a warm-black
   scrim (same rgba as the hero .gallery_li::after), the accepted on-image
   exception; the accent bar uses --accent so it reskins per site.
   Author pattern in Body Contents:
       section.slice.slice--about-carousel  >  Gallery module (UL mode)
   ========================================================================= */

/* Section band -- light-grey full-bleed band with the standard section
   padding, matching the .slice--light-grey worked examples. Baked into the
   slice (not an opt-in class) because the About rail is designed to sit on
   this band. --bg is rebound to the band colour locally so the SHARED carousel
   edge-fades (.carousel-viewport::before/after fade to --bg) match the band
   instead of smudging white at the rail edges; the nav buttons use --surface
   so they stay light and legible on the grey. Do NOT also add .slice--light-grey
   in Body Contents -- that would paint a second band. */
.slice--about-carousel {
  position:      relative;   /* anchor for the full-bleed band ::before */
  margin-block:  0;          /* flush like the themed bands; neighbours' margins supply the gap */
  padding-top:    var(--space-12);          /* balances the centred title (see head rule below) */
  padding-bottom: var(--section-pad-block);
  --bg:      var(--section-light);
  --bg-rgb:  var(--section-light-rgb);
}
.slice--about-carousel::before {
  content:    "";
  position:   absolute;
  z-index:    -1;            /* behind the slice content */
  top:        0;
  bottom:     0;
  left:       50%;
  width:      100vw;         /* full-bleed band; content stays in its column */
  transform:  translateX(-50%);
  background: var(--section-light);
}

/* Balance the centred title vertically: equal gap above it (the section's
   padding-top) and below it (this margin) -- space-12 each -- so it sits
   equidistant between the band top and the top of the cards. */
.slice--about-carousel .slice__head--centred {
  margin-bottom: var(--space-12);
}

/* Rail track -- horizontal scroll-snap, mirrors the .al-cards-rail track. */
.slice--about-carousel .gallery_ul {
  list-style:       none;
  margin:           0;
  padding-block:    0 var(--space-1);
  /* Slight left offset so the first card starts inset (St Alfege style);
     --about-rail-inset is a single tunable knob a skin can override. */
  padding-inline-start:        var(--about-rail-inset, var(--space-8));
  padding-inline-end:          var(--container-gutter);
  scroll-padding-inline-start: var(--about-rail-inset, var(--space-8));
  display:          flex;
  gap:              var(--space-5);
  overflow-x:       auto;
  scroll-snap-type: x mandatory;
  scroll-behavior:  smooth;
  scrollbar-width:  none;
  min-width:        0;
}
.slice--about-carousel .gallery_ul::-webkit-scrollbar { display: none; }

/* Each card -- tall portrait, image fills, content overlaid at the bottom. */
.slice--about-carousel .gallery_li {
  --about-card-w:    300px;
  position:          relative;
  flex:              0 0 var(--about-card-w);
  scroll-snap-align: start;
  aspect-ratio:      3 / 4;
  overflow:          hidden;
  border-radius:     var(--radius);
  background:        var(--accent-soft);   /* placeholder tone before image loads */
}
.slice--about-carousel .gallery_li img {
  position:   absolute;
  inset:      0;
  width:      100%;
  height:     100%;
  object-fit: cover;
  transition: transform 600ms ease;
}
@media (hover: hover) {
  .slice--about-carousel .gallery_li:hover img { transform: scale(1.04); }
}

/* Bottom scrim for legibility -- warm-black, matching the hero overlay. */
.slice--about-carousel .gallery_li::after {
  content:        "";
  position:       absolute;
  inset:          0;
  background:     linear-gradient(to top,
                    rgba(26, 22, 18, 0.78) 0%,
                    rgba(26, 22, 18, 0.35) 40%,
                    rgba(26, 22, 18, 0)    72%);
  pointer-events: none;
}

/* Card content block (built by setupAboutCarousel from the img caption). */
.slice--about-carousel .about-card__body {
  position:       absolute;
  left:           0;
  right:          0;
  bottom:         0;
  z-index:        2;              /* above the scrim + the stretched link */
  pointer-events: none;           /* clicks pass through to .about-card__link */
  padding:        var(--space-5);
  display:        flex;
  flex-direction: column;
  align-items:    center;
  text-align:     center;
  color:          #FFFFFF;
}
.slice--about-carousel .about-card__title {
  font-family:    var(--font-display);
  font-size:      var(--text-lg);
  font-weight:    500;
  line-height:    1.15;
  letter-spacing: 0.02em;
  text-transform: uppercase;
  color:          #FFFFFF;
  text-shadow:    0 2px 18px rgba(0, 0, 0, 0.35);
}
/* Accent bar under the title -- reskins via --accent. */
.slice--about-carousel .about-card__bar {
  display:    block;
  width:      42px;
  height:     2px;
  margin:     var(--space-3) 0;
  background: var(--accent);
}
.slice--about-carousel .about-card__desc {
  font-family: var(--font-body);
  font-size:   var(--text-sm);
  line-height: 1.5;
  max-width:   30ch;
  color:       rgba(255, 255, 255, 0.92);
  text-shadow: 0 1px 10px rgba(0, 0, 0, 0.4);
}
/* Outline pill CTA -- white frame on the image, solid on hover. */
.slice--about-carousel .about-card__cta {
  display:         inline-block;
  margin-top:      var(--space-4);
  padding:         var(--space-2) var(--space-5);
  border:          1px solid rgba(255, 255, 255, 0.85);
  border-radius:   var(--radius);
  font-family:     var(--font-body);
  font-size:       var(--text-sm);
  font-weight:     500;
  color:           #FFFFFF;
  text-decoration: none;
  transition:      background var(--transition),
                   color      var(--transition),
                   border-color var(--transition);
}
@media (hover: hover) {
  .slice--about-carousel .gallery_li:hover .about-card__cta {
    background:   #FFFFFF;
    color:        var(--ink);
    border-color: #FFFFFF;
  }
}

/* Whole-card stretched link (built when the caption carries a URL). The pill
   above is a visual span only, so the card is a single link -- no nested <a>. */
.slice--about-carousel .about-card__link {
  position:      absolute;
  inset:         0;
  z-index:       1;              /* above image + scrim, below the text body */
  border-radius: inherit;
}
.slice--about-carousel .about-card__link:focus-visible {
  outline:        2px solid #FFFFFF;
  outline-offset: -4px;
}

/* Staggered one-by-one reveal. JS adds .about-reveal-armed (cards hide ONLY
   once JS runs -- a failure never hides them), then .is-revealed when the rail
   enters view; per-card transition-delay is set inline. Opacity-only, because
   a translateY would be clipped by the rail's horizontal overflow. */
.slice--about-carousel .gallery_ul.about-reveal-armed > .gallery_li {
  opacity:    0;
  transition: opacity 900ms var(--ease-out-soft);
}
.slice--about-carousel .gallery_ul.about-reveal-armed.is-revealed > .gallery_li {
  opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
  .slice--about-carousel .gallery_ul.about-reveal-armed > .gallery_li {
    opacity:    1;
    transition: none;
  }
}

/* Fade host: a positioned wrapper (added by JS) that provides the subtle
   left/right edge shading as the rail scrolls -- cards dissolve into the band
   at both ends. The fade colour is --bg, which this slice rebinds to
   --section-light, so it tracks whatever the skin sets for the band. */
.slice--about-carousel .about-carousel__viewport {
  position: relative;
}
.slice--about-carousel .about-carousel__viewport::before,
.slice--about-carousel .about-carousel__viewport::after {
  content:        "";
  position:       absolute;
  top:            0;
  bottom:         0;
  width:          clamp(1.5rem, 5vw, 4rem);
  z-index:        3;                /* above the cards; clicks pass through */
  pointer-events: none;
  opacity:        0;                /* gated by scroll state, like the shared rails */
  transition:     opacity 200ms ease;
}
.slice--about-carousel .about-carousel__viewport::before {
  left:       0;
  background: linear-gradient(to right,
                var(--bg),
                rgba(var(--bg-rgb), 0));
}
.slice--about-carousel .about-carousel__viewport::after {
  right:      0;
  background: linear-gradient(to left,
                var(--bg),
                rgba(var(--bg-rgb), 0));
}
/* At rest (unscrolled) only the right fade shows; the left appears once
   scrolled off the start -- matches the shared rails. */
.slice--about-carousel .about-carousel__viewport[data-can-scroll-left]::before  { opacity: 1; }
.slice--about-carousel .about-carousel__viewport[data-can-scroll-right]::after  { opacity: 1; }

/* Control bar below the rail: prev / dots / next, centred (St Alfege style). */
.slice--about-carousel .about-carousel__nav {
  display:         flex;
  align-items:     center;
  justify-content: center;
  gap:             var(--space-5);
  margin-top:      var(--space-6);
}
.slice--about-carousel .about-carousel__arrow {
  display:         inline-flex;
  align-items:     center;
  justify-content: center;
  width:           2.5rem;
  height:          2.5rem;
  padding:         0;
  border:          0;
  background:      transparent;
  color:           var(--accent);
  cursor:          pointer;
  border-radius:   50%;
  transition:      color var(--transition), background var(--transition);
}
.slice--about-carousel .about-carousel__arrow svg {
  width:           22px;
  height:          22px;
  fill:            none;
  stroke:          currentColor;
  stroke-width:    2;
  stroke-linecap:  round;
  stroke-linejoin: round;
}
@media (hover: hover) {
  .slice--about-carousel .about-carousel__arrow:hover {
    color:      var(--ink);
    background: rgba(0, 0, 0, 0.05);
  }
}
.slice--about-carousel .about-carousel__arrow:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 2px;
}
.slice--about-carousel .about-carousel__dots {
  display:     flex;
  align-items: center;
  gap:         var(--space-3);
}
.slice--about-carousel .about-carousel__dot {
  width:         10px;
  height:        10px;
  padding:       0;
  border:        0;
  border-radius: 50%;
  background:    var(--rule);
  cursor:        pointer;
  transition:    background var(--transition), transform var(--transition);
}
.slice--about-carousel .about-carousel__dot.is-active {
  background: var(--accent);
  transform:  scale(1.15);
}
.slice--about-carousel .about-carousel__dot:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 2px;
}


/* -------------------------------------------------------------------------
   3.7c  MODULE: Testimonials  --  .slice--testimonials  (Blog module, type 22)
   -------------------------------------------------------------------------
   A fading testimonial slideshow. The source is a standard Blog module,
   bound in the Hubb UI to a group of short articles -- one testimonial per
   article. Each article's body is just the quote text, then a line break,
   then the person's name. The article's publish date (Hubb renders it in a
   ".author" div) is intentionally dropped -- it is not part of a testimonial.

   Rendered shape (Blog module, type_id 22):
     section.slice.slice--testimonials
       > div                              (Hubb wrapper, passthrough)
         > .blogcomponent_div             the stage
           > .blogcomponent_articlediv    one slide per article
               > div > .ArticleBody       quote <br> name <div.author>date</div>

   setupTestimonials() in script.js rebuilds each .ArticleBody into a clean
   .testimonial__quote (blockquote) + .testimonial__name (cite), then adds
   .is-testimonial-armed to the stage and cross-fades between slides with a
   prev / dots / next + play-pause control bar.

   NO-JUMP LAYOUT: once armed, every slide is placed in the SAME grid cell
   (grid-template-areas:"stack") so the stage sizes to the TALLEST
   testimonial and shorter ones don't make the section change height as they
   fade in. visibility:hidden (not display:none) keeps inactive slides
   contributing to that sizing while removing them from the tab order and
   the accessibility tree.

   GRACEFUL DEGRADATION: the grid-stack and the hide-inactive rules are
   scoped to .is-testimonial-armed, which only JS adds. With JS off the
   slides simply flow down the page -- every quote readable, just not a
   slideshow -- so a script failure never leaves the section blank. This
   mirrors the About-carousel "hide only once JS is running" principle.

   No module CSS Class is needed -- the .slice--testimonials wrapper is the
   whole switch. With a single article it shows as one static quote and no
   controls are built.
   ------------------------------------------------------------------------- */
.slice--testimonials .slice__head--centred { margin-bottom: var(--space-12); }

/* Reading measure + centring apply always (armed or not). */
.slice--testimonials .blogcomponent_div {
  max-width:     62rem;                       /* ~992px -- wide, readable measure for the quote */
  margin-inline: auto;
}
.slice--testimonials .blogcomponent_articlediv { text-align: center; }

/* Empty Hubb signup-link wrapper that sits after each ArticleBody. */
.slice--testimonials .ArticleRotaSignupLink { display: none; }

/* ---- Armed state (JS is running): stack every slide in one grid cell ---- */
.slice--testimonials .blogcomponent_div.is-testimonial-armed {
  display:             grid;
  grid-template-areas: "stack";
  align-items:         center;
  justify-items:       center;
}
.slice--testimonials .is-testimonial-armed > .blogcomponent_articlediv {
  grid-area:      stack;
  width:          100%;
  opacity:        0;
  visibility:     hidden;
  pointer-events: none;
  transition:     opacity var(--reveal-duration) var(--ease-out-soft),
                  visibility var(--reveal-duration) var(--ease-out-soft);
}
.slice--testimonials .is-testimonial-armed > .blogcomponent_articlediv.is-active {
  opacity:        1;
  visibility:     visible;
  pointer-events: auto;
}

/* ---- The quote ----
   A large decorative opening mark sits above the text. It is generated
   content (already out of the accessibility tree) and aria-hidden by nature. */
.slice--testimonials .testimonial__quote {
  position:    relative;
  margin:      0;
  padding-top: var(--space-12);
  quotes:      none;
}
.slice--testimonials .testimonial__quote::before {
  content:        "\201C";                    /* left double quotation mark */
  position:       absolute;
  top:            calc(-1 * var(--space-2));
  left:           50%;
  transform:      translateX(-50%);
  font-family:    var(--font-display);
  font-size:      4rem;
  line-height:    1;
  color:          var(--accent-soft);
  pointer-events: none;
}
.slice--testimonials .testimonial__quote p {
  margin:         0;
  font-family:    var(--font-display);
  font-size:      var(--text-lg);
  font-weight:    400;
  line-height:    1.5;
  letter-spacing: -0.01em;
  color:          var(--ink);
}

/* ---- Attribution (the em-dash prefix is decorative, added in CSS) ---- */
.slice--testimonials .testimonial__name {
  display:     block;
  margin-top:  var(--space-6);
  font-family: var(--font-body);
  font-size:   var(--text-base);
  font-weight: 600;
  font-style:  normal;
  color:       var(--ink);
}
.slice--testimonials .testimonial__name:empty { display: none; }
.slice--testimonials .testimonial__name::before {
  content: "\2014\00A0";                      /* em dash + non-breaking space */
  color:   var(--ink-soft);
}

/* ---- Control bar: prev / dots / next + play-pause ----
   Visual language mirrors the About-carousel controls (section 3.7b) so the
   two feel like one family, but the rules are scoped separately here. */
.slice--testimonials .testimonial-nav {
  display:         flex;
  align-items:     center;
  justify-content: center;
  gap:             var(--space-5);
  margin-top:      var(--space-8);
}
.slice--testimonials .testimonial-nav__btn {
  display:         inline-flex;
  align-items:     center;
  justify-content: center;
  width:           2.5rem;
  height:          2.5rem;
  padding:         0;
  border:          0;
  background:      transparent;
  color:           var(--accent);
  cursor:          pointer;
  border-radius:   50%;
  transition:      color var(--transition), background var(--transition);
}
.slice--testimonials .testimonial-nav__btn svg {
  width:           22px;
  height:          22px;
  fill:            none;
  stroke:          currentColor;
  stroke-width:    2;
  stroke-linecap:  round;
  stroke-linejoin: round;
}
/* Play/pause glyphs are filled shapes, not stroked chevrons. */
.slice--testimonials .testimonial-nav__btn--playpause svg {
  fill:   currentColor;
  stroke: none;
  width:  18px;
  height: 18px;
}
@media (hover: hover) {
  .slice--testimonials .testimonial-nav__btn:hover {
    color:      var(--ink);
    background: rgba(0, 0, 0, 0.05);
  }
}
.slice--testimonials .testimonial-nav__btn:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 2px;
}
.slice--testimonials .testimonial-nav__dots {
  display:     flex;
  align-items: center;
  gap:         var(--space-3);
}
.slice--testimonials .testimonial-nav__dot {
  width:         10px;
  height:        10px;
  padding:       0;
  border:        0;
  border-radius: 50%;
  background:    var(--rule);
  cursor:        pointer;
  transition:    background var(--transition), transform var(--transition);
}
.slice--testimonials .testimonial-nav__dot.is-active {
  background: var(--accent);
  transform:  scale(1.15);
}
.slice--testimonials .testimonial-nav__dot:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 2px;
}

/* Reduced motion: the cross-fade becomes an instant swap. Auto-rotation is
   already suppressed in JS unless the visitor explicitly presses play. */
@media (prefers-reduced-motion: reduce) {
  .slice--testimonials .is-testimonial-armed > .blogcomponent_articlediv {
    transition: none;
  }
}


/* -------------------------------------------------------------------------
   3.10d  MODULE: Latest Media 2  --  .latest_media_2  (Media List module)
   -------------------------------------------------------------------------
   Settings -> CSS Class = "latest_media_2"

   Horizontal carousel -- same architecture as Upcoming Events 2 above.
   See section 3.10c for the full design rationale and HTML pattern.

   The card (and most of the carousel CSS) is shared with section 3.10c
   via combined selectors -- only the card-content rules are
   module-specific (.mediaListImage / .mediaListDate / .mediaListTitle /
   .mediaListSpeaker), already covered above next to their event
   counterparts. This section header exists for navigability only --
   no rules live here uniquely.
   ------------------------------------------------------------------------- */


/* =========================================================================
   3.10e  MODULE: Latest Media 3  --  .latest_media_3  (Media List module)
   -------------------------------------------------------------------------
   Settings -> CSS Class = "latest_media_3"

   Same item visuals as the v1 .latest_media (4:3 image with play overlay,
   uppercase date tag, display-font title with underline-grow, italic
   "by speaker" byline, hover lift + image scale) but laid out as a 2x2
   grid in a narrower section -- matching the width that
   .explore_church_2 occupies. The wrapper width detection uses the
   same :has() trick as the explore variants, so a single page can mix
   v1 (full 1600px) and v3 (1400px) on the same .latest-media wrapper
   class without conflict.

   Item-level styling (image, date, title, speaker, hover) is shared
   with v1 via :is(.latest_media, .latest_media_3) in section 3.10b
   above. Only the grid layout and section width are unique here.

   Body Contents pattern:
     <section class="latest-media">
       <h2 class="latest-media__heading">Latest Sermons</h2>
       <module alt="Recent Media Uploads" type_id="11"
               cssClass="latest_media_3" />
       <a class="section-link" href="...">See more media</a>
     </section>

   Configure the module to return 4 items so the 2x2 grid is filled.
   ========================================================================= */

/* Narrower wrapper. 1200px matches the base .explore-church
   wrapper width -- a curated-feeling section rather than full
   container-width. (Note: explore_church_2 specifically gets a
   1400px override because its 4-up grid needs extra horizontal
   room; we don't need that here since the 2-up grid breathes
   fine at 1200px.)

   Selector uses :has(.latest_media_3) (descendant) rather than
   :has(> .latest_media_3) (direct child) because Hubb's Media List
   module renders inside asp.net control containers, so the
   .latest_media_3 div isn't always a literal direct child of the
   .latest-media section. The descendant form matches regardless of
   how Hubb wraps things. (Galleries don't get this treatment in
   Hubb's output -- the .explore_church_2 div sits as a direct
   child -- which is why the explore_church_2 :has(> ...) form
   works. Asymmetry is invisible until you inspect rendered DOM.) */
.slice--latest-media:has(.latest_media_3),
.latest-media:has(.latest_media_3) {
  max-width: 1200px;
}

/* 2x2 grid. The .MediaList span container is what Hubb gives us
   (same as v1) -- we make it a 2-col grid; with 4 items configured
   in the module, items naturally wrap to 2 rows.

   minmax(0, 1fr) instead of plain 1fr because flex/grid items have
   a default min-width: auto that can be larger than 1fr if the
   content doesn't fit. Long sermon titles or wide thumbnails could
   otherwise blow out the grid; minmax(0, 1fr) lets them shrink
   correctly. */
.latest_media_3 .MediaList {
  display:               grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap:                   var(--space-8);
}


/* =========================================================================
   3.10f  MODULE: Upcoming Events 3 -- stacked rows  --  .upcoming_events_3
   -------------------------------------------------------------------------
   Settings -> CSS Class = "upcoming_events_3"
   A stacked list of full-width horizontal event cards (the Genesis
   "Featured Events" layout, rebuilt in Tabernacle tokens). Same Calendar
   module as 3.10 (type_id 2, same .eventList* anchors -- see MODULES.md);
   only the layout differs. Reuses structureEventDates() (date badge) and
   addEventImagePlaceholders() from script.js -- their selectors include
   .upcoming_events_3.

   Card layout (desktop):
     [ image + date badge ]  [ title / time / location ]  [ More details ]
   The whole card opens the event popup via a stretched-link on the title;
   the location link is lifted above so map clicks still work. The "More
   details" pill is a CSS affordance (::after) -- Hubb emits no such element
   and the card click already routes to the popup.

   Author pattern in Body Contents:
     <section class="slice slice--upcoming-events">
       <div class="slice__head slice__head--centred">
         <h2 class="slice__heading upcoming-events__heading">Upcoming Events</h2>
       </div>
       <module alt="Upcoming Events" module_id="..." type_id="2"
               cssClass="upcoming_events_3" />
       <a class="section-link" href="/Calendar/Month.aspx">View the calendar</a>
     </section>
   ------------------------------------------------------------------------- */

/* Vertical stack of cards */
.upcoming_events_3 {
  display:        flex;
  flex-direction: column;
  gap:            var(--space-5);
}

/* Each card -- horizontal: image | content | more-details */
.upcoming_events_3 > div {
  display:               grid;
  grid-template-columns: minmax(0, 240px) 1fr auto;
  grid-template-rows:    auto auto auto;
  align-items:           center;
  column-gap:            var(--space-8);
  row-gap:               var(--space-2);
  position:              relative;   /* badge overlay + stretched-link anchor */
  background:            var(--surface);
  border:                1px solid var(--rule);
  border-radius:         var(--radius-lg);
  padding:               var(--space-5);
  transition:            border-color var(--transition);
}
@media (hover: hover) {
  .upcoming_events_3 > div:hover { border-color: var(--ink-soft); }
}

/* Some events carry a summary/description; the stacked card shows only
   title / time / location, so hide them (and stop them auto-placing). */
.upcoming_events_3 .eventListSummary,
.upcoming_events_3 .eventListDescription {
  display: none;
}

/* --- Image (spans the content rows on the left) --- */
.upcoming_events_3 .eventListImage {
  grid-column:   1;
  grid-row:      1 / -1;
  align-self:    stretch;
  min-height:    150px;
  overflow:      hidden;
  border-radius: var(--radius);
  background:    var(--accent-soft);
}
.upcoming_events_3 .eventListImage a { display: block; height: 100%; }
.upcoming_events_3 .eventListImage img {
  width:      100%;
  height:     100%;
  object-fit: cover;
  transition: transform 600ms ease;
}
@media (hover: hover) {
  .upcoming_events_3 > div:hover .eventListImage img { transform: scale(1.04); }
}

/* --- Date badge -- pulled out of .eventListDate and overlaid on the image.
   structureEventDates() splits the raw date into .day-month + .time; the
   .time stays in the content as the time line below. --- */
.upcoming_events_3 .eventListDate .day-month {
  position:       absolute;
  top:            var(--space-6);
  left:           var(--space-6);
  z-index:        2;
  display:        flex;
  flex-direction: column;
  align-items:    center;
  padding:        var(--space-2) var(--space-3);
  border-radius:  var(--radius);
  background:     var(--surface);
  box-shadow:     0 2px 8px rgba(0, 0, 0, 0.12);
  min-width:      48px;
}
.upcoming_events_3 .eventListDate .day {
  font-family:    var(--font-display);
  font-size:      1.5rem;
  font-weight:    500;
  line-height:    1;
  color:          var(--ink);
  letter-spacing: -0.02em;
}
.upcoming_events_3 .eventListDate .month {
  font-size:      0.65rem;
  font-weight:    700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color:          var(--accent);
  margin-top:     2px;
}

/* --- Title (row 1) with a short accent rule beneath, Genesis-style --- */
.upcoming_events_3 .eventListTitle {
  grid-column:    2;
  grid-row:       1;
  font-family:    var(--font-display);
  font-size:      var(--text-xl);
  font-weight:    500;
  line-height:    1.2;
  letter-spacing: -0.01em;
}
.upcoming_events_3 .eventListTitle::after {
  content:    "";
  display:    block;
  width:      var(--space-12);
  height:     2px;
  margin-top: var(--space-3);
  background: var(--accent);
}
.upcoming_events_3 .eventListTitle a {
  color:           var(--ink);
  text-decoration: none;
}
.upcoming_events_3 .eventListTitle a::before {
  content:  "";
  position: absolute;
  inset:    0;
  z-index:  1;            /* whole card opens the event popup */
}

/* --- Time line (row 2) -- accent-dark text colour --- */
.upcoming_events_3 .eventListDate {
  grid-column: 2;
  grid-row:    2;
  font-family: var(--font-body);
  font-size:   var(--text-sm);
  font-weight: 600;
  color:       var(--accent-dark);
}

/* --- Location (row 3) with a pin glyph; lifted above the stretched link --- */
.upcoming_events_3 .eventListLocation {
  grid-column: 2;
  grid-row:    3;
  display:     inline-flex;
  align-items: center;
  gap:         var(--space-2);
  font-size:   var(--text-sm);
  color:       var(--ink-soft);
  position:    relative;
  z-index:     2;
}
.upcoming_events_3 .eventListLocation::before {
  content:             "";
  width:               15px;
  height:              15px;
  flex-shrink:         0;
  background-color:    currentColor;
  -webkit-mask-image:  url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z'/><circle cx='12' cy='10' r='3'/></svg>");
          mask-image:  url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z'/><circle cx='12' cy='10' r='3'/></svg>");
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-size:   contain;
          mask-size:   contain;
}
.upcoming_events_3 .eventListLocation a {
  color:           var(--ink-soft);
  text-decoration: none;
  transition:      color var(--transition);
}
.upcoming_events_3 .eventListLocation a:hover { color: var(--accent); }

/* --- "More details" pill (col 3) -- CSS affordance; the card click does
   the work, so this is decorative and sits under the stretched link. --- */
.upcoming_events_3 > div::after {
  content:        "More details";
  grid-column:    3;
  grid-row:       1 / -1;
  align-self:     center;
  white-space:    nowrap;
  font-family:    var(--font-body);
  font-size:      var(--text-xs);
  font-weight:    700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color:          var(--ink);
  padding:        var(--space-3) var(--space-5);
  border:         1px solid var(--rule);
  border-radius:  999px;
  transition:     background var(--transition), border-color var(--transition);
}
@media (hover: hover) {
  .upcoming_events_3 > div:hover::after {
    background:   var(--accent-soft);
    border-color: var(--accent);
  }
}


/* =========================================================================
   3.10g  MODULE: Latest Media 4 -- stacked rows  --  .latest_media_4
   -------------------------------------------------------------------------
   Settings -> CSS Class = "latest_media_4"
   Sermon version of the stacked "Featured Events" layout (3.10f), same shape
   in Tabernacle tokens. Same Media List module as 3.10b (type_id 11, same
   .mediaList* anchors -- see MODULES.md); items are nested in span.MediaList,
   so the stack lives on .MediaList. Reuses structureEventDates() for the date
   badge and the shared play-overlay icon.

   Card layout (desktop):
     [ image + date badge + play badge ]  [ title / speaker ]  [ Play ]
   The whole card opens the media player via a stretched-link on the title
   (javascript:playMedia). The "Play" pill is a CSS affordance.

   Author pattern in Body Contents:
     <section class="slice slice--latest-media">
       <div class="slice__head slice__head--centred">
         <h2 class="slice__heading latest-media__heading">Latest Sermons</h2>
       </div>
       <module alt="Latest Media" module_id="..." type_id="11"
               cssClass="latest_media_4" />
       <a class="section-link" href="/Groups/.../Sermon_Series.aspx">See more media</a>
     </section>
   ------------------------------------------------------------------------- */

/* Vertical stack of cards (the list lives inside Hubb's span.MediaList) */
.latest_media_4 .MediaList {
  display:        flex;
  flex-direction: column;
  gap:            var(--space-5);
}

/* Each card -- horizontal: image | content | play */
.latest_media_4 .MediaList > div {
  display:               grid;
  grid-template-columns: minmax(0, 240px) 1fr auto;
  grid-template-rows:    auto auto;
  align-items:           center;
  column-gap:            var(--space-8);
  row-gap:               var(--space-2);
  position:              relative;   /* badge overlay + stretched-link anchor */
  background:            var(--surface);
  border:                1px solid var(--rule);
  border-radius:         var(--radius-lg);
  padding:               var(--space-5);
  transition:            border-color var(--transition);
}
@media (hover: hover) {
  .latest_media_4 .MediaList > div:hover { border-color: var(--ink-soft); }
}

/* --- Image (spans the content rows on the left). The shared play-overlay
   icon (bottom-right) is added via the :is() group above. --- */
.latest_media_4 .mediaListImage {
  grid-column:   1;
  grid-row:      1 / -1;
  align-self:    stretch;
  min-height:    150px;
  overflow:      hidden;
  border-radius: var(--radius);
  background:    var(--accent-soft);
  position:      relative;   /* anchor for the play-overlay ::after */
}
.latest_media_4 .mediaListImage a { display: block; height: 100%; }
.latest_media_4 .mediaListImage img {
  width:      100%;
  height:     100%;
  object-fit: cover;
  transition: transform 600ms ease;
}
@media (hover: hover) {
  .latest_media_4 .MediaList > div:hover .mediaListImage img { transform: scale(1.04); }
}

/* --- Date badge -- structureEventDates() rebuilds .mediaListDate into
   .day-month (+ empty .time, since media dates carry no time). We take the
   wrapper out of the grid flow (display:contents), hide any time, and overlay
   the badge on the image. --- */
.latest_media_4 .mediaListDate { display: contents; }
.latest_media_4 .mediaListDate .time { display: none; }
.latest_media_4 .mediaListDate .day-month {
  position:       absolute;
  top:            var(--space-6);
  left:           var(--space-6);
  z-index:        2;
  display:        flex;
  flex-direction: column;
  align-items:    center;
  padding:        var(--space-2) var(--space-3);
  border-radius:  var(--radius);
  background:     var(--surface);
  box-shadow:     0 2px 8px rgba(0, 0, 0, 0.12);
  min-width:      48px;
}
.latest_media_4 .mediaListDate .day {
  font-family:    var(--font-display);
  font-size:      1.5rem;
  font-weight:    500;
  line-height:    1;
  color:          var(--ink);
  letter-spacing: -0.02em;
}
.latest_media_4 .mediaListDate .month {
  font-size:      0.65rem;
  font-weight:    700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color:          var(--accent);
  margin-top:     2px;
}

/* --- Title (row 1) with a short accent rule beneath --- */
.latest_media_4 .mediaListTitle {
  grid-column:    2;
  grid-row:       1;
  font-family:    var(--font-display);
  font-size:      var(--text-xl);
  font-weight:    500;
  line-height:    1.2;
  letter-spacing: -0.01em;
}
.latest_media_4 .mediaListTitle::after {
  content:    "";
  display:    block;
  width:      var(--space-12);
  height:     2px;
  margin-top: var(--space-3);
  background: var(--accent);
}
.latest_media_4 .mediaListTitle a {
  color:           var(--ink);
  text-decoration: none;
}
.latest_media_4 .mediaListTitle a::before {
  content:  "";
  position: absolute;
  inset:    0;
  z-index:  1;            /* whole card opens the player */
}

/* --- Speaker byline (row 2) --- */
.latest_media_4 .mediaListSpeaker {
  grid-column: 2;
  grid-row:    2;
  font-family: var(--font-body);
  font-size:   var(--text-sm);
  font-style:  italic;
  color:       var(--ink-soft);
  line-height: 1.4;
}
.latest_media_4 .mediaListSpeaker::before {
  content:    "by ";
  font-style: italic;
}

/* --- "Play" pill (col 3) -- CSS affordance; the card click plays. --- */
.latest_media_4 .MediaList > div::after {
  content:        "Play";
  grid-column:    3;
  grid-row:       1 / -1;
  align-self:     center;
  white-space:    nowrap;
  font-family:    var(--font-body);
  font-size:      var(--text-xs);
  font-weight:    700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color:          var(--ink);
  padding:        var(--space-3) var(--space-5);
  border:         1px solid var(--rule);
  border-radius:  999px;
  transition:     background var(--transition), border-color var(--transition);
}
@media (hover: hover) {
  .latest_media_4 .MediaList > div:hover::after {
    background:   var(--accent-soft);
    border-color: var(--accent);
  }
}


/* -------------------------------------------------------------------------
   3.11  MODULE: Feature Section  --  <feature> element
   -------------------------------------------------------------------------
   A full-bleed parallax section to break up two content blocks -- designed
   to sit between "Latest News" and "Upcoming Events" on the homepage.
   Large background image with a subtle dark overlay for legibility, plus
   centred title / description / CTA -- all editable directly in the
   template HTML.

   Why text in HTML (not in the image caption):
     The Image module puts the image's *Title* into the alt attribute,
     not the Caption. So caption text isn't available to JS at runtime.
     We could put the long-form text in the Title field, but that field
     is conventionally a short identifier and Hubb uses it elsewhere.
     Easier and less surprising: text lives in the template HTML; the
     <module> tag just provides the picture and the link.

   Hubb setup:
     Module type:     Image
     Show:            Fixed image -> select your uploaded image
     Text overlay:    None
     Link:            Link from image  (uses image's own link)
     CSS Class:       leave blank (the <feature> wrapper does the styling)

   Body Contents pattern:
     <feature>
       <module alt="Image" module_id="..." type_id="4" />
       <div class="feature__text">
         <h2 class="feature__title">Sunday Services</h2>
         <p class="feature__description">Are you new to the church...</p>
         <a class="feature__cta" href="/Group/Group.aspx?ID=...">Learn More</a>
       </div>
     </feature>

   Rendered HTML (Hubb fills the <module> in):
     <feature>
       <div class="hubb-image-module"><a href="..."><img src="..." alt="..."></a></div>
       <div class="feature__text"> ...your text and CTA... </div>
     </feature>

   JS (setupFeatureSection in script.js):
     - Finds each <feature> element
     - Reads the img's src and applies it as the <feature>'s background
     - The <div class="hubb-image-module"> wrapper is hidden via CSS
       (kept in DOM so img alt is still read by screen readers and the
       link is still followable as a fallback)

   Parallax effect uses background-attachment: fixed on desktop. On
   mobile that's buggy/janky (especially iOS Safari), so we fall back
   to a static scroll background below 720px.

   Reusability: any time you want a similar full-bleed image section
   anywhere on the site, wrap an Image module + text block in
   <feature>...</feature> and you'll get the same treatment.
   ------------------------------------------------------------------------- */

.slice--feature,
feature {
  /* <feature> was the v0.x custom element wrapper; .slice--feature
     is the v0.130 canonical class. Both kept for backward compat;
     the legacy <feature> form removable in v0.131 once Body Contents
     is migrated everywhere.
     Custom element <feature> needs display:flex applied since custom
     elements default to inline; .slice--feature inherits .slice's
     position:relative but needs flex layout overriding the slice
     base's block default. */
  display: flex;
  align-items:     center;
  justify-content: center;

  /* Break out of the container and span the full viewport width.
     The .slice base sets max-width + padding-inline -- override
     both here so the feature can be full-bleed. */
  max-width:      none;
  padding-inline: 0;
  margin-inline:  calc(50% - 50vw);
  /* margin-block scales with viewport -- 32px floor on phones,
     64px ceiling on big desktops. */
  margin-block:  clamp(var(--space-8), 6vw, var(--space-16));

  position:   relative;
  min-height: clamp(420px, 60vh, 680px);
  /* Padding scales smoothly: vertical 48-96px, horizontal 24-48px.
     This keeps text comfortably away from the edges at every viewport
     size -- the v0.45 fixed values made the inner block feel cramped
     against the section edge as the device narrowed. */
  padding:    clamp(var(--space-12), 8vw, var(--space-24))
              clamp(var(--space-6), 5vw, var(--space-12));
  overflow:   hidden;

  background-color: var(--ink);   /* fallback before bg-image element loads */
}

/* Background image element -- injected by JS (setupFeatureSection in
   script.js) as the first child of <feature>. Sized larger than the
   container so it has room to translateY for the parallax effect
   without ever exposing the section's edges. JS updates --parallax-y
   on scroll; we transform translate3d() for GPU-accelerated movement.

   Why a real <div> instead of background-attachment: fixed:
     - background-attachment: fixed is unreliable: broken on iOS Safari,
       janky on retina displays, and gets disabled by some browsers
       under low memory or battery saver. A scroll-driven transform
       on a real element is rock solid across the board.
     - We get to control the parallax intensity precisely (currently
       +/-60px) instead of being locked to viewport scroll.
     - Falls back gracefully to a static background when JS is off
       or prefers-reduced-motion is set. */
.feature__bg {
  position: absolute;
  inset:    -8% 0;                /* extra height (top & bottom) for parallax travel */
  background-size:     cover;
  background-position: center;
  background-repeat:   no-repeat;
  z-index:  0;
  will-change: transform;
  transform: translate3d(0, var(--parallax-y, 0), 0);
}

/* Dark overlay sits between bg image and text. Lighter than v0.35
   because pure white text doesn't need a heavy curtain -- the image
   should still feel like the image, not a flat dark backdrop. */
.slice--feature::before,
feature::before {
  content:        "";
  position:       absolute;
  inset:          0;
  background:     linear-gradient(135deg, rgba(0,0,0,0.50), rgba(0,0,0,0.30));
  pointer-events: none;
  z-index:        1;
}

/* Hide Hubb's image module wrapper -- we use its src to populate
   .feature__bg via JS. Kept in the DOM (not display:none) so screen
   readers still get the alt text, and the link is present for SEO. */
.slice--feature .hubb-image-module,
feature .hubb-image-module {
  position:       absolute;
  width:          1px;
  height:         1px;
  opacity:        0;
  overflow:       hidden;
  pointer-events: none;
}

/* Text block sits on top of the overlay.
   Setting white at the parent so it cascades to anything inside that
   doesn't override (e.g. a stray <strong> or <em> in the description). */
.feature__text {
  position:   relative;
  z-index:    2;
  max-width:  640px;
  text-align: center;
  color:      #ffffff;
}

/* Explicit white text for both title and description.
   !important is used here deliberately (and reluctantly): Hubb often
   wraps modules in containers like .hubb-wrap or .ArticleBody whose
   `h2/p { color: var(--ink) }` rules can tie or beat ours on
   specificity. Legibility of overlaid text is non-negotiable, so we
   set a hard floor here. This is the ONLY place in the whole stylesheet
   that uses !important for color -- keep it that way. */
feature .feature__title,
.feature__title {
  margin:         0 0 var(--space-12);                /* match description margin-bottom -- equal vertical rhythm title->description->button */
  font-family:    var(--font-display);
  /* Tier-2 heading (peer of welcome article H1). 36-60px range
     keeps the feature section's prominence consistent with the
     page H1 -- both anchor major content blocks below the hero. */
  font-size:      clamp(2.25rem, 4.5vw + 1rem, 3.75rem);
  font-weight:    500;
  line-height:    1.15;
  letter-spacing: -0.01em;
  color:          #ffffff !important;
  text-shadow:    0 2px 16px rgba(0, 0, 0, 0.5);
}

feature .feature__description,
.feature__description {
  margin:      0 0 var(--space-12);                   /* generous space below description -- matches title-to-description rhythm */
  font-family: var(--font-body);
  font-size:   var(--text-base);
  line-height: 1.7;
  color:       #ffffff !important;
  opacity:     1;
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.5);
}

/* CTA button -- matches the .ArrowToRight button in the welcome article
   for visual consistency across the page. Pill-shaped, generous padding,
   small uppercase label, animated -> arrow that nudges right on hover.

   Difference from the welcome version: the welcome button hovers to
   dark ink (works on a cream background). On the feature section we're
   already on a dark image, so we hover to accent-dark instead -- keeps
   the button visible without disappearing into the overlay. */
feature .feature__cta,
.feature__cta {
  display:         inline-flex;
  align-items:     center;
  gap:             var(--space-3);
  padding:         var(--space-3) var(--space-6);
  background:      var(--accent);
  color:           var(--accent-ink) !important;
  font-family:     var(--font-body);
  font-weight:     700;
  font-size:       var(--text-xs);
  letter-spacing:  0.1em;
  text-transform:  uppercase;
  text-decoration: none;
  border-radius:   999px;
  transition:      background 0.2s ease, opacity 0.2s ease;
}
feature .feature__cta::after,
.feature__cta::after {
  content:    "->";
  font-size:  1.15em;
  /* v0.143: see .welcome_article .ArrowToRight::after for rationale. */
  transform:  translateY(-0.08em);
  transition: transform 0.2s ease;
}
feature .feature__cta:focus-visible,
.feature__cta:focus-visible {
  background: var(--accent-dark);
  color:      var(--accent-ink) !important; /* beats Hubb base.min.css a { color } at 0,0,1 */
  opacity:    1;
}
feature .feature__cta:active,
.feature__cta:active {
  background: var(--accent-dark);
  transform:  translateY(1px);
}
@media (hover: hover) {
  feature .feature__cta:hover,
  .feature__cta:hover {
    background: var(--accent-dark);
    color:      var(--accent-ink) !important; /* beats Hubb base.min.css a { color } at 0,0,1 */
    opacity:    1;
  }
  feature .feature__cta:hover::after,
  feature .feature__cta:focus-visible::after,
  .feature__cta:hover::after,
  .feature__cta:focus-visible::after {
    transform: translate(3px, -0.08em);   /* X slide + preserves baseline Y nudge */
  }
}

/* Respect prefers-reduced-motion -- disable parallax movement */
@media (prefers-reduced-motion: reduce) {
  .feature__bg {
    transform: none !important;
    will-change: auto;
  }
}


/* =========================================================================
   3.11b  MODULE: Feature Split -- image + text, two columns
           --  .slice--feature-split  (+ .slice--feature-split--reverse)
   -------------------------------------------------------------------------
   A two-column "media beside text" feature (the Origin "Sunday Services"
   layout) in Tabernacle tokens. Unlike the full-bleed parallax .slice--feature
   (3.11), this sits inside the editorial column on the page background and
   shows the image as a real block beside a text column with a heading, a bold
   lead line, body copy and a two-button group.

   Two orientations:
     .slice--feature-split                                 -> image LEFT,  text RIGHT
     .slice--feature-split.slice--feature-split--reverse   -> image RIGHT, text LEFT
   Both stack to image-on-top on mobile.

   The image is a Hubb Image module (type_id 4) wrapped in an authored
   .feature-split__media div, so the image is styled via that wrapper rather
   than Hubb's per-install output class. No JS -- pure layout (it does NOT
   trigger the parallax setupFeatureSection, which only matches .slice--feature).

   Buttons: .feature-split__cta is the solid accent pill; add --ghost for the
   outlined secondary. Drop one if a single CTA is enough.

   Body Contents pattern (image left):
     <section class="slice slice--feature-split">
       <div class="feature-split__media">
         <module alt="Image" module_id="..." type_id="4" />
       </div>
       <div class="feature-split__text">
         <h2 class="feature-split__title">Sunday Services</h2>
         <p class="feature-split__lead">We meet every Sunday at 10.00am...</p>
         <p class="feature-split__description">As we gather together...</p>
         <div class="feature-split__actions">
           <a class="feature-split__cta feature-split__cta--ghost" href="...">Sundays</a>
           <a class="feature-split__cta" href="...">What's on</a>
         </div>
       </div>
     </section>
   For image right, add the --reverse modifier to the section class.
   ------------------------------------------------------------------------- */

.slice--feature-split {
  display:               grid;
  grid-template-columns: 1fr 1fr;
  align-items:           center;
  gap:                   clamp(var(--space-8), 5vw, var(--space-16));
}

/* Reverse orientation -- flip the columns via order. DOM stays media-first,
   so screen readers and the mobile stack keep image-then-text. */
.slice--feature-split--reverse .feature-split__media { order: 2; }
.slice--feature-split--reverse .feature-split__text  { order: 1; }

/* --- Media column --- */
.feature-split__media img {
  display:       block;
  width:         100%;
  height:        auto;
  border-radius: var(--radius-lg);
}

/* --- Text column --- */
.feature-split__title {
  margin:         0 0 var(--space-5);
  font-family:    var(--font-display);
  font-size:      clamp(1.75rem, 3vw + 0.5rem, 2.75rem);
  font-weight:    500;
  line-height:    1.15;
  letter-spacing: -0.01em;
  color:          var(--ink);
}
.feature-split__lead {
  margin:      0 0 var(--space-5);
  font-family: var(--font-body);
  font-size:   var(--text-lg);
  font-weight: 700;
  line-height: 1.5;
  color:       var(--ink);
}
.feature-split__description {
  margin:      0 0 var(--space-4);
  font-family: var(--font-body);
  font-size:   var(--text-base);
  line-height: 1.7;
  color:       var(--ink-soft);
}
.feature-split__description:last-of-type { margin-bottom: 0; }
.feature-split__description a {
  color:                 var(--ink);
  text-decoration:       underline;
  text-underline-offset: 2px;
  transition:            color var(--transition);
}
.feature-split__description a:hover { color: var(--accent-dark); }

/* --- Button group --- */
.feature-split__actions {
  display:    flex;
  flex-wrap:  wrap;
  gap:        var(--space-4);
  margin-top: var(--space-8);
}
.feature-split__cta {
  display:         inline-flex;
  align-items:     center;
  padding:         var(--space-3) var(--space-6);
  background:      var(--accent);
  color:           var(--accent-ink);
  font-family:     var(--font-body);
  font-weight:     700;
  font-size:       var(--text-xs);
  letter-spacing:  0.1em;
  text-transform:  uppercase;
  text-decoration: none;
  border:          1px solid var(--accent);
  border-radius:   999px;
  transition:      background var(--transition), border-color var(--transition), color var(--transition);
}
/* Ghost / secondary -- outline on the light page background. Listed after the
   base so it wins the equal-specificity tie for the shared .feature-split__cta. */
.feature-split__cta--ghost {
  background: transparent;
  color:      var(--ink);
  border:     1px solid var(--rule);
}
@media (hover: hover) {
  .feature-split__cta:hover {
    background:   var(--accent-dark);
    border-color: var(--accent-dark);
    color:        var(--accent-ink);
  }
  .feature-split__cta--ghost:hover {
    background:   var(--accent-soft);
    border-color: var(--accent);
    color:        var(--ink);
  }
}
.feature-split__cta:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 2px;
}


/* =========================================================================
   3.11c  MODULE: Article Feature -- image + summary, two columns
           --  .feature_1 (image left)  /  .feature_2 (image right)
   -------------------------------------------------------------------------
   An article-driven feature section: ONE article shown as a large two-column
   teaser (image one side; title, summary and a Read more button the other).
   All content is managed in the article (summary image, title, Summary field,
   and the link to the article page) -- nothing lives in the template, so
   editors control it without touching Body Contents.

   Source module: Articles List (type_id 1) set to show a single article.
   Set the module's CSS Class to:
     feature_1   -> image LEFT,  text RIGHT
     feature_2   -> image RIGHT, text LEFT
   Each is a single class -- shared styling is grouped via :is() below, so no
   space-separated modifier is needed in Hubb's CSS Class field.

   Rendered output (Articles List, one item):
     <div class="feature_1">
       <div id="...">                                  <- the article (the card)
         <div class="articleListImage"><a href="/Articles/.../X.aspx"><img ...></a></div>
         <div class="articleListTitle"><a href="/Articles/.../X.aspx">Title</a></div>
         <div class="articleListSummary"><span>Summary text</span></div>
         <div class="articleListLink">Read more</div>   <- plain text, not a link
       </div>
     </div>

   The whole card opens the article via a stretched-link on the title (the
   image anchor points to the same page). "Read more" is Hubb's plain-text
   .articleListLink, restyled as the CTA -- the card click does the work.

   Reusability: any single-article teaser can become a feature -- point an
   Articles List at one article and give it feature_1 or feature_2.
   ------------------------------------------------------------------------- */

/* Outer wrapper -- this module is dropped in bare (not inside a .slice), so
   it carries its own container constraint. Capped well below the 1600px
   editorial column: a single feature with a short summary reads far better
   contained than full-bleed. No .slice wrapper is needed in Body Contents. */
:is(.feature_1, .feature_2) {
  max-width:      1300px;
  margin-inline:  auto;
  /* These are major content blocks that often sit directly next to each other
     (e.g. Sunday Services then Get Involved). The base 64px slice rhythm made
     adjacent features read as squashed, so this family gets more room: 64px
     floor, scaling to 96px on wide desktops. Vertical margins still collapse,
     so the gap between two adjacent features is one of these values, not the
     sum. The floor keeps mobile spacing as-is (features are full-width there). */
  margin-block:   clamp(var(--space-16), 6vw, var(--space-24));
  padding-inline: var(--container-gutter);
}

/* Card -- two columns: image one side, text stack the other. The text column
   is given a little more room than the image. align-items:center keeps the
   text/button block vertically centred against the image rather than pinned
   to the top. */
:is(.feature_1, .feature_2) > div {
  display:               grid;
  grid-template-columns: 1fr 1.25fr;
  grid-template-rows:    auto auto auto;
  align-items:           center;
  column-gap:            clamp(var(--space-8), 4vw, var(--space-16));
  row-gap:               var(--space-3);
  position:              relative;   /* stretched-link anchor */
}

/* --- Image -- fixed 2:1 ratio (matches an 800x400 summary image) with
   object-fit cover, so a taller source is cropped to a clean landscape and
   can't blow the section out vertically. Spans the rows, centred. --- */
:is(.feature_1, .feature_2) .articleListImage {
  grid-column:   1;
  grid-row:      1 / -1;
  align-self:    center;
  width:         100%;
  aspect-ratio:  2 / 1;
  overflow:      hidden;
  border-radius: var(--radius-lg);
  background:    var(--accent-soft);
}
:is(.feature_1, .feature_2) .articleListImage a { display: block; height: 100%; }
:is(.feature_1, .feature_2) .articleListImage img {
  display:    block;
  width:      100%;
  height:     100%;
  object-fit: cover;
  transition: transform 600ms ease;
}
@media (hover: hover) {
  :is(.feature_1, .feature_2) > div:hover .articleListImage img { transform: scale(1.03); }
}

/* --- Title (row 1) + short accent rule --- */
:is(.feature_1, .feature_2) .articleListTitle {
  grid-column:    2;
  grid-row:       1;
  font-family:    var(--font-display);
  font-size:      clamp(1.75rem, 3vw + 0.5rem, 2.75rem);
  font-weight:    500;
  line-height:    1.15;
  letter-spacing: -0.01em;
}
:is(.feature_1, .feature_2) .articleListTitle::after {
  content:    "";
  display:    block;
  width:      var(--space-12);
  height:     2px;
  margin-top: var(--space-4);
  background: var(--accent);
}
:is(.feature_1, .feature_2) .articleListTitle a {
  color:           var(--ink);
  text-decoration: none;
}
:is(.feature_1, .feature_2) .articleListTitle a::before {
  content:  "";
  position: absolute;
  inset:    0;
  z-index:  1;            /* whole card opens the article */
}

/* --- Summary (row 2) --- */
:is(.feature_1, .feature_2) .articleListSummary {
  grid-column: 2;
  grid-row:    2;
  font-family: var(--font-body);
  font-size:   var(--text-lg);
  line-height: 1.6;
  color:       var(--ink-soft);
}

/* --- "Read more" pill (row 3) -- Hubb's plain-text .articleListLink,
   restyled as the CTA. The card click does the work. --- */
:is(.feature_1, .feature_2) .articleListLink {
  grid-column:    2;
  grid-row:       3;
  justify-self:   start;
  margin-top:     var(--space-2);
  display:        inline-flex;
  align-items:    center;
  padding:        var(--space-3) var(--space-6);
  background:     var(--accent);
  color:          var(--accent-ink);
  font-family:    var(--font-body);
  font-weight:    700;
  font-size:      var(--text-xs);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  border-radius:  999px;
  transition:     background var(--transition);
}
@media (hover: hover) {
  :is(.feature_1, .feature_2) > div:hover .articleListLink { background: var(--accent-dark); }
}

/* --- Reverse orientation (.feature_2): image right, text left.
   Same specificity as the base placement above, listed after it so it wins
   the source-order tie. --- */
.feature_2 > div { grid-template-columns: 1.25fr 1fr; }
.feature_2 .articleListImage { grid-column: 2; }
.feature_2 .articleListTitle,
.feature_2 .articleListSummary,
.feature_2 .articleListLink { grid-column: 1; }


/* =========================================================================
   3.11d  MODULE: Giving Advert  --  .slice--giving-advert
   -------------------------------------------------------------------------
   A full-bleed appeal band: a background image runs edge to edge and a card
   floats on top -- a photo on the left, a dark panel on the right carrying a
   headline, a short summary, a set of "what your gift buys" amount rows, and
   a Donate button.

   ONE Article module (type_id 10) holds EVERYTHING -- both images and all the
   copy -- so a client edits a single article and never opens the template. The
   template is just the section wrapper around this one module. The article
   renders <div><div class="ArticleBody">...</div> <div
   class="ArticleRotaSignupLink"></div></div>; styling hangs off .ArticleBody so
   no CSS Class field is needed. The two images (each an Image module embed) are
   the first two things in the body, in this order:
     1st image -> the card PHOTO (left column)
     2nd image -> the BACKGROUND (full-bleed, behind the card)
   Both are layered by CSS; see the card block below.

   How the article is authored (all standard editor actions -- no HTML):
     - two images at the very top (photo first, then     -> 1st = left photo
       the background)                                      2nd = the backdrop
     - a Heading (1 or 2)                                -> the headline
                                                            (italicise a word to
                                                            tint it the figure
                                                            colour)
     - a short intro paragraph                           -> the summary line
     - a 2-column TABLE: left cell = amount, right cell  -> the amount rows
       = description, one row per gift                      (add/remove rows to
                                                            add/remove gifts)
     - a Donate link on its own line                     -> the button
   The table is the whole trick: clients edit cells, columns auto-align, and a
   text edit can't break the layout. No bold or special formatting required.

   Re-skin: every colour is a component-scoped variable on the section, so a
   branded site overrides just these -- e.g. a navy panel, blue figures and a
   pink button to match a client's brand:
       .slice--giving-advert {
         --giving-panel:  #262a5e;
         --giving-figure: #29a3dd;
         --giving-btn-bg: #e6007e;  --giving-btn-ink: #ffffff;
       }
   Vanilla defaults: charcoal panel, white figures, a white button.

   No JS -- the table gives the row structure; everything is pure CSS.

   Body Contents pattern (template stays this simple -- no image modules here):
     <section class="slice slice--giving-advert">
       <module alt="Article" module_id="..." type_id="10" />
     </section>
   ------------------------------------------------------------------------- */

.slice--giving-advert {
  /* ---- Re-skin surface: override THESE (only) per brand ---- */
  --giving-panel:        var(--section-dark);          /* right-hand panel background */
  --giving-panel-ink:    #ffffff;                      /* headline + amount description text */
  --giving-panel-muted:  rgba(255, 255, 255, 0.72);    /* summary text */
  --giving-rule:         rgba(255, 255, 255, 0.18);    /* hairlines between amount rows */
  --giving-figure:       #ffffff;                      /* big money figures + tinted heading word */
  --giving-btn-bg:       var(--accent-ink);            /* button fill  (vanilla: white) */
  --giving-btn-ink:      var(--ink);                   /* button label (vanilla: dark)  */
  --giving-btn-bg-hover: rgba(255, 255, 255, 0.85);
  --giving-photo-width:  42%;                          /* left photo's share of the card (desktop) */
  --giving-pad-block:    clamp(var(--space-12), 6vw, var(--space-24)); /* section padding-block AND bg overshoot -- one token so they stay in sync */

  /* Full-bleed band: break out of the editorial column so the background image
     (authored INSIDE the article, see the card block below) can run edge to
     edge. main { overflow-x: clip } + this section's overflow:hidden absorb the
     100vw overshoot. */
  max-width:       none;
  padding-inline:  var(--container-gutter);
  margin-inline:   calc(50% - 50vw);
  margin-block:    clamp(var(--space-8), 6vw, var(--space-16));
  padding-block:   var(--giving-pad-block);

  position:        relative;
  overflow:        hidden;             /* clip the 100vw background image to the band */
  display:         block;
  background-color: var(--section-light);   /* fallback tone before the bg image paints */
}

/* Hide the empty node Hubb appends after the article body. The card centres
   itself with margin-inline:auto below, so the article module's bare wrapper
   div can stay a normal block -- no display:contents needed (that chain failed
   to constrain the card in the live environment, letting it stretch wide). */
.slice--giving-advert .ArticleRotaSignupLink { display: none; }

/* ---- The card = .ArticleBody -----------------------------------------------
   BOTH images live inside the article, so a client edits ONE article and never
   touches the template. Order in the article body:
     - div:nth-of-type(1) = the card PHOTO  (left column of the card)
     - div:nth-of-type(2) = the BACKGROUND  (full-bleed, behind the whole card)
   The dark card panel is drawn by ::before (NOT the ArticleBody background), so
   the background image can sit behind the panel while the panel stays opaque in
   front. ArticleBody is a local stacking context (z-index:0) so the layers
   order cleanly: background (-2) < panel (-1) < photo + copy. overflow stays
   VISIBLE so the 100vw background can bleed past the centred card. ---- */
.slice--giving-advert .ArticleBody {
  position:        relative;
  z-index:         0;                  /* local stacking context for the layers below */
  width:           100%;
  max-width:       1200px;
  margin-inline:   auto;               /* centre the card */
  min-height:      clamp(260px, 22vw, 400px);
  display:         flex;
  flex-direction:  column;
  justify-content: center;
  color:           var(--giving-panel-ink);
  padding:         clamp(var(--space-6), 3vw, var(--space-12));
  /* min() caps the photo column so the copy can't be pushed far right. */
  padding-left:    calc(min(var(--giving-photo-width), 520px) + clamp(var(--space-6), 3vw, var(--space-12)));
}

/* The dark card panel -- a layer behind the copy + photo, in front of the bg. */
.slice--giving-advert .ArticleBody::before {
  content:       "";
  position:      absolute;
  inset:         0;
  z-index:       -1;
  background:    var(--giving-panel);
  border-radius: var(--radius-lg);
  box-shadow:    0 30px 60px -30px rgba(0, 0, 0, 0.45);
}

/* Card PHOTO (1st image in the article) -- pinned to the card's left column.
   Its own left corners are rounded to match the panel (the card doesn't clip,
   so the background can bleed past it); the <img> is pinned to fill it. */
.slice--giving-advert .ArticleBody > div:nth-of-type(1) {
  position:      absolute;
  top:           0;
  bottom:        0;
  left:          0;
  width:         min(var(--giving-photo-width), 520px);
  margin:        0;
  overflow:      hidden;
  border-radius: var(--radius-lg) 0 0 var(--radius-lg);
}
.slice--giving-advert .ArticleBody > div:nth-of-type(1) img {
  position:   absolute;
  inset:      0;
  width:      100%;
  height:     100%;
  object-fit: cover;
  display:    block;
}

/* BACKGROUND image (2nd image in the article) -- a 100vw layer behind the
   panel, sized to the exact band height via the section's padding token so
   object-fit:cover frames it correctly. The section's overflow:hidden clips it
   to the band (and prevents horizontal scroll). */
.slice--giving-advert .ArticleBody > div:nth-of-type(2) {
  position:  absolute;
  left:      50%;
  transform: translateX(-50%);
  width:     100vw;
  top:       calc(-1 * var(--giving-pad-block));
  bottom:    calc(-1 * var(--giving-pad-block));
  z-index:   -2;
  margin:    0;
  overflow:  hidden;
}
.slice--giving-advert .ArticleBody > div:nth-of-type(2) img {
  position:   absolute;
  inset:      0;
  width:      100%;
  height:     100%;
  object-fit: cover;
  display:    block;
}

/* Headline -- Heading 1 or 2. An italic word is recoloured to the figure tint
   (the italic mark is just the "tint me" signal; font-style is reset so it
   doesn't actually render slanted). */
.slice--giving-advert .ArticleBody :is(h1, h2) {
  margin:         0 0 var(--space-4);
  font-family:    var(--font-display);
  font-size:      clamp(1.9rem, 3.4vw + 0.5rem, 3rem);
  font-weight:    700;
  line-height:    1.05;
  letter-spacing: -0.01em;
  color:          var(--giving-panel-ink);
}
.slice--giving-advert .ArticleBody :is(h1, h2) :is(em, i) {
  font-style: normal;
  color:      var(--giving-figure);
}

/* Summary -- a plain paragraph (no leading bold, no standalone link). */
.slice--giving-advert .ArticleBody p {
  margin:      0 0 var(--space-5);
  font-family: var(--font-body);
  font-size:   var(--text-base);
  line-height: 1.6;
  color:       var(--giving-panel-muted);
}

/* Amounts -- authored as a 2-column table (left cell = figure, right cell =
   description, one row per gift). A table is a first-class editor construct
   that survives CKEditor round-trips, and a table's columns auto-align across
   all rows -- so clients just edit cells, the descriptions line up for free,
   and a text edit can't break the layout. Hubb renders a clean
   <table><tbody><tr><td> with no classes, so plain element selectors do the
   job -- no JS needed. */
.slice--giving-advert .ArticleBody table {
  width:           100%;
  border-collapse: collapse;
  margin:          0;
}
.slice--giving-advert .ArticleBody td {
  padding:        var(--space-3) 0;
  border-top:     1px solid var(--giving-rule);
  vertical-align: top;                 /* top-align the description with the figure */
}
.slice--giving-advert .ArticleBody tr:last-child td {
  border-bottom: 1px solid var(--giving-rule);
}
/* Figure cell (first column) -- sized to its own content and shared across all
   rows by the table, so every description starts at the same x. */
.slice--giving-advert .ArticleBody td:first-child {
  width:          1%;                  /* shrink to the widest figure; 2nd column takes the rest */
  white-space:    nowrap;
  padding-right:  var(--space-6);
  font-family:    var(--font-display);
  font-size:      clamp(1.6rem, 2.4vw + 0.5rem, 2.4rem);
  font-weight:    700;
  line-height:    1;
  letter-spacing: -0.02em;
  color:          var(--giving-figure);
}
/* Description cell (second column). */
.slice--giving-advert .ArticleBody td:last-child {
  color:       var(--giving-panel-ink);
  font-size:   var(--text-base);
  line-height: 1.4;
}

/* Donate button -- the link sits in its own paragraph after the table. Styling
   every link in the card as the pill keeps the CTA correct wherever it sits. */
.slice--giving-advert .ArticleBody p:has(> a:only-child) {
  margin: var(--space-6) 0 0;
}
.slice--giving-advert .ArticleBody a {
  display:         inline-flex;
  align-items:     center;
  padding:         var(--space-3) var(--space-8);
  background:      var(--giving-btn-bg);
  color:           var(--giving-btn-ink);
  font-family:     var(--font-body);
  font-weight:     700;
  font-size:       var(--text-base);
  letter-spacing:  0.01em;
  text-decoration: none;
  border-radius:   999px;
  transition:      background var(--transition), transform var(--transition);
}
.slice--giving-advert .ArticleBody a:active { transform: translateY(1px); }
.slice--giving-advert .ArticleBody a:focus-visible {
  outline:        3px solid var(--giving-btn-bg);
  outline-offset: 3px;
}
@media (hover: hover) {
  .slice--giving-advert .ArticleBody a:hover { background: var(--giving-btn-bg-hover); }
}

/* -------------------------------------------------------------------------
   3.11e  MODULE: Mailing Signup  --  .slice--signup / .slice--signup-cta
   -------------------------------------------------------------------------
   Two full-bleed accent-band mailing CTAs, each ONE Article (type 10), styled
   off .ArticleBody like the Giving Advert. Client edits copy + links in the
   site manager; nothing typed can break the layout.

   VARIANT A -- .slice--signup (split): heading + intro on the left, a Subscribe
   button pulled to the right (vertically centred) on desktop, an optional
   privacy line beneath. Article:
     <h2>Love <i>The Church</i> and want to know more?</h2>   (italic word = tint)
     <p>intro</p>
     <p><a href="...">Subscribe</a></p>                       (link alone = button)
     <p>For our Privacy Policy, please <a>click here</a></p>   (inline link = plain)

   VARIANT B -- .slice--signup-cta (centred): a single centred pill with an
   envelope icon + chevron, optional heading above. Article:
     <p><a href="...">Sign up to our Newsletter</a></p>

   The FIRST link (alone in its paragraph) is the button; any LATER link (the
   privacy link, inline in a sentence) stays a plain underlined link --
   distinguished by paragraph order (p:has(a) ~ p:has(a)), so it survives
   CKEditor whitespace and needs no special formatting.

   Re-skin per brand via the --signup-* tokens. A subtle decoration (faint
   envelope on A, faint rings on B) is drawn in CSS in white-alpha so it works
   on ANY accent with no image. A site wanting a custom backdrop can add an
   Image module (handled like the Giving Advert bg) -- ask to wire that.

   Body Contents:
     <section class="slice slice--signup">     ... </section>   (A)
     <section class="slice slice--signup-cta"> ... </section>   (B)
   ------------------------------------------------------------------------- */
.slice--signup,
.slice--signup-cta {
  /* ---- Re-skin surface: override THESE per brand ---- */
  --signup-bg:           var(--accent);        /* band colour */
  --signup-ink:          var(--accent-ink);    /* copy colour (vanilla: white) */
  --signup-muted:        rgba(255, 255, 255, 0.80);
  --signup-tint:         var(--accent-ink);    /* italic-word tint in the heading */
  --signup-btn-bg:       var(--accent-ink);    /* button fill  (vanilla: white) */
  --signup-btn-ink:      var(--ink);           /* button label (vanilla: dark)  */
  --signup-btn-bg-hover: rgba(255, 255, 255, 0.85);
  --signup-decor:        rgba(255, 255, 255, 0.08);  /* faint decoration tint */
  --signup-scrim:        rgba(0, 0, 0, 0.45);        /* legibility veil over a bg image (set to transparent to drop) */
  --signup-pad-block:    clamp(var(--space-8), 5vw, var(--space-16));

  /* Full-bleed band (same technique as the Giving Advert). */
  max-width:        none;
  padding-inline:   var(--container-gutter);
  margin-inline:    calc(50% - 50vw);
  margin-block:     clamp(var(--space-8), 6vw, var(--space-16));
  padding-block:    var(--signup-pad-block);
  position:         relative;
  overflow:         hidden;            /* clip the decoration + 100vw overshoot */
  background-color: var(--signup-bg);
  color:            var(--signup-ink);
}
.slice--signup .ArticleRotaSignupLink,
.slice--signup-cta .ArticleRotaSignupLink { display: none; }

/* Content column. max-width matches the Giving Advert card (1200px) so this
   band's heading lines up with where that section's left image starts, and
   the button no longer flies out to the full-width edge. */
.slice--signup .ArticleBody,
.slice--signup-cta .ArticleBody {
  position:      relative;
  z-index:       1;
  width:         100%;
  max-width:     1200px;
  margin-inline: auto;
}

/* ---- Optional background image (an Image module in the article) ----------
   Drop an Image module into the article and it becomes the full-bleed band
   background, covering the CSS decoration. Leave the module BLANK (no image
   picked) and it is hidden -- the CSS decoration shows and the empty slot
   takes no space. Only an <img> with a real src activates the background, so
   a blank slot is always safe. Mirrors the Giving Advert's background layer:
   breaks out to 100vw and extends past the copy by the section padding so
   object-fit:cover fills the whole band; the section's overflow:hidden clips
   it. A dark scrim (--signup-scrim) keeps the white copy legible. */
.slice--signup .ArticleBody > div:not(:has(img[src]:not([src=""]))),
.slice--signup-cta .ArticleBody > div:not(:has(img[src]:not([src=""]))) {
  display: none;                 /* blank / image-less slot -> hidden */
}
.slice--signup .ArticleBody > div:has(img[src]:not([src=""])),
.slice--signup-cta .ArticleBody > div:has(img[src]:not([src=""])) {
  position:  absolute;
  left:      50%;
  transform: translateX(-50%);
  width:     100vw;
  top:       calc(-1 * var(--signup-pad-block));
  bottom:    calc(-1 * var(--signup-pad-block));
  z-index:   -1;                 /* behind the copy, above the CSS decoration */
  margin:    0;
  overflow:  hidden;
}
.slice--signup .ArticleBody > div:has(img[src]:not([src=""])) img,
.slice--signup-cta .ArticleBody > div:has(img[src]:not([src=""])) img {
  position:   absolute;
  inset:      0;
  width:      100%;
  height:     100%;
  object-fit: cover;
  display:    block;
}
/* Scrim over the image (above it, below the copy). */
.slice--signup .ArticleBody > div:has(img[src]:not([src=""]))::after,
.slice--signup-cta .ArticleBody > div:has(img[src]:not([src=""]))::after {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--signup-scrim);
}

/* ---- Faint CSS decoration (no image needed) ---- */
.slice--signup::before {                 /* A: oversized envelope, left */
  content: "";
  position: absolute;
  left: clamp(-40px, -2vw, 0px);
  top: 50%;
  transform: translateY(-50%);
  width: clamp(220px, 26vw, 380px);
  aspect-ratio: 1;
  z-index: 0;
  background-color: var(--signup-decor);
  -webkit-mask: var(--icon-mail) no-repeat center / contain;
          mask: var(--icon-mail) no-repeat center / contain;
  pointer-events: none;
}
.slice--signup-cta::before {             /* B: faint concentric rings, left */
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  background: repeating-radial-gradient(circle at 12% 50%,
    transparent 0 46px,
    var(--signup-decor) 46px 50px,
    transparent 50px 96px);
  pointer-events: none;
}

/* ===== VARIANT A -- split ================================================= */
.slice--signup .ArticleBody {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  column-gap: clamp(var(--space-6), 5vw, var(--space-16));
  row-gap: var(--space-3);
}
.slice--signup .ArticleBody > * { grid-column: 1; margin: 0; }   /* text -> left */
.slice--signup .ArticleBody p:has(a) {                           /* button -> right, centred */
  grid-column: 2;
  grid-row: 1 / span 10;      /* span the text rows so align-self centres it */
  align-self: center;
}
.slice--signup .ArticleBody p:has(a) ~ p:has(a) {                /* privacy -> back left */
  grid-column: 1;
  grid-row: auto;
}
.slice--signup .ArticleBody :is(h1, h2) {
  font-family:    var(--font-display);
  font-size:      clamp(1.8rem, 2.8vw + 0.6rem, 2.75rem);
  font-weight:    700;
  line-height:    1.1;
  letter-spacing: -0.01em;
  color:          var(--signup-ink);
}
.slice--signup .ArticleBody :is(h1, h2) :is(em, i) {   /* italic word = tint */
  font-style: normal;
  color:      var(--signup-tint);
}
.slice--signup .ArticleBody p {
  font-size:   var(--text-base);
  line-height: 1.6;
  color:       var(--signup-muted);
}

/* ===== VARIANT B -- centred CTA =========================================== */
.slice--signup-cta .ArticleBody { text-align: center; }
.slice--signup-cta .ArticleBody :is(h1, h2) {
  font-family:    var(--font-display);
  font-size:      clamp(1.6rem, 2.4vw + 0.5rem, 2.4rem);
  font-weight:    700;
  line-height:    1.15;
  color:          var(--signup-ink);
  margin:         0 0 var(--space-6);
}
.slice--signup-cta .ArticleBody p { margin: 0; }

/* ===== Shared button (the CTA link) ====================================== */
.slice--signup .ArticleBody p:has(a) a,
.slice--signup-cta .ArticleBody a {
  display:         inline-flex;
  align-items:     center;
  gap:             var(--space-3);
  padding:         var(--space-4) var(--space-8);
  background:      var(--signup-btn-bg);
  color:           var(--signup-btn-ink);
  font-family:     var(--font-body);
  font-weight:     700;
  font-size:       var(--text-base);
  letter-spacing:  0.01em;
  text-decoration: none;
  border-radius:   999px;
  white-space:     nowrap;
  transition:      background var(--transition), transform var(--transition);
}
.slice--signup .ArticleBody p:has(a) a:active,
.slice--signup-cta .ArticleBody a:active { transform: translateY(1px); }
@media (hover: hover) {
  .slice--signup .ArticleBody p:has(a) a:hover,
  .slice--signup-cta .ArticleBody a:hover { background: var(--signup-btn-bg-hover); }
}
.slice--signup .ArticleBody p:has(a) a:focus-visible,
.slice--signup-cta .ArticleBody a:focus-visible {
  outline: 3px solid var(--signup-btn-ink);
  outline-offset: 3px;
}
/* A: trailing paper-plane on the Subscribe button. */
.slice--signup .ArticleBody p:has(a) a::after {
  content: "";
  width: 1.1em; height: 1.1em; flex: none;
  background-color: currentColor;
  -webkit-mask: var(--icon-send) no-repeat center / contain;
          mask: var(--icon-send) no-repeat center / contain;
}
/* B: leading envelope + trailing chevron on the pill. */
.slice--signup-cta .ArticleBody a::before {
  content: "";
  width: 1.2em; height: 1.2em; flex: none;
  background-color: currentColor;
  -webkit-mask: var(--icon-mail) no-repeat center / contain;
          mask: var(--icon-mail) no-repeat center / contain;
}
.slice--signup-cta .ArticleBody a::after {
  content: "";
  width: 1em; height: 1em; flex: none;
  background-color: currentColor;
  -webkit-mask: var(--icon-chevron-right) no-repeat center / contain;
          mask: var(--icon-chevron-right) no-repeat center / contain;
}

/* ---- Privacy / fine-print link (a LATER link) -- undo the button look ---- */
.slice--signup .ArticleBody p:has(a) ~ p:has(a) {
  font-size: var(--text-sm);
  color:     var(--signup-muted);
}
.slice--signup .ArticleBody p:has(a) ~ p:has(a) a {
  display:         inline;
  padding:         0;
  background:      none;
  color:           var(--signup-ink);
  font-weight:     500;
  font-size:       var(--text-sm);
  border-radius:   0;
  white-space:     normal;
  text-decoration: underline;
  text-underline-offset: 2px;
  transition:      opacity var(--transition);
}
.slice--signup .ArticleBody p:has(a) ~ p:has(a) a::after { content: none; }
/* Undo the button's :hover / :active on the fine-print link -- those rules are
   more specific than the resting override above, so without these the link
   picks up the near-white button-hover background (an ugly box). Keep it a
   plain text link: a soft fade on hover, no box, no press-shift. */
.slice--signup .ArticleBody p:has(a) ~ p:has(a) a:active { transform: none; }
@media (hover: hover) {
  .slice--signup .ArticleBody p:has(a) ~ p:has(a) a:hover {
    background: none;
    opacity:    0.72;
  }
}

/* -------------------------------------------------------------------------
   3.11e Mailing Signup -- phone: variant A stacks to one column; variant B
   pill tightens a touch.
   ------------------------------------------------------------------------- */
@media (max-width: 720px) {
  .slice--signup .ArticleBody { display: block; }
  .slice--signup .ArticleBody > * { margin: 0; }
  .slice--signup .ArticleBody :is(h1, h2) { margin-bottom: var(--space-3); }
  .slice--signup .ArticleBody p { margin-bottom: var(--space-4); }
  .slice--signup .ArticleBody p:has(a) { margin: var(--space-5) 0; }        /* button */
  .slice--signup .ArticleBody p:has(a) ~ p:has(a) { margin: 0; }            /* privacy */
  .slice--signup-cta .ArticleBody a { padding: var(--space-3) var(--space-6); font-size: var(--text-sm); }
}

/* -------------------------------------------------------------------------
   3.12  MODULE: Site Footer  --  .site-footer
   -------------------------------------------------------------------------
   Combines three Hubb modules: Site Email Address, Site Phone, and
   Footer bar. Wrap them in body Contents like:

     <footer class="site-footer">
       <div class="site-footer__inner">
         <h3 class="site-footer__heading">Get in touch</h3>
         <module ... type_id="89" alt="Site Email Address" />
         <module ... type_id="92" alt="Site Phone" />
       </div>
       <div class="site-footer__bottom">
         <div class="site-footer__inner">
           <module alt="Footer bar" ... type_id="9" />
         </div>
       </div>
     </footer>

   Module CSS Class settings (in Hubb):
     - Site Email Address: site_email
     - Site Phone:         phone_number
     - Footer bar:         (no class needed; outputs <div class="footer">)

   IMPORTANT: in the Footer bar module settings, set "Hubb logo colour"
   to "For dark background" so the logo renders correctly on our dark
   footer. Currently set to light -- needs flipping in Hubb.
   ------------------------------------------------------------------------- */

/* Scoped to the <footer> element so it doesn't leak onto an inner div
   if Hubb's Footer bar module accidentally shares the class name. */
.site-footer {
  background:    var(--ink);
  color:         rgba(255, 255, 255, 0.78);
  padding-top:   var(--space-16);
  margin-top:    var(--space-16);
  font-family:   var(--font-body);
}

.site-footer__inner {
  max-width:      var(--container-width);
  margin-inline:  auto;
  padding-inline: var(--container-gutter);
}

/* Top contact area -- grid layout. Heading spans the full row.
   The columns use `repeat(auto-fit, minmax(min(400px, 100%), 1fr))`
   which guarantees:
     - Each column wants at least 400px (or 100% of container,
       whichever is smaller -- that's the min() trick that makes single
       column fall back gracefully on narrow screens)
     - On a 1600px container that means at most 3 tracks fit,
       so we never get an awkward 4th empty track
     - 1fr forces the columns that DO exist to stretch and distribute
       evenly across the full container width
   Why 400px instead of 260px: with auto-fit and three short address
   blocks the previous 260px minimum was creating four tracks at
   wider breakpoints -- the 4th was technically empty (collapsed), but
   the 1fr distribution was still calculated against the "could fit
   four" sizing and items packed left. 400px ensures we get exactly
   3 tracks on big screens, distributing perfectly across the row.
   Selector dropped the `footer.` prefix in case Hubb renders the
   site-footer as a <div> on some installs. */
.site-footer > .site-footer__inner {
  display:               grid;
  grid-template-columns: repeat(auto-fit, minmax(min(400px, 100%), 1fr));
  gap:                   var(--space-6) var(--space-12);
  align-items:           start;
  /* v0.149: padding-block-start added so the dark address block has
     breathing room above its content, separating it from the cream
     safeguarding + umbrella zone above. Adam reported in v0.146
     "footer is so tight to umbrella section". v0.148 tried solving
     this with margin-block on .slice--partner-logos but that put
     a visible dark gap BETWEEN safeguarding and umbrella (two cream
     zones with a black band between -- "big black block"). The
     correct framing: safeguarding + umbrella are one continuous
     cream zone, and THIS dark address block needs its own internal
     top padding to breathe away from that cream zone above. The
     space lives on the dark block where it semantically belongs. */
  padding-block:         var(--space-16) var(--space-12);
}

.site-footer__heading {
  font-family:    var(--font-display);
  font-size:      clamp(1.5rem, 2.5vw, 2rem);
  font-weight:    400;
  font-style:     italic;
  color:          #FFFFFF;
  margin:         0 0 var(--space-2);
  letter-spacing: -0.01em;
  grid-column:    1 / -1;          /* heading spans full row */
}

/* Each contact item -- value below a small uppercase label.
   Stacked layout so multi-line address sits cleanly alongside
   single-line email and phone, all three look like a row of
   matching cards. */
.site-footer .site_address,
.site-footer .site_email,
.site-footer .phone_number {
  display:     block;
  font-family: var(--font-display);
  font-size:   var(--text-lg);
  line-height: 1.4;
  margin:      0;
}

/* Address may have line breaks. For best results, tick "Convert
   commas into line breaks" in the Hubb Site Address module settings
   -- this turns "Street, Town, Postcode" into 3 lines. white-space
   pre-line also respects any literal \n in the source. */
.site-footer .site_address {
  white-space: pre-line;
}

/* Stacked column inside the footer grid -- wraps the email + social
   modules so they share a single grid cell with the social icons
   sitting beneath the email. Used in body Contents like:

     <div class="site-footer__inner">
       <h3 class="site-footer__heading">Get in touch</h3>
       <module alt="Site Address" .../>
       <module alt="Site Phone" .../>
       <div class="site-footer__stack">
         <module alt="Site Email Address" .../>
         <module alt="Social Media Icons" .../>
       </div>
     </div>

   Result on wide screens: 3 columns -- Address | Phone | (Email/Social).
   Without the wrapper the social icons would land in their own column
   making the address look big and lonely on the left while the right
   side felt sparse. */
.site-footer__stack {
  display:        flex;
  flex-direction: column;
  gap:            var(--space-4);
}

/* Block labels above each value -- small uppercase caps, muted */
.site-footer .site_address::before,
.site-footer .site_email::before,
.site-footer .phone_number::before {
  display:        block;
  font-family:    var(--font-body);
  font-size:      var(--text-xs);
  font-weight:    700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color:          rgba(255, 255, 255, 0.5);
  margin:         0 0 var(--space-2);
}
.site-footer .site_address::before { content: "Address"; }
.site-footer .site_email::before   { content: "Email"; }
.site-footer .phone_number::before { content: "Tel"; }

.site-footer .site_email a,
.site-footer .phone_number a {
  color:           #FFFFFF;
  text-decoration: none;
  transition:      color 0.2s ease;
}

.site-footer .site_email a:hover,
.site-footer .phone_number a:hover {
  color:   var(--accent-soft);
  opacity: 1;
}


/* -------------------------------------------------------------------------
   Footer -- Social Media Icons
   -------------------------------------------------------------------------
   Article module rendered as a row of circular icon pills, each masked
   with the appropriate platform glyph based on the link's href.

   Body Contents pattern (placed inside .site-footer__inner alongside
   the contact modules):

     <module alt="Social Media Icons" type_id="10" />
                                       CSS Class: social_media_icons

   The article body itself is a list of plain anchor tags separated by
   <br>:

     <a href="https://www.facebook.com/" target="_blank">facebook</a><br>
     <a href="https://www.instagram.com/" target="_blank">instagram</a><br>
     <a href="https://www.x.com/" target="_blank">x</a><br>
     <a href="https://www.youtube.com/" target="_blank">youtube</a>

   What the CSS does:
   - Hides the text labels and the <br> separators
   - Lays the links out as a horizontal row (wraps on narrow widths)
   - Uses mask-image with platform-specific SVGs (inlined as data
     URIs -- no extra HTTP requests) to render each link as an icon
   - Currently supports: Facebook, Instagram, X (Twitter), YouTube,
     LinkedIn, TikTok. Anything else renders as a plain text link so
     nothing is silently dropped.

   Hover transition: lift + colour shift to the brand accent. Timing
   uses the standard --transition token to match the rest of the
   footer's interactive elements for a single consistent rhythm.
   ------------------------------------------------------------------------- */

.site-footer .social_media_icons {
  margin-top: var(--space-2);
}

.site-footer .social_media_icons .ArticleBody {
  display:   flex;
  flex-wrap: wrap;
  gap:       var(--space-3);
}

.site-footer .social_media_icons .ArticleBody br {
  display: none;
}

/* Default treatment -- plain text link (fallback for unknown platforms). */
.site-footer .social_media_icons .ArticleBody a {
  color:                  rgba(255, 255, 255, 0.85);
  font-size:              var(--text-sm);
  text-decoration:        underline;
  text-underline-offset:  3px;
  transition:             color var(--transition);
}
.site-footer .social_media_icons .ArticleBody a:hover {
  color:   var(--accent-soft);
  opacity: 1;
}

/* Known platforms -- override to circular icon pill. */
.site-footer .social_media_icons .ArticleBody a[href*="facebook"],
.site-footer .social_media_icons .ArticleBody a[href*="instagram"],
.site-footer .social_media_icons .ArticleBody a[href*="x.com"],
.site-footer .social_media_icons .ArticleBody a[href*="twitter"],
.site-footer .social_media_icons .ArticleBody a[href*="youtube"],
.site-footer .social_media_icons .ArticleBody a[href*="linkedin"],
.site-footer .social_media_icons .ArticleBody a[href*="tiktok"] {
  display:           inline-flex;
  align-items:       center;
  justify-content:   center;
  width:             38px;
  height:            38px;
  border-radius:     50%;
  background:        rgba(255, 255, 255, 0.08);
  color:             #FFFFFF;
  font-size:         0;            /* hide the text label */
  text-indent:       -9999px;
  overflow:          hidden;
  position:          relative;
  flex-shrink:       0;
  text-decoration:   none;
  transition:        background var(--transition),
                     color      var(--transition),
                     transform  var(--transition);
}

.site-footer .social_media_icons .ArticleBody a[href*="facebook"]:hover,
.site-footer .social_media_icons .ArticleBody a[href*="instagram"]:hover,
.site-footer .social_media_icons .ArticleBody a[href*="x.com"]:hover,
.site-footer .social_media_icons .ArticleBody a[href*="twitter"]:hover,
.site-footer .social_media_icons .ArticleBody a[href*="youtube"]:hover,
.site-footer .social_media_icons .ArticleBody a[href*="linkedin"]:hover,
.site-footer .social_media_icons .ArticleBody a[href*="tiktok"]:hover {
  background: var(--accent);
  color:      var(--accent-ink);
  transform:  translateY(-2px);
  opacity:    1;
}

.site-footer .social_media_icons .ArticleBody a[href*="facebook"]:focus-visible,
.site-footer .social_media_icons .ArticleBody a[href*="instagram"]:focus-visible,
.site-footer .social_media_icons .ArticleBody a[href*="x.com"]:focus-visible,
.site-footer .social_media_icons .ArticleBody a[href*="twitter"]:focus-visible,
.site-footer .social_media_icons .ArticleBody a[href*="youtube"]:focus-visible,
.site-footer .social_media_icons .ArticleBody a[href*="linkedin"]:focus-visible,
.site-footer .social_media_icons .ArticleBody a[href*="tiktok"]:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 2px;
}

/* Glyph layer -- masks SVG into the link's currentColor so the colour
   transition flows through naturally on hover (currentColor is
   transitionable; mask-image alone is not). */
.site-footer .social_media_icons .ArticleBody a[href*="facebook"]::before,
.site-footer .social_media_icons .ArticleBody a[href*="instagram"]::before,
.site-footer .social_media_icons .ArticleBody a[href*="x.com"]::before,
.site-footer .social_media_icons .ArticleBody a[href*="twitter"]::before,
.site-footer .social_media_icons .ArticleBody a[href*="youtube"]::before,
.site-footer .social_media_icons .ArticleBody a[href*="linkedin"]::before,
.site-footer .social_media_icons .ArticleBody a[href*="tiktok"]::before {
  content:           "";
  position:          absolute;
  inset:             0;
  background:        currentColor;
  -webkit-mask:      center / 18px 18px no-repeat;
          mask:      center / 18px 18px no-repeat;
}

/* Per-platform mask images -- solid filled SVG paths inlined as data
   URIs so there's zero extra HTTP requests. Each is the standard
   brand glyph (Facebook lowercase f, Instagram camera, X geometric,
   YouTube play-in-pill, LinkedIn "in" mark, TikTok music note). */

.site-footer .social_media_icons .ArticleBody a[href*="facebook"]::before {
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M22.675 0H1.325C.593 0 0 .593 0 1.325v21.351C0 23.407.593 24 1.325 24H12.82v-9.294H9.692v-3.622h3.128V8.413c0-3.1 1.893-4.788 4.659-4.788 1.325 0 2.463.099 2.795.143v3.24l-1.918.001c-1.504 0-1.795.715-1.795 1.763v2.313h3.587l-.467 3.622h-3.12V24h6.116c.73 0 1.323-.593 1.323-1.325V1.325C24 .593 23.407 0 22.675 0z'/></svg>");
          mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M22.675 0H1.325C.593 0 0 .593 0 1.325v21.351C0 23.407.593 24 1.325 24H12.82v-9.294H9.692v-3.622h3.128V8.413c0-3.1 1.893-4.788 4.659-4.788 1.325 0 2.463.099 2.795.143v3.24l-1.918.001c-1.504 0-1.795.715-1.795 1.763v2.313h3.587l-.467 3.622h-3.12V24h6.116c.73 0 1.323-.593 1.323-1.325V1.325C24 .593 23.407 0 22.675 0z'/></svg>");
}

.site-footer .social_media_icons .ArticleBody a[href*="instagram"]::before {
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z'/></svg>");
          mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zM12 0C8.741 0 8.333.014 7.053.072 2.695.272.273 2.69.073 7.052.014 8.333 0 8.741 0 12c0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98C8.333 23.986 8.741 24 12 24c3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98C15.668.014 15.259 0 12 0zm0 5.838a6.162 6.162 0 100 12.324 6.162 6.162 0 000-12.324zM12 16a4 4 0 110-8 4 4 0 010 8zm6.406-11.845a1.44 1.44 0 100 2.881 1.44 1.44 0 000-2.881z'/></svg>");
}

.site-footer .social_media_icons .ArticleBody a[href*="x.com"]::before,
.site-footer .social_media_icons .ArticleBody a[href*="twitter"]::before {
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z'/></svg>");
          mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z'/></svg>");
}

.site-footer .social_media_icons .ArticleBody a[href*="youtube"]::before {
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z'/></svg>");
          mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z'/></svg>");
}

.site-footer .social_media_icons .ArticleBody a[href*="linkedin"]::before {
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.063 2.063 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z'/></svg>");
          mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 01-2.063-2.065 2.063 2.063 0 112.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z'/></svg>");
}

.site-footer .social_media_icons .ArticleBody a[href*="tiktok"]::before {
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-5.2 1.74 2.89 2.89 0 0 1 2.31-4.64 2.93 2.93 0 0 1 .88.13V9.4a6.84 6.84 0 0 0-1-.05A6.33 6.33 0 0 0 5.8 20.1a6.34 6.34 0 0 0 10.86-4.43v-7a8.16 8.16 0 0 0 4.77 1.52v-3.4a4.85 4.85 0 0 1-1.84-.1z'/></svg>");
          mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-5.2 1.74 2.89 2.89 0 0 1 2.31-4.64 2.93 2.93 0 0 1 .88.13V9.4a6.84 6.84 0 0 0-1-.05A6.33 6.33 0 0 0 5.8 20.1a6.34 6.34 0 0 0 10.86-4.43v-7a8.16 8.16 0 0 0 4.77 1.52v-3.4a4.85 4.85 0 0 1-1.84-.1z'/></svg>");
}

/* Hide Hubb's empty article appendage */
.site-footer .social_media_icons .ArticleRotaSignupLink:empty {
  display: none;
}


/* -------------------------------------------------------------------------
   Footer -- Safeguarding notice
   -------------------------------------------------------------------------
   A supplementary block sitting between the main contact grid
   ("Get in touch") and the utility strip (Hubb logo + links). Used
   for legal/safeguarding notices that need to appear sitewide but
   shouldn't compete with primary footer content for attention.

   Body Contents pattern (placed INSIDE the <footer>, BETWEEN the
   .site-footer__inner contact block and the .site-footer__bottom):

     <section class="footer-safeguarding">
       <h3 class="footer-safeguarding__heading">Safeguarding</h3>
       <module alt="Safeguarding" type_id="10" />     <- Article module
                                                       CSS Class: safeguarding_notice
     </section>

   The Article module renders into <div class="safeguarding_notice">,
   which contains an <h1> with the article title -- we hide that to
   avoid duplicating the user-supplied <h3> heading.

   Visual treatment: centered text on the dark footer background,
   slightly smaller and more restrained than "Get in touch" -- this is
   a notice, not a primary CTA. Heading styled to match the existing
   .site-footer__heading family (italic display font, white).
   ------------------------------------------------------------------------- */

.footer-safeguarding {
  /* v0.142: explicit cream background + full-bleed layout. Adam
     reported the safeguarding text was unreadable when the section
     sat inside the dark footer band -- ink-soft text on dark ink
     (footer's --ink background) gave near-zero contrast. The
     comment below this line in v0.130 mentioned a "cream-coloured
     backdrop" but no actual `background` property was ever written,
     so the dark footer bg bled straight through. Two changes:
     - Set background: var(--bg) so the section reads as cream
     - Drop max-width + margin-inline auto so the cream band runs
       edge-to-edge of the viewport (otherwise we'd get a centred
       cream block with dark wings on either side, which looks broken)
     The inner content reattaches to container width via the
     `.footer-safeguarding > *` rule below. Visual order is now:
     light page -> cream safeguarding band -> dark footer below,
     which reads as a clean panel transition. */
  background:     var(--bg);
  max-width:      none;
  margin-inline:  0;
  padding-inline: var(--container-gutter);
  /* As of v0.130 this lives INSIDE <footer> (was previously a sibling
     between </main> and <footer>, which was structurally weird).
     v0.143: margin-top inverted from var(--space-16) to its negative
     so the safeguarding band sits flush at the very top of the
     dark footer, NOT 64px below it. The .site-footer has padding-top:
     var(--space-16); without this negation, the cream band started
     64px down inside the footer, leaving 64px of dark band visible
     above it -- which is the "area above safeguarding ... not sure
     why it's black" Adam reported. The negative margin pulls the
     section up into (and through) the footer's padding-top zone,
     so the cream band starts at the footer's top edge. */
  margin-top:     calc(0px - var(--space-16));
  padding-top:    var(--space-8);
  padding-bottom: var(--space-8);
  border-top:     1px solid var(--rule);
  text-align:     center;
}

/* Inner content stays at container width for readable line lengths
   even though the cream background above runs full-bleed. */
.footer-safeguarding > * {
  max-width:     var(--container-width);
  margin-inline: auto;
}

/* Heading -- matches the .site-footer__heading style family but a touch
   smaller because this is a secondary block. Targets the heading by
   structural selector (h1/h2/h3) so it works regardless of which
   heading level the body Contents uses, AND by the recommended
   `.footer-safeguarding__heading` class for explicit semantic clarity. */
.footer-safeguarding > h1,
.footer-safeguarding > h2,
.footer-safeguarding > h3,
.footer-safeguarding__heading {
  font-family:    var(--font-display);
  font-size:      clamp(1.25rem, 2vw, 1.5rem);
  font-weight:    400;
  font-style:     italic;
  color:          var(--ink);
  /* v0.162: inline margins MUST stay `auto`. This rule outranks the
     `.footer-safeguarding > *` centring rule (0,1,1 vs 0,1,0), so the
     old `margin: 0 0 var(--space-3)` shorthand silently zeroed the
     auto inline margins from that rule. The heading kept its 1600px
     cap but lost its centring -- left-anchored at the 40px gutter,
     so its text-align:center put the word "Safeguarding" at
     gutter + 800px = 840px from the left edge instead of the
     viewport centre. Invisible below 1600px viewports (cap never
     engages); obvious on ultrawide. Same bug class as the
     .gallery_ul fix in 3.22 below. */
  margin:         0 auto var(--space-3);
  letter-spacing: -0.01em;
  text-align:     center;
}

/* Article body -- ink-soft text on cream, narrow column for readable
   line length. Hubb's Article module renders an internal <h1> matching
   the article title; we hide it because the body Contents already has
   its own heading above the module. */
.footer-safeguarding .safeguarding_notice .ArticleBody > h1 {
  display: none;
}
.footer-safeguarding .safeguarding_notice .ArticleBody {
  font-size:     var(--text-sm);
  color:         var(--ink-soft);
  line-height:   1.6;
  max-width:     720px;
  margin-inline: auto;
}
.footer-safeguarding .safeguarding_notice .ArticleBody p {
  margin: 0;
}

/* Link styling -- accent-dark passes WCAG AA (6.86:1) on cream
   background, vs --accent which fails (4.24:1). Hubb wraps link
   text in <u>; with our <a> already underlined that creates a
   doubled underline visually, so we strip the <u>'s decoration. */
.footer-safeguarding .safeguarding_notice .ArticleBody a {
  color:                 var(--accent-dark);
  text-decoration:       underline;
  text-underline-offset: 3px;
  transition:            color var(--transition);
}
.footer-safeguarding .safeguarding_notice .ArticleBody a:hover {
  color:   var(--ink);
  opacity: 1;
}
.footer-safeguarding .safeguarding_notice u {
  text-decoration: none;
}

/* Strip Hubb's empty article appendages -- same pattern we use on
   the welcome article and other Article module instances. */
.footer-safeguarding .ArticleRotaSignupLink:empty { display: none; }
.footer-safeguarding .safeguarding_notice .pagebox { display: none; }


/* =========================================================================
   3.22  UMBRELLA ORG LOGOS  --  .umbrella_org_logo
   -------------------------------------------------------------------------
   Optional logo strip showing organisations the church is affiliated
   with -- denomination (e.g. Baptist Union of GB), local networks,
   missions partners, charities. Sits between the safeguarding section
   and the footer as a quiet "we belong to / partner with" footnote.

   Hubb-rendered structure:
     <div class="umbrella_org_logo">
       <ul class="gallery_ul">
         <li class="gallery_li">
           <img src="..." alt="">
         </li>
         ...
       </ul>
     </div>

   Body Contents pattern (added by Adam):
     <module alt="Gallery" type_id="84" />
   With CSS Class field set to: umbrella_org_logo

   Visual treatment: greyscale + reduced opacity by default so the
   logos read as supporting context rather than competing with the
   footer's primary content. Restore full colour on hover so users
   can recognise specific brands when investigating.
   ========================================================================= */

.umbrella_org_logo {
  /* v0.143: full-bleed cream band, same pattern as
     .footer-safeguarding above, so the safeguarding section and
     the umbrella-org logo strip read as one continuous light zone
     between the page content and the dark address/email footer
     block below. Adam's screenshot showed the Westpoint logo
     drowning in the dark band; cream backdrop fixes contrast and
     gives the partner logos a cleaner stage. Inner content
     reattaches to container width via the > * rule below.

     v0.150: padding-block asymmetric -- top stays at var(--space-8)
     (32px) because the safeguarding band sits flush above and the
     cream-to-cream transition needs no extra space, but bottom
     goes to var(--space-16) (64px) so there's more cream below
     the logo before the cream-to-dark transition into the dark
     footer__inner. Adam: "add 32px more padding between footer
     black area and the umbrella logo on desktop". Mobile rule
     (in the responsive section) overrides padding-block to
     var(--space-6) symmetric, so this asymmetry is desktop-only. */
  background:     var(--bg);
  max-width:      none;
  margin-inline:  0;
  padding-inline: var(--container-gutter);
  padding-block:  var(--space-8) var(--space-16);
  text-align:     center;
}

/* Inner content stays at container width for readable logo strip
   layout even though the cream band above runs full-bleed. */
.umbrella_org_logo > * {
  max-width:     var(--container-width);
  margin-inline: auto;
}

.umbrella_org_logo .gallery_ul {
  display:         flex;
  flex-wrap:       wrap;
  align-items:     center;
  justify-content: center;
  gap:             var(--space-12);
  list-style:      none;
  /* v0.162: `margin: 0` here outranked the `.umbrella_org_logo > *`
     centring rule (0,2,0 vs 0,1,0) and zeroed its auto inline
     margins, leaving the ul width-capped at 1600px but left-anchored
     at the gutter. justify-content:center then centred the logo at
     gutter + 800px instead of the viewport centre -- the "Westpoint
     logo drifts left on wide screens" bug. Block margins stay 0;
     inline margins must stay auto so the capped ul itself centres. */
  margin-block:    0;
  margin-inline:   auto;
  padding:         0;
}

.umbrella_org_logo .gallery_li {
  flex-shrink: 0;
}

/* Logos: capped height (not width) so wider/narrower logos balance
   visually. Display as-is with a subtle opacity fade on hover only
   when wrapped in a link -- no greyscale or transform treatment,
   since the image files themselves are already prepared for display
   (e.g. dark text on transparent background) and Adam wants logos
   to stay still on hover (no movement / nudge). v0.148 simplified. */
.umbrella_org_logo .gallery_li img {
  display:    block;
  height:     auto;
  width:      auto;
  max-height: 56px;
  max-width:  240px;
  object-fit: contain;
  transition: opacity var(--transition);
}
/* Hover state ONLY when the logo is wrapped in an anchor -- if
   there's no link, there's no action, so no hover should be
   suggested (Adam: "why have the hover state?!"). Subtle opacity
   fade rather than transform: Adam doesn't want logos moving on
   hover; opacity gives tactile feedback without nudging the
   page geometry. focus-visible mirrors hover for keyboard users. */
.umbrella_org_logo .gallery_li a:hover img,
.umbrella_org_logo .gallery_li a:focus-visible img {
  opacity: 0.65;
}

/* If logos link out (Hubb may wrap them in <a> if the gallery item
   has a target URL), make the link a clean block so the hover
   states above work without underline artefacts. */
.umbrella_org_logo .gallery_li a {
  display:         inline-block;
  text-decoration: none;
  border:          0;
}


/* -------------------------------------------------------------------------
   3.22a  WIDE-VIEWPORT SCALING  --  safeguarding + umbrella @ >=1600px
   -------------------------------------------------------------------------
   v0.161: on viewports wider than --container-width (1600px), the
   safeguarding band and umbrella-logo strip run full-bleed cream
   while their inner content stays sized for a 1400-1600px canvas.
   At 2557px this leaves a 24px heading, 15px body text in a 720px
   column, and a 240x56px logo floating in a 2557px-wide cream band
   with 478px cream wings on each side -- the heading and logo read
   as isolated pinpricks rather than deliberate composition.

   Adam: "look what happened to safeguarding and umbrella image when
   the screen is stretched..." (2557px viewport screenshot).

   The breakpoint is anchored to --container-width specifically: cream
   wings exist if and only if viewport > container, so 1600px is the
   exact threshold where this becomes visible. Below 1600px, the
   container fills the viewport and the existing fixed sizes look
   correct -- nothing changes. The clamps below all use min values
   equal to the current fixed sizes, so there is no jump at the
   breakpoint -- values grow smoothly from current sizes upward.

   Scaling targets (at 2557px viewport):
     - Heading:    24px  -> 36px  (+50%) via vw-based clamp
     - Body font:  15.2px -> 18px (+18%) via vw-based clamp
     - Body col:   720px -> 1000px (+39%) so the text doesn't read as
                                         a narrow ribbon
     - Logo h:     56px  -> ~90px (+60%)
     - Logo w:     240px -> 380px (+58%)
   ------------------------------------------------------------------------- */
@media (min-width: 1600px) {
  .footer-safeguarding > h1,
  .footer-safeguarding > h2,
  .footer-safeguarding > h3,
  .footer-safeguarding__heading {
    /* Min 1.5rem = 24px matches the previous fixed cap exactly, so
       no visible jump at the 1600px breakpoint. 1.4vw scales it up
       gradually; capped at 2.25rem (36px) so an absurdly wide
       monitor doesn't get an absurdly large heading. */
    font-size: clamp(1.5rem, 1.4vw, 2.25rem);
  }

  .footer-safeguarding .safeguarding_notice .ArticleBody {
    /* Body font min 0.95rem = same as --text-sm cap, so no jump.
       Min body column 720px = current fixed value, so no jump.
       45vw gives roughly half the viewport, capped at 1000px for
       readable line length (about 85ch at 18px). */
    font-size: clamp(0.95rem, 0.4vw + 0.55rem, 1.125rem);
    max-width: clamp(720px, 45vw, 1000px);
  }

  .umbrella_org_logo .gallery_li img {
    /* Min 56px height / 240px width = current fixed values, so no
       jump at breakpoint. Caps at 96px / 380px so a 4K monitor
       doesn't end up with a huge partner logo dominating the band. */
    max-height: clamp(56px, 3.5vw, 96px);
    max-width:  clamp(240px, 15vw, 380px);
  }
}


/* Bottom strip -- separated by hairline, smaller text, lighter weight */
.site-footer__bottom {
  margin-top:    var(--space-12);
  padding-block: var(--space-4) var(--space-6);
  border-top:    1px solid rgba(255, 255, 255, 0.12);
}

/* Hubb's Footer bar outputs a wrapper div with the Hubb logo link +
   utility links + charity number inside. We target it structurally
   (any div directly inside .site-footer__bottom > .site-footer__inner)
   so it works regardless of whatever CSS class is set on the module. */
.site-footer__bottom > .site-footer__inner > div {
  display:        flex;
  flex-wrap:      wrap;
  align-items:    center;
  gap:            var(--space-3) var(--space-4);
  font-size:      var(--text-xs);
  letter-spacing: 0.05em;
  color:          rgba(255, 255, 255, 0.5);
}

.site-footer__bottom > .site-footer__inner > div > a {
  color:           rgba(255, 255, 255, 0.65);
  text-decoration: none;
  transition:      color 0.2s ease;
}

.site-footer__bottom > .site-footer__inner > div > a:hover {
  color:   #FFFFFF;
  opacity: 1;
}

/* Hubb logo link -- sits on the FAR LEFT of the bottom strip
   (matching the reference design where "site by HUBB.CHURCH" leads). */
.site-footer__bottom > .site-footer__inner > div > a.footer {
  /* no margin -- the auto margin on the next sibling handles spacing */
}

/* Equal-space layout in the bottom strip:
     [logo]  <----- gap ----->  [link link link link link link]  <----- gap ----->  [charity]
   Two `margin-left: auto` declarations split the remaining space
   evenly: one before the first non-logo link (pushes the link cluster
   away from the logo) and one before the charity (pushes the charity
   to the far right). Flexbox distributes auto-margin space equally
   between competing claims. */
.site-footer__bottom > .site-footer__inner > div > a.footer + a {
  margin-left: auto;
}

.site-footer .footer-logo {
  display:     block;
  opacity:     0.55;
  transition:  opacity 0.2s ease;
  max-width:   150px;
  height:      auto;
  padding-top: 0 !important;   /* override Hubb's inline padding-top: 10px */
}

.site-footer__bottom > .site-footer__inner > div > a.footer:hover .footer-logo {
  opacity: 0.9;
}

/* Charity number -- pushed to the far right via auto margin */
.site-footer #footer_charity {
  margin-left: auto;
  font-style:  italic;
  color:       rgba(255, 255, 255, 0.45);
}


/* =========================================================================
   3.14  DIALOGS -- Event & Media popups (jQuery UI overrides)
   -------------------------------------------------------------------------
   Hubb opens event details and media playback in jQuery UI dialogs that
   ship with extremely default jQuery UI styling -- beige gradients,
   dotted borders, generic Arial text. We aggressively override to make
   them feel like part of our design system.

   Wrapper structure:
     .endis-jquery-ui-dialog                     <- outermost wrapper
       .ui-dialog                                <- the dialog (positioned by JS)
         .ui-dialog-titlebar                     <- thin top strip (close lives here)
         .ui-dialog-content                      <- body content, flex-grows
           .calendar-event-dialog   OR           <- event variant
           .media-player                         <- media variant

   Approach -- RIGHT-SIDE SLIDING PANEL:
   - Pin the dialog to the right edge of the viewport, full height,
     580px wide (or full-width on mobile). This eliminates the position
     jitter we saw with fixed-centred + transform -- jQuery's calculated
     coords used to fight our centring, occasionally causing the box to
     "jump" mid-interaction. With the panel pinned to a viewport edge
     there are no calculated coords to drift.
   - Slide in from the right with a CSS keyframe on render. No
     slide-out (jQuery hides the dialog on close, animation isn't
     needed for the close path).
   - Use !important for everything that needs to beat jQuery UI's CSS
     OR the inline styles JS sets at runtime. Defensive override of
     third-party CSS we don't control.
   - Mobile: panel becomes full-width via the min() in the width rule.
   ========================================================================= */

/* Backdrop scrim -- jQuery inserts .ui-widget-overlay as a sibling of
   the dialog. Default style is a beige gradient. Replace with a clean
   ink-tinted scrim. */
.ui-widget-overlay {
  background: rgba(26, 22, 18, 0.55) !important;
  opacity:    1 !important;
  position:   fixed !important;
  inset:      0 !important;
  z-index:    20002 !important;
  animation:  dialogOverlayFade 240ms ease-out;
}

/* Defensive scroll restoration -- Hubb's modal-open code locks body
   scroll on dialog open and the cleanup on close is unreliable. The
   side panel is position: fixed so it stays in place regardless of
   body scroll state -- locking body scroll while the panel is open is
   not necessary for our visual model. So we override any scroll lock
   that gets applied, EXCEPT our own .menu-open lock for the mobile
   navigation drawer (which is a distinct, intentional case).
   
   This is the strongest layer of defence against the "page won't
   scroll after closing the dialog" bug -- it doesn't matter whether
   Hubb removes the lock or not, because our !important override
   always wins. */
html {
  overflow-y: auto !important;
  position:   static !important;
  height:     auto !important;
}
body:not(.menu-open) {
  overflow:   visible !important;
  position:   static !important;
  top:        auto !important;
  width:      auto !important;
  height:     auto !important;
}

@keyframes dialogSlideIn {
  from { transform: translateX(100%); }
  to   { transform: translateX(0); }
}
/* Mobile bottom-sheet variant -- slides up from the bottom edge
   rather than in from the right. */
@keyframes dialogSlideUp {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}
@keyframes dialogOverlayFade {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Outer dialog -- full-height panel pinned to the right edge. Width is
   580px on desktop, capped by viewport on smaller screens via min().
   No border-radius (edge-to-edge), shadow on the LEFT only (where it
   meets the dimmed page).
   
   Height handling:
   - height: 100vh   = static viewport height (legacy fallback for
                       browsers without dvh support: iOS <15.4, etc.)
   - height: 100dvh  = dynamic viewport height (excludes URL bar, tab
                       bar, etc. when collapsed; updates as they
                       show/hide). Modern browsers honour this; older
                       ones ignore the second declaration. */
.endis-jquery-ui-dialog .ui-dialog {
  position:       fixed !important;
  top:            0 !important;
  right:          0 !important;
  bottom:         0 !important;
  left:           auto !important;
  margin:         0 !important;
  width:          min(540px, 100vw) !important;
  max-width:      100vw !important;
  height:         100vh !important;
  height:         100dvh !important;
  max-height:     100vh !important;
  max-height:     100dvh !important;
  background:     var(--surface) !important;
  border:         0 !important;
  border-radius:  0 !important;
  box-shadow:     -16px 0 48px -12px rgba(26, 22, 18, 0.25) !important;
  padding:        0 !important;
  font-family:    var(--font-body) !important;
  color:          var(--ink) !important;
  display:        flex !important;
  flex-direction: column !important;
  overflow:       hidden !important;
  z-index:        20003 !important;
  animation:      dialogSlideIn 280ms cubic-bezier(0.16, 1, 0.3, 1);
}

/* Title bar -- a real flex strip at the top of the dialog rather than
   absolute-positioned floaters. Reasons for the change in v0.95:
     1. The brief asked for icons to feel like part of the layout,
        not "tiny utility icons added afterwards".
     2. Reordering .media-player children via flex order (so the
        media player sits above the title) means the player would
        otherwise sit BEHIND the absolute icons, with the icons
        floating over the top-right of video frames. Putting the
        icons in a strip avoids that overlap entirely. */
.endis-jquery-ui-dialog .ui-dialog-titlebar {
  position:        static !important;
  flex:            0 0 auto;
  background:      transparent !important;
  border:          0 !important;
  border-radius:   0 !important;
  /* Asymmetric padding (top right bottom left = 32 16 12 32):
     - Top 32px: clear breathing room around the close button
       against the dialog's top edge.
     - Right 16px: tighter than top so the close icon reads as
       equally inset visually. The button is 44x44 with a 22px
       icon centred; mathematically 32px on both sides gives
       identical distances, but the eye reads horizontal expanse
       as bigger so the glyph appeared "further from the right
       than the top". v0.133 patch tightens to 16px right; the
       hover circle (44px) still has full breathing room before
       it would touch the edge.
     - Bottom 12px: tight gap between the close-button row and
       the title below, so they read as one related band.
     - Left 32px: matches top; we don't want anything pulling
       the eye into the empty left side. */
  padding:         var(--space-8) var(--space-4) var(--space-3) var(--space-8) !important;
  display:         flex !important;
  align-items:     center !important;
  justify-content: flex-end !important;
  min-height:      auto !important;
  z-index:         10;
}
.endis-jquery-ui-dialog .ui-dialog-title {
  display: none !important;
}

/* Button pane -- flex with gap between buttons */
.endis-jquery-ui-dialog .ui-dialog-titlebar-buttonpane {
  position:       static !important;
  margin:         0 !important;
  height:         auto !important;
  display:        flex !important;
  flex-direction: row-reverse;
  align-items:    center;
  gap:            var(--space-2);    /* 8px between close and open-in-new */
}

/* Title bar buttons -- 44x44 hit area meets WCAG 2.5.5 (minimum
   target size) and the brief's explicit "at least 44 x 44 clickable
   area" requirement. Visual icon stays smaller (22px) so the buttons
   read as icon affordances, not chunky toolbar buttons. */
.endis-jquery-ui-dialog .ui-dialog-titlebar-close,
.endis-jquery-ui-dialog .ui-dialog-titlebar-buttonpane > a {
  display:         inline-flex !important;
  align-items:     center !important;
  justify-content: center !important;
  width:           44px !important;
  height:          44px !important;
  padding:         0 !important;
  background:      transparent !important;
  border:          0 !important;
  border-radius:   50% !important;
  color:           var(--ink-soft) !important;
  text-decoration: none !important;
  cursor:          pointer !important;
  float:           none !important;
  transition:      background var(--transition), color var(--transition) !important;
}
.endis-jquery-ui-dialog .ui-dialog-titlebar-close:hover,
.endis-jquery-ui-dialog .ui-dialog-titlebar-buttonpane > a:hover {
  background: var(--accent-soft) !important;
  color:      var(--accent) !important;
}
/* Visible focus ring for keyboard users -- the hover background alone
   doesn't satisfy WCAG 2.4.7. */
.endis-jquery-ui-dialog .ui-dialog-titlebar-close:focus-visible,
.endis-jquery-ui-dialog .ui-dialog-titlebar-buttonpane > a:focus-visible {
  outline:        2px solid var(--accent) !important;
  outline-offset: 2px;
  background:     var(--accent-soft) !important;
  color:          var(--accent) !important;
}
.endis-jquery-ui-dialog .ui-dialog-titlebar-close i,
.endis-jquery-ui-dialog .ui-dialog-titlebar-buttonpane > a i {
  font-size:   22px;
  line-height: 1;
}

/* Hide the alternate window-state buttons (restore, collapse,
   maximize, minimize). jQuery ships them with display:none inline,
   but Hubb's "screen-constrained pinned-to-bottom" mode flips them
   visible at runtime so we need to lock them shut. The selectors
   include the buttonpane parent so they have one more class than the
   `.ui-dialog-titlebar-buttonpane > a { display: inline-flex }` rule
   above -- without that bump, the inline-flex !important would win
   the cascade and the icons would still show. */
.endis-jquery-ui-dialog .ui-dialog-titlebar-buttonpane > .ui-dialog-titlebar-restore,
.endis-jquery-ui-dialog .ui-dialog-titlebar-buttonpane > .ui-dialog-titlebar-collapse,
.endis-jquery-ui-dialog .ui-dialog-titlebar-buttonpane > .ui-dialog-titlebar-maximize,
.endis-jquery-ui-dialog .ui-dialog-titlebar-buttonpane > .ui-dialog-titlebar-minimize {
  display: none !important;
}

/* Hide jQuery UI's resize handle -- shows as a small triangular dots
   pattern in the bottom-right corner of the dialog by default. We
   want a clean fixed-size modal, no manual resize. */
.endis-jquery-ui-dialog .ui-resizable-handle,
.endis-jquery-ui-dialog .ui-resizable-se,
.endis-jquery-ui-dialog .ui-resizable-s,
.endis-jquery-ui-dialog .ui-resizable-e {
  display: none !important;
}

/* Dialog content -- flex item that takes remaining height & scrolls.
   Bottom padding adds safe-area-inset-bottom so the last scrollable
   line can scroll up past the iPhone home indicator.

   Padding uses clamp() so it scales fluidly between mobile (16px)
   and desktop (24px), giving us roughly 20px on tablet widths
   (1024px viewport -> clamp resolves to ~20px) without needing a
   separate tablet breakpoint. The mobile @media override below
   forces 16px below 720px in case the clamp's vw component doesn't
   bottom out aggressively enough on small phones. */
/* The body of the dialog -- where the actual event/media content lives.
   Padding is asymmetric: top is tighter than the other sides because
   the .ui-dialog-titlebar above (which holds the close button) already
   contributes 12px below the close-button row, and the title is the
   very first element in this content area. v0.131-pre had clamp(16,
   2vw, 24) all round; Adam's screenshot showed a perceptible empty
   band between the close-button row and the title. v0.134 patch:
   top dropped to clamp(8, 1vw, 12), so the title sits just under the
   close-button band rather than floating in its own space.
   Sides + bottom keep the original generous padding -- that's where
   the dialog content needs breathing room from the panel edges and
   the bottom safe-area inset (iOS home indicator). */
.endis-jquery-ui-dialog .ui-dialog-content {
  background:  var(--surface) !important;
  color:       var(--ink) !important;
  padding:     clamp(var(--space-2), 1vw, var(--space-3))
               clamp(var(--space-4), 2vw, var(--space-6))
               calc(clamp(var(--space-4), 2vw, var(--space-6)) + var(--safe-bottom))
               clamp(var(--space-4), 2vw, var(--space-6)) !important;
  margin:      0 !important;
  flex:        1 1 auto !important;
  height:      auto !important;
  max-height:  none !important;
  overflow-y:  auto !important;
  font-family: var(--font-body);
}

/* ===== Event dialog content ============================================ */

/* Inner wrapper. Hubb's base.min.css applies `padding: 10px 20px` and
   `font-size: 16px` to .dialog-content.calendar-event-content, which
   we hadn't accounted for through v0.134 -- it was contributing extra
   padding above the title (on top of .ui-dialog-content's own padding)
   and stacking with the side gutters. DevTools diagnostic in v0.135
   revealed the duplication. Zero this wrapper out so .ui-dialog-content
   above is the single source of truth for inner padding. Font-size
   resets to inherit so our typography scale takes over. */
.calendar-event-dialog .dialog-content.calendar-event-content,
.calendar-event-dialog .calendar-event-content {
  padding:   0 !important;
  font-size: inherit;
}

/* Selector specificity (0,4,0) deliberately beats Hubb's base.min.css
   rule `div.dialog-content.calendar-event-content div.event-title`
   (0,2,2) which carries `font-size: 1.2em; font-weight: bold;
   line-height: 25px; margin-bottom: 10px`. v0.140 DevTools diagnostic
   showed our previous (0,2,0) rule was being entirely overridden --
   title was rendering at 1.2em BOLD because base was winning, and
   the v0.138 size-down to 1.125rem on mobile was struck through.
   Adam liked the 1.2em size visually, so we keep that explicitly
   and now also lock font-weight: 400 (italic display font reads
   wrong at bold) + line-height: 1.2 (base's 25px too loose for
   our scale). em-based so it scales with parent body font-size
   (which is itself responsive via clamp on --text-base) -- no
   separate mobile override needed. */
.calendar-event-dialog .dialog-content.calendar-event-content > .event-title {
  font-family:    var(--font-display);
  font-size:      1.2em;
  font-weight:    400;
  font-style:     italic;
  color:          var(--ink);
  margin:         0 0 var(--space-1);
  /* v0.143: dropped padding-right: 112px. That value was originally
     reservation space for a close button that floated INLINE with
     the title; since v0.130 the close button has lived in its own
     titlebar row ABOVE the dialog content, so the title no longer
     needs to clear it. Adam's mobile screenshot showed "Dads and
     Kids Breakfast" wrapping to two lines because of this leftover
     padding -- the title now uses the full content width on both
     desktop and mobile. */
  letter-spacing: -0.01em;
  line-height:    1.2;
}

/* Date/time line under the title -- subtle uppercase eyebrow.
   Selector specificity (0,3,1) beats base.min.css's
   `div.dialog-content.calendar-event-content > h3` (0,2,1). v0.140
   switches font-size from var(--text-xs) to 0.8em -- Adam's preferred
   value, scales with parent body font-size like the title above. */
.calendar-event-dialog .dialog-content.calendar-event-content > h3 {
  font-family:    var(--font-body);
  font-size:      0.8em;
  font-weight:    600;
  color:          var(--accent);
  margin:         0 0 var(--space-3);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}

.calendar-event-dialog .event-summary-image {
  display:       block;
  width:         100%;
  height:        auto;
  max-height:    220px;     /* tightened from 320 -- title was overflowing
                               above the centred dialog on smaller viewports */
  object-fit:    cover;
  border-radius: var(--radius);
  /* Bottom margin set to 0; the location row below provides its own
     symmetric top margin, so the gap is owned by ONE element rather
     than split across two (which made it harder to balance). */
  margin:        0;
}

/* Location row -- pin icon + clickable location text + open-in-new icon.
   Selector specificity (0,3,1) deliberately beats Hubb's base.min.css
   rule `.dialog-content.calendar-event-content > h4 { margin-top: 10px;
   font-size: 1.1em; }` (0,2,1). DevTools v0.135 diagnostic showed
   that rule was overriding our margin-top, leaving the row with
   asymmetric 10px top / 20px bottom margins despite our
   `margin: var(--space-5) 0` rule -- because base.min.css's more-
   specific selector won the cascade for margin-top only.
   Always specifying both font-size and a complete margin shorthand
   here means base.min.css can't punch through individual properties. */
.calendar-event-dialog .dialog-content.calendar-event-content > h4 {
  font-family: var(--font-body);
  font-size:   var(--text-sm);
  font-weight: 500;
  color:       var(--ink-soft);
  margin:      var(--space-5) 0;
  display:     flex;
  align-items: center;
  gap:         var(--space-2);
}
.calendar-event-dialog .event-location-icon {
  display:     inline-flex;
  align-items: center;
  color:       var(--accent);
  /* Sized to match the location TEXT (var(--text-sm)) rather than
     scaled up to 1.25rem. Adam reported the row looked asymmetric --
     with the pin at 1.25rem the row's box-height was dominated by the
     pin's line-box, making the visible icon glyph touch the row's top
     edge while the text baseline sat below centre. Optical effect: the
     gap above the row read smaller than the gap below, even with
     symmetric margins on the box.
     Pin at text-size means both items contribute the same line-box
     height; the row centres cleanly and visible top/bottom whitespace
     reads as equal. The pin still reads as a clear location marker --
     terracotta colour does the work, not size. */
  font-size:   var(--text-sm);
  flex-shrink: 0;
}
.calendar-event-dialog .event-location-link {
  color:                  var(--ink);
  text-decoration:        underline;
  text-underline-offset:  3px;
  display:                inline-flex;
  align-items:            center;
  gap:                    var(--space-1);
  transition:             color var(--transition);
}
.calendar-event-dialog .event-location-link:hover {
  color:   var(--accent);
  opacity: 1;
}
.calendar-event-dialog .event-location-link i {
  font-size: 0.875rem;
  opacity:   0.7;
}

/* Event description body */
.calendar-event-dialog .event-details {
  font-family: var(--font-body);
  font-size:   var(--text-sm);
  color:       var(--ink);
  line-height: 1.6;
  margin:      0 0 var(--space-4);
}
/* Event contact line + any paragraph in the event body sit at
   --text-sm so the "Contact: <name>" line visually matches the
   location row above. v0.143 DevTools diagnostic exposed v0.142's
   wrong assumption: Hubb renders the contact as
   `<div class="event-contact"><b>Contact:</b><a>...</a></div>`
   not as a <p>, so the v0.142 `<p>` selector matched zero
   elements and contact kept inheriting the body default
   (--text-base, larger than --text-sm). Adding `.event-contact`
   here so the rule actually targets the right element; the
   <p> selector stays as a fallback for any future structure
   that might use it. Selector specificity (0,3,1) beats anything
   Hubb's base.min.css might apply to <p> or <div> directly. */
.calendar-event-dialog .dialog-content.calendar-event-content p,
.calendar-event-dialog .dialog-content.calendar-event-content .event-contact {
  font-size: var(--text-sm);
}
.calendar-event-dialog .event-details ul {
  margin:       var(--space-2) 0;
  padding-left: var(--space-6);
}
.calendar-event-dialog .event-details li {
  margin-bottom: var(--space-1);
}
.calendar-event-dialog .event-details a {
  color:                 var(--accent);
  text-decoration:       underline;
  text-underline-offset: 3px;
}

/* "More information" CTA -- pill button matching the feature CTA */
.calendar-event-dialog .event-link {
  display:    flex;
  margin-top: var(--space-3);
}
.calendar-event-dialog .event-link a {
  display:         inline-flex;
  align-items:     center;
  padding:         var(--space-3) var(--space-6);
  background:      var(--accent);
  color:           var(--accent-ink);
  border-radius:   999px;
  text-decoration: none;
  font-family:     var(--font-body);
  font-size:       var(--text-xs);
  font-weight:     700;
  letter-spacing:  0.12em;
  text-transform:  uppercase;
  /* Transition only background + opacity; no transform. v0.135 patch:
     Adam asked for nothing-moves-on-hover. The previous translateY(-1px)
     on :hover felt twitchy because it was a movement-based affordance
     -- subtle opacity dip is calmer and still signals interactivity
     clearly. Note we KEEP the :active translateY (touch press feedback);
     that's a tactile confirmation a finger has registered, which is
     a different UX role from hover. */
  transition:      background var(--transition), opacity var(--transition);
}
.calendar-event-dialog .event-link a:hover {
  background: var(--accent-dark);
  opacity:    0.92;
}
.calendar-event-dialog .event-link a:active {
  background: var(--accent-dark);
  transform:  translateY(1px);
}
.calendar-event-dialog .event-link a:focus-visible {
  background:     var(--accent-dark);
  outline:        2px solid var(--accent);
  outline-offset: 2px;
}

/* ===== Media dialog content ============================================
   Design reference: Hubb's Genesis template
   (https://templates.hubb.church/Groups/451503/Genesis.aspx).
   
   Layout model: .media-player is a flex column with `gap` between
   sections. We use `order` on children to get the brief's hierarchy
   (player -> title -> meta -> refs -> files) rather than the DOM
   order Hubb produces (title -> spinner -> region -> image -> h3 ->
   h4 -> refs -> details -> files). Reordering via CSS rather than
   moving DOM nodes lets Hubb's playMedia() keep working unchanged.
   
   Spacing: a single `gap: var(--space-6)` (24px) replaces the per-
   element margins the previous version stacked up. Easier to reason
   about and a calmer rhythm. */

.media-player {
  display:        flex;
  flex-direction: column;
  gap:            var(--space-6);    /* 24px between every section */
}

/* Order children to match the user's refined hierarchy: title at the
   top (acts as the article headline), then speaker/date meta, then
   the media player, then any bible refs, then the file rows.

   This is a refinement on the original brief's "player first" order:
   when seen rendered, the user preferred title-first because the
   drawer reads as a sermon article rather than a media app. The
   player still sits above the file rows, so it remains the visual
   anchor for media-focused interaction. */
.media-player .media-player-title  { order: 1; }
/* Meta wrapper order moved into the combined rule below so the
   negative margin-top stays beside the order on the same selector. */
/* Fallback for the unwrapped case -- if the JS wrap hasn't run yet
   (Hubb's two-phase dialog open sometimes races with our observer)
   the raw h3 and h4 default to order:0 and would render BEFORE the
   title (order:1). Explicit order:2 here keeps them visually correct
   even before/without the wrap. Once the JS wraps them, the wrapper
   div picks up order:2 and these direct-child selectors stop matching
   anything (h3 and h4 are now nested one level deeper).

   v0.141: negative margin-top to halve the title-cluster gaps. The
   parent flex `gap: var(--space-6)` (24px) puts 24px between every
   pair of children, so title->h3 and h3->h4 each got 24px. Adam wanted
   ~half. Pulling each by -12px against the 24px gap gives 12px
   effective spacing. The same rule on .media-player__meta covers the
   wrapped case (only the title->meta gap matters then since h3/h4 are
   inline inside the wrapper). h4's -12px margin only pulls it closer
   to h3 -- the gap *after* h4 (to the next major section) is
   untouched because gap doesn't compound with margin-bottom. */
.media-player > h3,
.media-player > h4,
.media-player .media-player__meta  { order: 2; margin-top: calc(0px - var(--space-3)); }
.media-player .media-player-region,
.media-player .media-summary-image { order: 3; }
.media-player .recording-passage,
.media-player .media-player-refs,
.media-player .media-recording-details,
.media-player .recording-details   { order: 4; }
.media-player .media-files-list    { order: 5; }
.media-player .media-player-spinner { order: 0; }   /* hidden anyway */

/* Title -- no underline accent. The brief is for two rows of header
   text only (title; speaker/date), and the underline registered as
   a third visual row. Padding-bottom is also dropped because the
   parent flex-gap (24px) handles the spacing to the meta line below;
   the previous 12px padding existed only to leave room for the
   underline.

   Selector specificity (0,3,1) deliberately beats Hubb's base.min.css
   rule `div.media-player div.media-player-title { font-weight: bold;
   font-size: 20px; margin-bottom: 15px }` (0,2,2). Adam's DevTools
   diagnostic in v0.137 showed the base rule's margin-bottom: 15px
   was winning the cascade and stacking 15px on top of the parent
   flex-gap's 24px -- producing a visibly larger gap below the
   title than between any other sibling pair. Our 0,2,0 selector
   couldn't override individual properties from the third-party rule.
   Same lesson as the calendar event h4 in v0.136. */
/* Parent font-size normalisation. Hubb's base.min.css applies
   `div.media-player { font-size: 16px }` (specificity 0,1,1).
   The event-title's parent .calendar-event-content was given
   `font-size: inherit` in v0.135, so that title's 1.2em computes
   from the body's responsive --text-base (~17px) -> ~20.4px. The
   media-title sat inside this 16px parent -> 1.2em -> 19.2px.
   Same em value, different parents, different rendered sizes.
   Adam reported "the title are not the same size" in v0.146.

   Override the parent context here so 1.2em on the title computes
   identically in both popups. Specificity 0,2,0 beats Hubb's
   0,1,1. Other elements inside .media-player use explicit rem /
   px / clamp sizes (the file-row labels, the speaker meta, etc.)
   so this only affects the one em-based child that needed
   matching. */
.endis-jquery-ui-dialog .media-player {
  font-size: inherit;
}

.endis-jquery-ui-dialog .media-player .media-player-title {
  font-family:    var(--font-display);
  /* v0.145: switched from clamp(1.5rem, 3vw, 1.875rem) (24-30px) to
     1.2em to match the event-title above. Adam asked for the two
     right-panel title sizes to match for consistency -- both popups
     are dialog headers with the same role in the UI, and reading
     them at different sizes felt arbitrary. em-based scales with the
     parent body font-size (which is itself responsive via clamp on
     --text-base), so no separate mobile rule needed. */
  font-size:      1.2em;
  font-weight:    400;
  font-style:     italic;
  color:          var(--ink);
  margin:         0;                                /* parent flex `gap` handles spacing -- 0 here is essential, not cosmetic */
  letter-spacing: -0.01em;
  line-height:    1.2;
}

/* Player region wrapper. For VIDEO we enforce a 16:9 aspect ratio
   on the wrapper so the dark frame is consistent regardless of
   actual file dimensions, and the rounded corners read as a clean
   "screen". For AUDIO we let the native pill render against the
   panel background -- a black tile around an audio pill would look
   broken. Scoped via :has(video).

   Selector specificity (0,3,1) bumped to beat Hubb's base.min.css
   rule `div.media-player div.media-player-region { margin: 10px 0 }`
   (0,2,2). v0.137 DevTools diagnostic: that base rule was winning
   the cascade and adding 10px above + 10px below the player on top
   of the parent flex-gap's 24px, breaking the "gap is the single
   source of truth" promise of the layout. */
.endis-jquery-ui-dialog .media-player .media-player-region {
  margin:        0;                                  /* parent gap handles it */
  background:    transparent;
  position:      relative;
}
.endis-jquery-ui-dialog .media-player .media-player-region:has(video) {
  background:    #000;
  border-radius: var(--radius-lg);                   /* 12px, brief asks for 12-16 */
  overflow:      hidden;
  aspect-ratio:  16 / 9;
}
.endis-jquery-ui-dialog .media-player .media-player-region:has(video) video {
  width:   100%;
  height:  100%;
  display: block;
  object-fit: cover;
}
/* AUDIO MODE OVERRIDE. v0.141 fix for "horrible black box" report.
   Hubb uses a single <video> element for ALL playback regardless of
   file type -- video AND audio. So the :has(video) rule above
   (which gives video a black 16:9 frame, sensible behaviour) was
   also matching when an audio file was playing, producing a 460x259
   black tile under the audio scrubber. The fix is to detect audio
   mode via the `.media-player-audio` class Hubb adds to the parent
   .media-player when the active file is audio, and override every
   property the video frame rule applies.

   Selector specificity (0,4,1) beats the :has(video) rule's (0,3,2)
   so we don't need !important. The audio scrubber renders at its
   natural height against the dialog background instead of being
   floated inside a black box. */
.endis-jquery-ui-dialog .media-player.media-player-audio .media-player-region {
  background:    transparent;
  aspect-ratio:  auto;
  border-radius: 0;
  overflow:      visible;
}
.endis-jquery-ui-dialog .media-player.media-player-audio .media-player-region video {
  height:     auto;     /* let the audio controls determine height */
  min-height: auto;
}
.media-player .media-player-region audio {
  width:   100%;
  display: block;
}

/* Static summary image -- stands in for the media region when no
   video is rendered (audio sermons get the sermon series artwork
   as a hero, rather than an empty player block). */
.media-player.media-player-video .media-summary-image {
  display: none;
}
.media-player .media-summary-image {
  display:       block;
  width:         100%;
  height:        auto;
  max-height:    320px;
  object-fit:    cover;
  border-radius: var(--radius-lg);                   /* match video region */
  margin:        0;                                   /* parent gap handles it */
}

/* Meta line -- speaker (h3) + date (h4) wrapped in a JS-injected
   .media-player__meta div so they group as a single flex child of
   .media-player (and so the slash-separated inline rendering still
   works). Display the wrapper as a block; h3 and h4 inside stay
   inline with our existing slash separator. */
.media-player .media-player__meta {
  margin: 0;
}
.media-player .media-player__meta > h3,
.media-player .media-player__meta > h4 {
  display:     inline;
  font-family: var(--font-body);
  font-size:   var(--text-base);                      /* ~16-18px, the brief's "meta but readable" range */
  margin:      0;
  line-height: 1.5;
}
.media-player .media-player__meta > h3 {
  font-weight: 600;
  color:       var(--ink);
}
.media-player .media-player__meta > h3::after {
  content:     " / ";
  color:       var(--ink-soft);
  font-weight: 400;
  margin:      0 var(--space-1);
}
.media-player .media-player__meta > h4 {
  font-weight: 400;
  color:       var(--ink-soft);
  font-style:  italic;
}

/* Bible reference / passage -- Hubb uses one of several class names
   depending on how the media item was set up. We style them all so
   the reference always gets consistent treatment regardless of which
   one is in use. Margin reset because the parent flex gap takes care
   of spacing now. */
.media-player .recording-passage,
.media-player .media-player-refs,
.media-player .media-recording-details,
.media-player .recording-details {
  margin:      0;
  font-family: var(--font-body);
  font-size:   var(--text-base);
  color:       var(--ink-soft);
  line-height: 1.5;
}
.media-player .recording-passage a,
.media-player .media-player-refs a,
.media-player .media-recording-details a,
.media-player .recording-details a {
  color:                     var(--ink);
  text-decoration:           underline;
  text-underline-offset:     3px;
  text-decoration-thickness: 1px;
  transition:                color var(--transition);
}
.media-player .recording-passage a:hover,
.media-player .media-player-refs a:hover,
.media-player .media-recording-details a:hover,
.media-player .recording-details a:hover {
  color: var(--accent);
}

/* Files list -- clean horizontal rows. Margin reset since parent
   flex gap handles spacing. */
.media-player .media-files-list {
  list-style:     none;
  margin:         0;
  padding:        0;
  display:        flex;
  flex-direction: column;
  gap:            var(--space-3);          /* 12px between rows */
  /* Defeat three rules from Hubb's base.min.css that target
     `.media-player ul.media-files-list` directly:
       1. border-top: 1px solid #ccc  -- the grey divider line
          appearing above the first card. NOT from our CSS or from
          a Hubb-injected <hr>; it's a default border on the <ul>.
       2. max-height: 310px            -- caps the list and forces
          a scroll container, which is wrong for a sidebar drawer
          where we want all rows visible inline.
       3. overflow-y: auto             -- pairs with the max-height.
     All three need !important to override the base stylesheet's
     specificity (`.media-player ul.media-files-list`). */
  border-top:     0 !important;
  max-height:     none !important;
  overflow:       visible !important;
}

/* Each row: explicit specificity (.endis-jquery-ui-dialog prefix) and
   !important on alignment props so Hubb's own jQuery UI styles can't
   override us. min-height locks consistent row height. box-sizing is
   border-box so the min-height includes padding. Radius is --radius-lg
   (12px) so the rows read as cards rather than rectangles, matching
   the brief's "soft card" treatment. */
.endis-jquery-ui-dialog .media-player .media-file {
  display:       flex !important;
  /* Geometric centre. The framed icons (type icon left, action buttons
     right) and the title+meta column all have similar visual weight,
     so centring each one within the row produces a clean horizontal
     reading line. Earlier versions tried title-line alignment with
     negative-margin nudges, which only made sense when the type icon
     had no frame and floated bare against the text -- once the icon
     gets a 40x40 fill, geometric centre is the right answer. */
  align-items:   center !important;
  gap:           var(--space-4);          /* 16px between icon, text, controls */
  padding:       var(--space-4);          /* 16px all round -- breathing room */
  min-height:    72px;                    /* a touch taller so each row reads
                                             as a substantive card, not a tight strip */
  box-sizing:    border-box;
  /* v0.141: switched from filled `background: var(--bg)` (cream) to
     a transparent fill with a 1px --rule border. Adam's screenshot
     showed Audio and Notes rows reading as if they had no background
     at all -- which is technically true: --bg sits so close to the
     panel's --surface tone (warm-white on warm-white) that the
     difference vanishes against the dialog backdrop. A hairline
     border gives every row a defined edge regardless of how the
     fill colours relate. Active row keeps its accent-soft fill (its
     border-color is matched to the fill below so the overall card
     shape doesn't acquire a competing outline). */
  background:    transparent;
  border:        1px solid var(--rule);
  border-radius: var(--radius-lg);
  outline:       0 !important;            /* defeat jQuery UI's leaked outline */
  transition:    background var(--transition), border-color var(--transition);
}
/* Hover: faint cream tint via --bg. The previous version filled
   with --rule (matching the new border colour) which would now flash
   the entire row to the same tone as the border -- not the effect
   we want. --bg is a step lighter and reads as a soft highlight. */
@media (hover: hover) and (pointer: fine) {
  .endis-jquery-ui-dialog .media-player .media-file:hover {
    background: var(--bg);
  }
}
/* Visible focus halo when keyboard-tabbing through the inner action
   buttons. Subtle accent shadow rather than border so the layout
   doesn't shift. */
.endis-jquery-ui-dialog .media-player .media-file:focus-within {
  box-shadow: 0 0 0 2px var(--accent-soft);
}
.endis-jquery-ui-dialog .media-player .media-file.active {
  background:   var(--accent-soft);
  /* Match border to the fill so the active card reads as a single
     coloured block, not a coloured block inside a contrasting outline. */
  border-color: var(--accent-soft);
}

/* Hide the PNG fallback type icons (we use the icon font version) */
.media-player .media-file > img.media-file-type {
  display: none;
}
.media-player .media-file > i.media-file-type.offlined {
  display: none !important;
}
.media-player .media-file > img.caching {
  display: none;
}

/* Type icon -- framed in a 40x40 rounded-square button matching the
   action buttons on the right. Geometric centring within the row;
   no margin nudge needed because the frame gives the icon enough
   visual weight to balance the action buttons opposite. The frame
   uses --surface (white) so it pops gently against the --bg-tone
   card; on the active card (--accent-soft fill) it pops more clearly,
   which is the right signal for the currently-playing row. */
.endis-jquery-ui-dialog .media-player .media-file > i.media-file-type:not(.offlined) {
  display:         inline-flex !important;
  align-items:     center !important;
  justify-content: center !important;
  align-self:      center !important;
  width:           40px !important;
  height:          40px !important;
  background:      var(--surface);
  /* 8px corners: softer than --radius (4px), tighter than --radius-lg
     (12px). Lands at the visual sweet-spot for small frame buttons -- a
     "rounded square" rather than a chiclet or a card. Computed via the
     existing token rather than introducing a new --radius-md. */
  border-radius:   calc(var(--radius) * 2);
  color:           var(--ink-soft);
  font-size:       20px;
  line-height:     1;       /* defeat any glyph descender pushing the mark down */
  flex-shrink:     0 !important;
  margin:          0 !important;       /* defensive against Hubb's default
                                          right-margin on .media-file-type */
  padding:         0 !important;
}

/* Per-type colour: PDF gets red, audio/video stay neutral */
.media-player .media-file:has(img[alt*="PDF" i]) > i.media-file-type:not(.offlined) {
  color: #c0392b;
}
.media-player .media-file.active > i.media-file-type:not(.offlined) {
  color: var(--accent);
}
.media-player .media-file.active:has(img[alt*="PDF" i]) > i.media-file-type:not(.offlined) {
  color: #c0392b;
}

/* Title + meta column -- centred vertically within the row so the
   block sits between the framed type icon (left) and action buttons
   (right). All three share a horizontal centre line, which is the
   "tidy row" reading the framed-icon design produces.

   Selector prefixed with .endis-jquery-ui-dialog (specificity 0,3,0)
   and key layout props all !important so Hubb's base stylesheet
   can't win on display / flex / align via equal-specificity source
   order. The <span> the markup uses for .media-file-details defaults
   to display: inline, which without our override would prevent the
   internal title-and-meta stack from laying out as a flex column. */
.endis-jquery-ui-dialog .media-player .media-file-details {
  flex:            1 1 auto !important;
  display:         flex !important;
  flex-direction:  column !important;
  justify-content: center !important;       /* centre title+meta vertically
                                               WITHIN the details block --
                                               redundant when the block is
                                               its content's natural height,
                                               but defensive if Hubb sets
                                               a min-height on the span */
  gap:             var(--space-1);          /* 4px between title and meta */
  min-width:       0;
  align-self:      center !important;
}
.endis-jquery-ui-dialog .media-player .media-file-title {
  font-family:   var(--font-body);
  font-size:     var(--text-base);
  font-weight:   600;
  color:         var(--ink);
  white-space:   nowrap;
  overflow:      hidden;
  text-overflow: ellipsis;
  line-height:   1.3;
  margin:        0 !important;
  padding:       0 !important;     /* defensive against Hubb defaults
                                      that might add padding-top on a
                                      <div class="media-file-title"> */
}
.endis-jquery-ui-dialog .media-player .media-file-format,
.endis-jquery-ui-dialog .media-player .media-file-duration {
  font-family: var(--font-body);
  font-size:   var(--text-sm);
  color:       var(--ink-soft);
  line-height: 1.3;
  margin:      0 !important;
  padding:     0 !important;
}
.endis-jquery-ui-dialog .media-player .media-file-duration {
  margin-right: var(--space-2) !important;
}

/* Action buttons -- 40x40 rounded squares matching the type icon
   shape exactly. Same fill (--surface), same corner radius. The
   visual symmetry (matched frames on both sides of the row) is what
   makes geometric centring read as "tidy" -- the eye sees three
   equally-weighted elements (left frame, text block, right frames)
   sharing a common axis.

   Selector / !important pattern matches the type icon and details
   column above -- bumped specificity + locked layout props so
   Hubb's base.min.css can't win on equal-specificity source order. */
.endis-jquery-ui-dialog .media-player .media-file-control {
  display:         inline-flex !important;
  align-items:     center !important;
  justify-content: center !important;
  align-self:      center !important;
  width:           40px !important;
  height:          40px !important;
  background:      var(--surface);
  border-radius:   calc(var(--radius) * 2);     /* 8px, matches type icon */
  color:           var(--ink-soft);
  text-decoration: none !important;
  flex-shrink:     0 !important;
  margin:          0 !important;
  padding:         0 !important;
  transition:      background var(--transition), color var(--transition);
}
.media-player .media-file-control:hover {
  background: var(--accent-soft);
  color:      var(--accent);
  opacity:    1;
}
.media-player .media-file-control:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 2px;
  background:     var(--accent-soft);
  color:          var(--accent);
}
/* On the active (currently-playing) card, the card itself is filled
   with --accent-soft; if the action button's hover bg goes to the
   same --accent-soft, the button frame visually disappears into the
   card. Override so the button stays --surface (white) and only the
   icon colour shifts to --accent -- the hover signal is still clear,
   the frame stays distinct. */
.endis-jquery-ui-dialog .media-player .media-file.active .media-file-control:hover,
.endis-jquery-ui-dialog .media-player .media-file.active .media-file-control:focus-visible {
  background: var(--surface);
  color:      var(--accent);
}
/* Control icon (download / play / pause).
   Bumped specificity + !important on key properties to defeat
   Hubb's base.min.css rule `div.media-player ul.media-files-list
   li i { font-size: 36px; margin-top: 9px; vertical-align: top }`
   (0,2,4) which would otherwise win on specificity over our
   single-class scoping.

   The margin-top: 9px was the visible bug Adam reported in v0.139:
   download icon LOOKED centred (its mdi glyph is bottom-biased --
   arrow + base line -- so the downward push roughly compensated
   for the glyph's natural top-bias) but play (triangle) and pause
   (two bars) sit visually centred in their em-boxes, so the same
   9px push offset them visibly low in their 40x40 frames.

   Same defensive pattern as the type icon (.media-file-type) on
   the left of each row -- that icon doesn't have this issue
   because it has its own !important rule wiping margin/padding.
   Bringing the control icons to parity. */
.endis-jquery-ui-dialog .media-player .media-file-control i {
  font-size:      20px !important;     /* matches type icon */
  line-height:    1 !important;
  margin:         0 !important;        /* defeats margin-top: 9px from base.min.css */
  padding:        0 !important;
  vertical-align: baseline !important; /* defeats vertical-align: top from base.min.css
                                          (moot in our flex parent but defensive) */
}

/* Play/Pause toggle -- show ONE button per row instead of both
   stacked. v0.145 update: switched from `.active` to `.is-playing`.

   v0.143 had the toggle keyed off Hubb's `.active` class on the
   media-file. Adam's v0.144 testing exposed the consequence: a
   row stays `.active` even after pause -- `.active` means
   "selected", not "playing" -- so paused rows kept showing the
   pause button with no way to resume.

   v0.145 introduced `.is-playing`, set by script.js listening to
   the underlying <video>/<audio> element's play / pause / ended
   events. The class is added on 'play' (only to the active row)
   and removed on 'pause' or 'ended'. CSS toggle now reflects real
   playback state, so pausing returns the button to play and the
   user can click it again to resume.

   v0.147: switched the listener mechanism from per-video
   addEventListener (set up by a per-dialog MutationObserver) to
   document-level capture-phase listeners attached once at script
   init. v0.145's per-video approach raced with Hubb's autoplay
   on the <video>: 'play' fired synchronously during element
   creation, before our observer could attach the listener, so
   the very first play was always missed. Document-level capture
   has no race because the listener exists from script init and
   catches every media event regardless of when the element was
   created. (capture: true is required because play/pause/ended
   don't bubble per spec.) See script.js initMediaPlayingTracker().

   The `:has()` icon-class fallbacks stay -- defensive against
   Hubb not using `.media-file-play` / `.media-file-pause` as
   button-modifier classes but conveying the role via the inner
   <i> icon class instead. */
.endis-jquery-ui-dialog .media-player .media-file:not(.is-playing) .media-file-pause,
.endis-jquery-ui-dialog .media-player .media-file:not(.is-playing) .media-file-control:has(i[class*="pause"]) {
  display: none !important;
}
.endis-jquery-ui-dialog .media-player .media-file.is-playing .media-file-play,
.endis-jquery-ui-dialog .media-player .media-file.is-playing .media-file-control:has(i[class*="play"]) {
  display: none !important;
}

/* Spinner -- Hubb's JS toggles this; default hidden in our styling */
.media-player .media-player-spinner {
  display: none;
}

/* Hide refs / recording-details containers by default. Hubb's
   renderer frequently emits these as <div class="x">\n   </div>
   -- whitespace-only -- which defeats :empty matching, leaving
   them rendered with their inherited ui-widget-content border-
   bottom. That border was the grey line appearing above the
   first media-file card in the sermon dialog. Hide unconditionally;
   only show if the element actually has a child (i.e. real
   content, not whitespace).

   Also kill any border on these containers as a belt-and-braces
   measure so even if a future Hubb update populates them with
   text content, no surprise grey rule appears. */
.media-player .media-player-refs,
.media-player .media-recording-details {
  display: none;
  border:  0 !important;
}
.media-player .media-player-refs:has(*),
.media-player .media-recording-details:has(*) {
  display: block;
}

/* Defensive: hide any <hr> Hubb injects inside the dialog body.
   Hubb's media markup often includes a separator between the player
   region and the file list, which by default renders as the browser's
   1px grey horizontal rule. The brief calls for content to flow
   cleanly from the video preview into the file rows without a
   dividing line, so we suppress these. */
.endis-jquery-ui-dialog .ui-dialog-content hr,
.media-player hr {
  display: none !important;
}


/* =========================================================================
   3.15  SECTION BACKGROUND UTILITIES  --  .section-bg--*
   -------------------------------------------------------------------------
   Apply one of these classes to a <section> element to control its
   background colour. Useful when stacking sections vertically and you
   want neighbouring sections to feel distinct rather than blending into
   one continuous block.

   Usage in body Contents:
     <section class="latest-news section-bg--white">
       <h2 class="latest-news__heading">Latest News</h2>
       <module .../>
       <a class="section-link" href="...">Read more news</a>
     </section>

   Available variants:
     .section-bg--cream  Page background (warm off-white). Default look.
     .section-bg--white  Pure white. Useful as a bright break between
                         cream sections.
     .section-bg--soft   Accent-soft warm tint. Subtle highlight, good
                         for "featured" sections.
     .section-bg--dark   Ink dark. High-contrast feature treatment.
                         Headings & links auto-flip to white.

   Each variant includes vertical padding so the background bleeds
   visibly above and below the content. If the section already has its
   own padding, that padding is preserved (these only set background +
   add a sensible vertical pad).
   ========================================================================= */

.section-bg--cream,
.section-bg--white,
.section-bg--soft,
.section-bg--dark {
  padding-block:  var(--space-16);
  margin-inline:  calc(var(--container-gutter) * -1); /* bleed past container if nested */
  padding-inline: var(--container-gutter);
}

.section-bg--cream { background: var(--bg);          color: var(--ink); }
.section-bg--white { background: var(--surface);     color: var(--ink); }
.section-bg--soft  { background: var(--accent-soft); color: var(--ink); }
.section-bg--dark  { background: var(--ink);         color: rgba(255, 255, 255, 0.85); }

/* DARK variant -- flip text colours so headings & links stay legible.
   We target each section heading variant explicitly; a blanket "all h2
   white" rule would catch headings inside cards / modules that already
   have correct colours against their own backgrounds. */
.section-bg--dark .latest-news__heading,
.section-bg--dark .upcoming-events__heading,
.section-bg--dark .latest-media__heading,
.section-bg--dark > h2 {
  color: #FFFFFF;
}

.section-bg--dark .section-link {
  color: rgba(255, 255, 255, 0.95);
}
.section-bg--dark .section-link:hover {
  color: var(--accent-soft);
}

/* Cards inside a dark section need a small visual lift to separate
   from the dark background. Most card modules already use --surface
   or --bg as their own background, which works fine on top of
   --section-bg--dark. If a particular card disappears, add a more
   specific override for that module. */


/* =========================================================================
   3.16  EMPTY STATES  --  .empty-state
   -------------------------------------------------------------------------
   What this is: a fallback render shown when one of the feed modules
   (Latest News, Upcoming Events, Latest Media) has nothing to display.
   Without this, an empty section either renders blank (looks broken),
   or shows Hubb's default "No items" plain text (looks unfinished).

   Injection: addEmptyStates() in script.js inspects each feed section
   after Hubb has rendered, and if the inner module wrapper has no
   item children, appends an .empty-state element with friendly copy
   and a CTA pointing to the section's full archive page.

   Visual approach: subtle, not apologetic. Centered text with a small
   icon glyph above; uses the cream background tint to feel intentional
   rather than empty. Always sits above the section-link so the user
   has a clear path forward.
   ========================================================================= */

.empty-state {
  display:        flex;
  flex-direction: column;
  align-items:    center;
  justify-content: center;
  gap:            var(--space-3);
  padding:        var(--space-12) var(--space-6);
  background:     var(--bg);
  border:         1px dashed var(--rule);
  border-radius:  var(--radius-lg);
  text-align:     center;
  color:          var(--ink-soft);
}

.empty-state__icon {
  width:           48px;
  height:          48px;
  display:         flex;
  align-items:     center;
  justify-content: center;
  background:      var(--surface);
  border-radius:   50%;
  color:           var(--accent);
  font-size:       24px;
  margin-bottom:   var(--space-1);
  border:          1px solid var(--rule);
}

.empty-state__title {
  font-family: var(--font-display);
  font-size:   var(--text-xl);
  font-style:  italic;
  font-weight: 400;
  color:       var(--ink);
  margin:      0;
  line-height: 1.3;
}

.empty-state__text {
  font-family: var(--font-body);
  font-size:   var(--text-base);
  color:       var(--ink-soft);
  margin:      0;
  max-width:   42ch;
  line-height: 1.5;
}


/* =========================================================================
   3.17  FORMS  --  opt-in styling for Hubb form modules
   -------------------------------------------------------------------------
   Hubb's form modules (contact, prayer request, event signup, mailing
   list, etc.) ship with no styling of their own. Without these rules,
   any form added to a page looks like a 1998 browser default -- Times
   New Roman labels, beveled inputs, blue submit button.

   !!  IMPORTANT -- why opt-in via class:
   Hubb is built on ASP.NET Web Forms, which wraps the entire page body
   in a single <form runat="server"> element to enable postbacks. We
   therefore CANNOT target the bare `form` selector -- it would style
   the entire page wrapper. Same risk applies to body/html.
   Lesson learned: we deploy form styles via an opt-in `.hubb-form`
   class. To activate these styles on a Hubb form module, set its
   "CSS Class" field to `hubb-form`.

   Coverage (when .hubb-form class is present on the form / module):
     - Text-style inputs (text, email, tel, url, password, number,
       search, date, datetime-local, time)
     - Textareas
     - Select dropdowns
     - Checkboxes & radios
     - Submit buttons (matched to .feature__cta visual treatment)
     - Labels, legends, fieldsets
     - Required-field indicators
     - Error / validation states
     - Help text
   ========================================================================= */

/* ---- Form layout container ---- */
.hubb-form {
  display:        flex;
  flex-direction: column;
  gap:            var(--space-4);
  font-family:    var(--font-body);
  max-width:      640px;
}

/* ---- Labels ---- */
.hubb-form label {
  display:        block;
  font-family:    var(--font-body);
  font-size:      var(--text-sm);
  font-weight:    600;
  color:          var(--ink);
  margin-bottom:  var(--space-2);
  letter-spacing: 0.01em;
}

/* Required field indicator -- Hubb may already add * but if not,
   any label containing class "required" gets one. */
.hubb-form label.required::after,
.hubb-form .required-field label::after {
  content:    " *";
  color:      var(--accent);
  font-weight: 700;
}

/* ---- Text inputs, textareas, selects ----
   Single shared rule for visual consistency across input types. */
.hubb-form input[type="text"],
.hubb-form input[type="email"],
.hubb-form input[type="tel"],
.hubb-form input[type="url"],
.hubb-form input[type="password"],
.hubb-form input[type="number"],
.hubb-form input[type="search"],
.hubb-form input[type="date"],
.hubb-form input[type="datetime-local"],
.hubb-form input[type="time"],
.hubb-form input:not([type]),
.hubb-form textarea,
.hubb-form select {
  width:           100%;
  padding:         var(--space-3) var(--space-4);
  background:      var(--surface);
  color:           var(--ink);
  font-family:     var(--font-body);
  font-size:       var(--text-base);
  line-height:     1.5;
  border:          1px solid var(--rule);
  border-radius:   var(--radius);
  transition:      border-color var(--transition), box-shadow var(--transition);
  -webkit-appearance: none;
  appearance:      none;
  box-sizing:      border-box;
}

/* Placeholder colour -- match ink-soft so it reads as muted hint */
.hubb-form input::placeholder,
.hubb-form textarea::placeholder {
  color:   var(--ink-soft);
  opacity: 0.7;
}

/* Focus state -- accent-coloured border + soft ring. */
.hubb-form input:focus-visible,
.hubb-form textarea:focus-visible,
.hubb-form select:focus-visible {
  outline:      none;
  border-color: var(--accent);
  box-shadow:   0 0 0 3px var(--accent-soft);
}

/* Textarea-specific */
.hubb-form textarea {
  min-height:  120px;
  resize:      vertical;
  font-family: var(--font-body);
}

/* Select-specific -- append a chevron because we removed default
   appearance. SVG embedded as data URI to avoid an extra request. */
.hubb-form select {
  background-image:    url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2357524B' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><polyline points='6 9 12 15 18 9'></polyline></svg>");
  background-repeat:   no-repeat;
  background-position: right var(--space-4) center;
  background-size:     16px;
  padding-right:       var(--space-12);
}

/* ---- Checkboxes and radios ----
   Native styling is awful and varies by OS/browser. accent-color
   tints the native check/dot to our brand. */
.hubb-form input[type="checkbox"],
.hubb-form input[type="radio"] {
  width:          18px;
  height:         18px;
  margin:         0 var(--space-2) 0 0;
  accent-color:   var(--accent);
  cursor:         pointer;
  vertical-align: middle;
}

/* Inline label adjacent to checkbox/radio -- no margin-bottom */
.hubb-form input[type="checkbox"] + label,
.hubb-form input[type="radio"] + label,
.hubb-form label > input[type="checkbox"],
.hubb-form label > input[type="radio"] {
  display:       inline-block;
  font-weight:   400;
  margin-bottom: 0;
}

/* ---- Fieldsets and legends ---- */
.hubb-form fieldset {
  border:        1px solid var(--rule);
  border-radius: var(--radius);
  padding:       var(--space-4) var(--space-5, 20px);
  margin:        0;
}
.hubb-form legend {
  font-family:    var(--font-body);
  font-size:      var(--text-sm);
  font-weight:    700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color:          var(--ink);
  padding:        0 var(--space-2);
}

/* ---- Help text ---- */
.hubb-form .help-text,
.hubb-form .field-hint,
.hubb-form small.form-text {
  display:    block;
  margin-top: var(--space-1);
  font-size:  var(--text-xs);
  color:      var(--ink-soft);
  font-style: italic;
}

/* ---- Validation / error states ---- */
.hubb-form input.error,
.hubb-form textarea.error,
.hubb-form select.error,
.hubb-form input.invalid,
.hubb-form textarea.invalid,
.hubb-form select.invalid,
.hubb-form input[aria-invalid="true"],
.hubb-form textarea[aria-invalid="true"],
.hubb-form select[aria-invalid="true"] {
  border-color: #c0392b;
  background:   rgba(192, 57, 43, 0.04);
}
.hubb-form input.error:focus-visible,
.hubb-form textarea.error:focus-visible,
.hubb-form input[aria-invalid="true"]:focus-visible,
.hubb-form textarea[aria-invalid="true"]:focus-visible {
  border-color: #c0392b;
  box-shadow:   0 0 0 3px rgba(192, 57, 43, 0.15);
}

.hubb-form .error-message,
.hubb-form .field-error,
.hubb-form .validation-message {
  display:     block;
  margin-top:  var(--space-2);
  font-size:   var(--text-sm);
  color:       #c0392b;
  font-weight: 600;
}

/* Success state (e.g. after form submission) */
.hubb-form .success-message {
  display:       block;
  padding:       var(--space-4);
  background:    rgba(46, 125, 50, 0.08);
  border:        1px solid rgba(46, 125, 50, 0.3);
  border-radius: var(--radius);
  color:         #2e7d32;
  font-weight:   600;
}

/* ---- Submit button ----
   Match the existing .feature__cta visual treatment. */
.hubb-form button[type="submit"],
.hubb-form input[type="submit"],
.hubb-form .button.submit {
  display:           inline-flex;
  align-items:       center;
  justify-content:   center;
  gap:               var(--space-3);
  align-self:        flex-start;
  padding:           var(--space-3) var(--space-6);
  background:        var(--accent);
  color:             var(--accent-ink);
  font-family:       var(--font-body);
  font-weight:       700;
  font-size:         var(--text-xs);
  letter-spacing:    0.1em;
  text-transform:    uppercase;
  text-decoration:   none;
  border:            0;
  border-radius:     999px;
  cursor:            pointer;
  transition:        background var(--transition), transform var(--transition);
  -webkit-appearance: none;
  appearance:        none;
}
.hubb-form button[type="submit"]:focus-visible,
.hubb-form input[type="submit"]:focus-visible {
  background: var(--accent-dark);
  outline:    2px solid var(--ink);
  outline-offset: 2px;
}
.hubb-form button[type="submit"]:active,
.hubb-form input[type="submit"]:active {
  background: var(--accent-dark);
  transform:  translateY(1px);
}
@media (hover: hover) {
  .hubb-form button[type="submit"]:hover,
  .hubb-form input[type="submit"]:hover {
    background: var(--accent-dark);
  }
}

/* Disabled state */
.hubb-form button[disabled],
.hubb-form input[disabled],
.hubb-form select[disabled],
.hubb-form textarea[disabled] {
  opacity:        0.55;
  cursor:         not-allowed;
  pointer-events: none;
}


/* =========================================================================
   3.18  ERROR / 404 PAGES  --  .page-error
   -------------------------------------------------------------------------
   Hubb's default 404 (and 500-level error) pages render inside the
   site template -- header and footer in place -- but the body content is
   typically a bare error message that looks like an internal staging
   page. We provide both:
   
     1. A body Contents pattern Adam can drop into the 404 template:
        <section class="page-error">
          <div class="page-error__inner">
            <p class="page-error__code">404</p>
            <h1 class="page-error__title">Page not found</h1>
            <p class="page-error__text">
              We can't find the page you're looking for. It may have
              moved, or the link may be out of date.
            </p>
            <div class="page-error__actions">
              <a href="/" class="feature__cta">Return home</a>
              <a href="/Contact.aspx" class="section-link">Contact us</a>
            </div>
          </div>
        </section>
   
     2. Defensive styling for any common Hubb error-page class so even
        an unstyled default error page picks up reasonable typography.
   ========================================================================= */

.page-error,
.page-not-found,
.error-page {
  display:        flex;
  align-items:    center;
  justify-content: center;
  min-height:     60vh;
  padding:        var(--space-16) var(--space-6);
  text-align:     center;
  background:     var(--bg);
}

.page-error__inner {
  display:        flex;
  flex-direction: column;
  align-items:    center;
  gap:            var(--space-4);
  max-width:      540px;
}

.page-error__code {
  font-family:    var(--font-display);
  font-size:      clamp(4rem, 12vw, 8rem);
  font-weight:    400;
  font-style:     italic;
  color:          var(--accent);
  margin:         0;
  line-height:    1;
  letter-spacing: -0.04em;
}

.page-error__title {
  font-family:    var(--font-display);
  font-size:      clamp(1.75rem, 4vw, 2.5rem);
  font-weight:    400;
  font-style:     italic;
  color:          var(--ink);
  margin:         0;
  line-height:    1.2;
}

.page-error__text {
  font-family: var(--font-body);
  font-size:   var(--text-lg);
  color:       var(--ink-soft);
  margin:      0;
  line-height: 1.5;
}

.page-error__actions {
  display:        flex;
  flex-wrap:      wrap;
  align-items:    center;
  justify-content: center;
  gap:            var(--space-4);
  margin-top:     var(--space-4);
}


/* =========================================================================
   3.19  SIDE PANEL  --  generic right-side slide-out pattern
   -------------------------------------------------------------------------
   Reusable side-panel pattern used by the visitor widget (3.20) and
   the account panel (3.21). Same visual language as the event/media
   dialogs (3.14) but lighter-weight: not a modal (body still scrolls),
   no jQuery UI involvement, single CSS class controls open state.

   Markup pattern (built by JS):
     <aside class="side-panel" id="..." role="dialog" aria-label="..." hidden>
       <div class="side-panel__backdrop"></div>
       <div class="side-panel__sheet">
         <button class="side-panel__close" aria-label="Close">x</button>
         <div class="side-panel__content"> ...arbitrary content... </div>
       </div>
     </aside>

   To open: remove [hidden] attribute and add .is-open class
   To close: remove .is-open class, then [hidden] after transition

   We keep body scroll active while the panel is open -- this isn't
   a modal that demands all attention, it's contextual content. The
   backdrop fades in subtly.
   ========================================================================= */

.side-panel {
  position:   fixed;
  inset:      0;
  z-index:    19000;          /* above page chrome, below jQuery UI dialogs (20003) */
  pointer-events: none;       /* allow clicks through when closed */
}
.side-panel.is-open {
  pointer-events: auto;
}
.side-panel[hidden] {
  display: none !important;
}

.side-panel__backdrop {
  position:   absolute;
  inset:      0;
  background: rgba(26, 22, 18, 0.45);
  opacity:    0;
  transition: opacity 280ms cubic-bezier(0.16, 1, 0.3, 1);
}
.side-panel.is-open .side-panel__backdrop {
  opacity: 1;
}

.side-panel__sheet {
  position:        absolute;
  top:             0;
  right:           0;
  bottom:          0;
  width:           min(420px, 100vw);
  background:      var(--surface);
  box-shadow:      -16px 0 48px -12px rgba(26, 22, 18, 0.25);
  display:         flex;
  flex-direction:  column;
  transform:       translateX(100%);
  transition:      transform 280ms cubic-bezier(0.16, 1, 0.3, 1);
  overflow:        hidden;
}
.side-panel.is-open .side-panel__sheet {
  transform: translateX(0);
}

/* Close button -- pinned top-right, large hit target, matches the
   dialog close button visual treatment (3.14). Safe-area insets
   compose with our spacing tokens so the button stays tappable
   when the device's notch / dynamic island would otherwise overlap
   it (iPhone in landscape). */
.side-panel__close {
  position:        absolute;
  top:             calc(var(--space-4) + var(--safe-top));
  right:           calc(var(--space-4) + var(--safe-right));
  width:           40px;
  height:          40px;
  display:         inline-flex;
  align-items:     center;
  justify-content: center;
  background:      transparent;
  border:          0;
  border-radius:   50%;
  color:           var(--ink-soft);
  font-size:       28px;
  line-height:     1;
  cursor:          pointer;
  z-index:         2;
  transition:      background var(--transition), color var(--transition);
}
.side-panel__close:hover,
.side-panel__close:focus-visible {
  background: var(--accent-soft);
  color:      var(--accent);
}
.side-panel__close:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 2px;
}
.side-panel__close:active {
  background: var(--accent);
  color:      var(--accent-ink);
  transition: background 80ms ease, color 80ms ease;
}

/* Content scrolls within the sheet, leaving the close button fixed.
   Top padding clears the absolutely-positioned close button. Bottom
   padding adds the device's safe-area-inset-bottom so the last bit
   of scrollable content can scroll up past the iPhone home indicator
   (otherwise the final lines sit behind the indicator). */
.side-panel__content {
  flex:        1;
  overflow-y:  auto;
  padding:     calc(var(--space-12) + var(--safe-top)) var(--space-6) calc(var(--space-6) + var(--safe-bottom));
}

/* Reduced motion -- instant open/close */
@media (prefers-reduced-motion: reduce) {
  .side-panel__backdrop,
  .side-panel__sheet {
    transition: none !important;
  }
}


/* =========================================================================
   3.20  VISITOR WIDGET  --  :is(.seeker-box, .SeekerBox):has(.visitor_widget)
   -------------------------------------------------------------------------
   "Hello / new here?" floating tab in the bottom-right corner that
   opens a side panel with a "Planning your Visit" article. Used to
   warmly invite first-time visitors. Hubb's SeekerBox module renders
   the markup; we add the .visitor_widget CSS Class to opt in.

   Hubb-rendered structure (simplified):
     <section class="SeekerBox HideElement visitor_widget">
       <div class="seeker_box">     <- floating tab (always visible)
         ...little hello card...
       </div>
       <div class="FullSeekerArticle">  <- expanded article (hidden by default)
         <h2>Planning your Visit</h2>
         <module .../>
         <img class="Close">
       </div>
       <img class="Close Outer">
     </section>

   Our approach: style the existing markup as a floating-tab + side-
   panel pattern. JS handlers wire up open/close (and hide the widget
   when the user is logged in). We override Hubb's existing
   show/hide behaviour cleanly via CSS scoped to .visitor_widget.
   ========================================================================= */

:is(.seeker-box, .SeekerBox).HideElement:has(.visitor_widget),
:is(.seeker-box, .SeekerBox):has(.visitor_widget) {
  /* Override Hubb's HideElement behaviour for this opted-in variant.
     The .HideElement class is Hubb's generic display:none utility;
     we want the floating tab visible at all times and use our own
     .is-open class to control panel visibility.
     
     We list both :is(.seeker-box, .SeekerBox).HideElement:has(...) and just :is(.seeker-box, .SeekerBox):has(...)
     so we win the specificity battle whether Hubb's native rule
     uses .HideElement or :is(.seeker-box, .SeekerBox).HideElement. */
  display: block !important;
  position:        fixed !important;
  /* Bottom + right offsets compose with safe-area insets so the
     pill clears the iPhone home indicator (~34px) and any side
     notch in landscape. On non-notched devices these tokens
     resolve to 0 and behaviour is unchanged. */
  bottom:          calc(var(--space-6) + var(--safe-bottom));
  right:           calc(var(--space-6) + var(--safe-right));
  top:             auto !important;
  left:            auto !important;
  z-index:         9100;          /* above mobile nav (9050), below side panels (19000) */
  pointer-events:  none;
  width:           auto !important;
  height:          auto !important;
  max-width:       none;
  margin:          0 !important;
}

/* Hide the widget for logged-in users. JS adds .is-hidden-for-user
   when it detects a personal greeting or a logout link; we need a
   CSS rule (not inline style) here because the override above uses
   `display: block !important`, which beats any inline display:none
   that JS sets via `element.style.display`.

   Selectors mirror the override block so specificity is at least
   equal in every variant; coming AFTER the override in source order
   means cascade wins where specificity ties. */
:is(.seeker-box, .SeekerBox).HideElement:has(.visitor_widget).is-hidden-for-user,
:is(.seeker-box, .SeekerBox):has(.visitor_widget).is-hidden-for-user {
  display: none !important;
}

/* Hide the widget when the visitor explicitly dismissed it via the
   X button on the pill. Persists across visits via localStorage
   (set in JS). Same specificity-mirror pattern as the logged-in
   hide above for the same reason -- needs to beat the override's
   `display: block !important`. */
:is(.seeker-box, .SeekerBox).HideElement:has(.visitor_widget).is-dismissed,
:is(.seeker-box, .SeekerBox):has(.visitor_widget).is-dismissed {
  display: none !important;
}

/* Floating tab -- the "Hello / Visitors click here" pill. Compact,
   warm, easy to dismiss visually. */
/* Floating tab -- compact pill matching Hope Church's pattern.
   Layout: small "Hello!" bubble floats at the TOP-RIGHT corner of
   the main pill, partially overlapping. The main pill itself
   contains just the "Visitors click here" call to action. This
   reads more like a tasteful badge than the chunkier inline
   layout we had before. */
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .seeker_box {
  pointer-events:  auto;
  position:        relative;       /* anchor for the absolutely-positioned HeadLink */
  display:         inline-block;
  /* Slightly more vertical padding to give the pill a satisfying
     pill shape; horizontal padding stays generous on the right
     to balance the pill optically against the floating bubble. */
  padding:         var(--space-3) var(--space-6);
  background:      var(--ink);
  color:           #FFFFFF;
  border-radius:   999px;
  /* Lighter base shadow than earlier versions -- was too heavy
     against the cream background. */
  box-shadow:      0 6px 18px -6px rgba(26, 22, 18, 0.25);
  cursor:          pointer;
  transition:      box-shadow var(--transition);
  /* Top + right margin reserves space for the Hello! bubble that
     overlaps above and past the pill -- without it the bubble would
     be clipped by the parent .visitor_widget container which sits
     against the viewport edge. With a 96px bubble, we need ~48px
     of clearance above and ~32px to the right. */
  margin-top:      var(--space-12);
  margin-right:    var(--space-8);
}
/* Hover gated to actual hover-capable pointers (mouse / trackpad)
   so it doesn't stick on touch devices. Touch devices get the
   :active state below for press feedback. */
@media (hover: hover) and (pointer: fine) {
  :is(.seeker-box, .SeekerBox):has(.visitor_widget) .seeker_box:hover {
    /* Very subtle hover -- barely-there shadow nudge for tactile
       feedback. Earlier versions intensified the shadow significantly
       which felt loud, especially with the floating Hello bubble
       beside it amplifying the visual weight. */
    box-shadow: 0 8px 22px -6px rgba(26, 22, 18, 0.32);
  }
}

/* Touch press feedback: subtle scale-down for tactile response.
   Excludes the Hello! bubble (.HeadLink) from the transform via
   the "it lives inside" but `transform` cascades through children
   so we limit to a `filter` change which doesn't compound. */
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .seeker_box:active {
  filter:     brightness(0.92);
  transition: filter 80ms ease;
}

/* Hide the original Hubb table/wrapper boilerplate inside the seeker_box
   that we don't need (the .pagebox table is a layout artifact, the rota
   signup link is empty). */
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .seeker_box .pagebox,
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .seeker_box .ArticleRotaSignupLink {
  display: none !important;
}

/* "Hello!" bubble -- floats at the top-right of the pill, partially
   overlapping above it. Terracotta circle so the white "HelloWhite.png"
   image inside reads cleanly. Slight shadow makes it feel like a
   distinct element above the pill rather than glued onto it. */
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .seeker_box .HeadLink {
  position:        absolute;
  top:             calc(var(--space-12) * -1);  /* lift ~48px (half of 96) above the pill */
  right:           calc(var(--space-4) * -1);   /* poke 16px past the right edge */
  width:           96px;
  height:          96px;
  border-radius:   50%;
  background:      var(--accent);
  display:         flex;
  align-items:     center;
  justify-content: center;
  box-shadow:      0 4px 12px -3px rgba(26, 22, 18, 0.18);
  z-index:         2;
}
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .seeker_box .HeadLink .Photo {
  width:        60px;
  height:       60px;
  background:   transparent;       /* parent bubble already terracotta */
  padding:      0;
  border-radius: 0;                /* image already a circular icon */
}

/* Article body inside the floating tab -- show one of the two text
   variants (verbose or non-verbose) depending on width. By default
   show the short one for a compact pill. */
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .seeker_box .ArticleBody {
  font-family:    var(--font-body);
  font-size:      var(--text-sm);
  font-weight:    600;
  line-height:    1.3;
  letter-spacing: 0.01em;
  /* Reserve room on the right for the floating Hello bubble.
     Math: bubble is 96px wide, positioned right:-16px -> extends
     80px in from pill's right edge. Pill itself has 24px right
     padding. So article body needs at least 80-24 = 56px of right
     padding to keep text clear. We use 76px (--space-16 + --space-3)
     for ~20px of comfortable visual breathing room beside the bubble. */
  padding-right:  calc(var(--space-16) + var(--space-3));
}
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .seeker_box .VerboseText {
  display: none;        /* compact pill -- show NonVerboseText only */
}
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .seeker_box .NonVerboseText a,
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .seeker_box .NonVerboseText {
  color:           #FFFFFF;
  text-decoration: none;
}

/* The expanded article becomes a side panel via .is-open toggling */
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle {
  position:       fixed;
  top:            0;
  right:          0;
  bottom:         0;
  width:          min(540px, 100vw);
  background:     var(--surface);
  box-shadow:     -16px 0 48px -12px rgba(26, 22, 18, 0.25);
  z-index:        19001;
  pointer-events: auto;
  transform:      translateX(100%);
  transition:     transform 280ms cubic-bezier(0.16, 1, 0.3, 1);
  overflow-y:     auto;
  /* Tighter top padding (was --space-12). Close button still has
     enough clearance because it's positioned absolutely; this just
     pulls the heading closer to the top so the panel doesn't read
     as half-empty above the fold. Safe-area-inset-bottom added so
     the form / closing copy can scroll past the iPhone home
     indicator instead of sitting behind it. */
  padding:        calc(var(--space-6) + var(--safe-top))
                  var(--space-6)
                  calc(var(--space-6) + var(--safe-bottom));
  display:        block;             /* override Hubb's potential display:none */
}
:is(.seeker-box, .SeekerBox):has(.visitor_widget).is-open .FullSeekerArticle {
  transform: translateX(0);
}

/* Backdrop scrim -- sibling pseudo-element on the panel section.
   Doesn't capture clicks unless visible; click closes panel via JS. */
:is(.seeker-box, .SeekerBox):has(.visitor_widget)::before {
  content:        "";
  position:       fixed;
  inset:          0;
  background:     rgba(26, 22, 18, 0.45);
  opacity:        0;
  z-index:        19000;
  pointer-events: none;
  transition:     opacity 280ms cubic-bezier(0.16, 1, 0.3, 1);
}
:is(.seeker-box, .SeekerBox):has(.visitor_widget).is-open::before {
  opacity:        1;
  pointer-events: auto;
}

/* Hide the floating tab while the panel is open (it would peek out
   beside the panel awkwardly otherwise). */
:is(.seeker-box, .SeekerBox):has(.visitor_widget).is-open .seeker_box {
  opacity:        0;
  pointer-events: none;
  transition:     opacity 200ms ease;
}

/* Style the panel content. The Hubb article inside is rich content
   (h1/h2/h3, p, img, ul, table). Use base typography that complements
   the welcome article styling (3.9). */
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle > h2 {
  font-family:    var(--font-display);
  font-size:      clamp(1.5rem, 3vw, 1.875rem);
  font-style:     italic;
  font-weight:    400;
  color:          var(--ink);
  /* Tighter bottom margin (was --space-6). The article's own opening
     content follows immediately -- too much gap made the panel read
     as two disconnected blocks. */
  margin:         0 0 var(--space-3);
  padding-right:  var(--space-12);    /* clearance for close button */
  letter-spacing: -0.01em;
  line-height:    1.2;
}

/* Hide the article's first h1 -- it's typically the article title
   ("A Warm Hello") which duplicates our static "Planning your Visit"
   heading above. Only the FIRST h1 in the article body gets hidden;
   any further h1s in the article (e.g. "Getting Connected", "Senior
   Leaders" later in the content) still display normally. Same
   pattern used for safeguarding (.safeguarding_notice .ArticleBody > h1). */
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .visitor_widget .ArticleBody h1:first-of-type {
  display: none;
}

/* Hubb's WYSIWYG editor often inserts a <br> immediately after the
   article title -- it's invisible normally but with the h1 hidden it
   becomes an orphan ~20px gap before the first real content. Remove
   it. Also zero the top margin on the first image so it sits right
   under the panel's "Planning your Visit" heading without an awkward
   void between them. */
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .visitor_widget .ArticleBody h1:first-of-type + br {
  display: none;
}
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .visitor_widget .ArticleBody h1:first-of-type ~ img:first-of-type,
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .visitor_widget .ArticleBody > img:first-child,
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .visitor_widget .ArticleBody > div > img:first-child {
  margin-top: 0;
}

:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle h1 {
  font-family:    var(--font-display);
  font-size:      clamp(1.375rem, 2.5vw, 1.625rem);
  font-style:     italic;
  font-weight:    400;
  color:          var(--ink);
  margin:         var(--space-6) 0 var(--space-3);
  letter-spacing: -0.01em;
  line-height:    1.2;
}
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle h3 {
  font-family:    var(--font-body);
  font-size:      var(--text-base);
  font-weight:    700;
  letter-spacing: 0.02em;
  color:          var(--ink);
  margin:         var(--space-6) 0 var(--space-2);
}
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle p {
  font-family: var(--font-body);
  font-size:   var(--text-sm);
  line-height: 1.6;
  color:       var(--ink-soft);
  margin:      0 0 var(--space-3);
}
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle ul,
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle ol {
  margin:       0 0 var(--space-3);
  padding-left: var(--space-6);
  font-family:  var(--font-body);
  font-size:    var(--text-sm);
  line-height:  1.6;
  color:        var(--ink-soft);
}
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle a {
  color:                     var(--accent-dark);
  text-decoration:           underline;
  text-underline-offset:     3px;
  text-decoration-thickness: 1px;
}

/* ---- Inline contact form styling ----
   Hubb's WYSIWYG editor produces tables-with-inputs markup rather
   than a clean <form> element when content authors build a form
   inside an Article. The outer table has inline styles
   (background-color:#f5f5f5; border:15px solid #f3f3f3) that we
   override with !important. Inner tables are layout-only -- make
   them transparent and properly spaced. Inputs/textarea/button get
   our standard form treatment. */
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle table[style*="background-color"] {
  background-color: var(--bg) !important;
  border:           1px solid var(--rule) !important;
  border-radius:    var(--radius);
  padding:          var(--space-2) !important;
  margin:           var(--space-4) 0;
  width:            100%;
  border-collapse:  separate;
}

/* Inner layout tables (label / input pairs) -- strip backgrounds and
   borders so they don't compete with the form's outer container. */
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle table table {
  background-color: transparent !important;
  border:           0 !important;
  margin:           var(--space-2) 0;
}

/* Cells: comfortable padding, top-aligned, smaller label text */
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle table td {
  padding:        var(--space-2) var(--space-3);
  vertical-align: top;
  font-size:      var(--text-sm);
  color:          var(--ink);
  line-height:    1.4;
}

/* Inputs and textarea -- match our form-element treatment from
   section 3.17 (.hubb-form), but scoped to the visitor panel since
   this isn't a real <form> element. Inputs are slightly more
   compact than the .hubb-form versions to fit the narrower panel. */
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle input[type="text"],
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle input[type="email"],
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle input[type="tel"],
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle input:not([type]),
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle textarea {
  width:               100%;
  padding:             var(--space-2) var(--space-3);
  background:          var(--surface);
  color:               var(--ink);
  font-family:         var(--font-body);
  font-size:           var(--text-sm);
  line-height:         1.5;
  border:              1px solid var(--rule);
  border-radius:       var(--radius);
  -webkit-appearance:  none;
  appearance:          none;
  box-sizing:          border-box;
  transition:          border-color var(--transition), box-shadow var(--transition);
}
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle input:focus-visible,
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle textarea:focus-visible {
  outline:      none;
  border-color: var(--accent);
  box-shadow:   0 0 0 3px var(--accent-soft);
}
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle textarea {
  min-height:  100px;
  resize:      vertical;
  font-family: var(--font-body);
}

/* Send button -- match .feature__cta language. Hubb sets inline
   width:90px which we override so the pill can size to its content. */
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle input.sitebutton,
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle input[type="button"] {
  width:              auto !important;
  display:            inline-flex;
  align-items:        center;
  justify-content:    center;
  padding:            var(--space-3) var(--space-6) !important;
  background:         var(--accent);
  color:              var(--accent-ink);
  font-family:        var(--font-body);
  font-size:          var(--text-xs);
  font-weight:        700;
  letter-spacing:     0.1em;
  text-transform:     uppercase;
  border:             0;
  border-radius:      999px;
  cursor:             pointer;
  transition:         background var(--transition);
  -webkit-appearance: none;
  appearance:         none;
}
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle input.sitebutton:hover,
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle input[type="button"]:hover {
  background: var(--accent-dark);
}

/* Hide the LastPass autofill icon Hubb's form inputs sometimes
   inherit -- it overlaps inputs awkwardly in the narrow panel. */
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle [data-lastpass-icon-root] {
  display: none !important;
}
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle img {
  max-width:     100%;
  height:        auto;
  border-radius: var(--radius);
  margin:        var(--space-3) 0;
}

/* Hide the original Hubb close-image affordances -- JS adds a clean
   close button matching our side-panel pattern. */
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .Close,
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .Close.Outer {
  display: none !important;
}

/* The injected close button (added by JS, .visitor-widget__close).
   Top + right offsets compose with safe-area insets so the button
   stays tappable even when the device's notch / dynamic island
   would otherwise overlap it (iPhone in landscape). */
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .visitor-widget__close {
  position:        absolute;
  top:             calc(var(--space-4) + var(--safe-top));
  right:           calc(var(--space-4) + var(--safe-right));
  width:           40px;
  height:          40px;
  display:         inline-flex;
  align-items:     center;
  justify-content: center;
  background:      transparent;
  border:          0;
  border-radius:   50%;
  color:           var(--ink-soft);
  font-size:       28px;
  line-height:     1;
  cursor:          pointer;
  z-index:         2;
  transition:      background var(--transition), color var(--transition);
}
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .visitor-widget__close:hover,
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .visitor-widget__close:focus-visible {
  background: var(--accent-soft);
  color:      var(--accent);
}
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .visitor-widget__close:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 2px;
}
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .visitor-widget__close:active {
  background: var(--accent);
  color:      var(--accent-ink);
  transition: background 80ms ease, color 80ms ease;
}

/* Pill DISMISS button -- small X before the "Visitors click here"
   text. Lets logged-out visitors who don't want the invite hide
   it permanently (per-device, via localStorage). Distinct from the
   panel close button above:
     - Panel close   : closes open side panel, widget remains for re-open
     - Pill dismiss  : hides widget entirely, persists across visits

   Visually subordinated (opacity 0.7) so the primary CTA still wins
   the eye, but always visible so it's discoverable on touch devices
   that have no hover. 24x24 minimum hit target meets WCAG 2.2 AA
   target-size minimum even where the visible glyph is smaller. */
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .visitor-widget__dismiss {
  appearance:      none;
  -webkit-appearance: none;
  border:          0;
  background:      transparent;
  padding:         0;
  width:           24px;
  height:          24px;
  display:         inline-flex;
  align-items:     center;
  justify-content: center;
  vertical-align:  middle;        /* sit on the text baseline of the CTA */
  margin-right:    var(--space-2);
  /* Pull leftwards a touch so the visual weight of the X aligns with
     the start of the pill rather than indented past its left padding. */
  margin-left:     calc(var(--space-2) * -1);
  color:           rgba(255, 255, 255, 0.7);
  font-family:     var(--font-body);
  font-size:       20px;           /* visible glyph ~14-16px after font cap */
  line-height:     1;
  border-radius:   50%;
  cursor:          pointer;
  transition:      background var(--transition), color var(--transition);
}
@media (hover: hover) and (pointer: fine) {
  :is(.seeker-box, .SeekerBox):has(.visitor_widget) .visitor-widget__dismiss:hover {
    color:      #FFFFFF;
    background: rgba(255, 255, 255, 0.14);
  }
}
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .visitor-widget__dismiss:focus-visible {
  outline:        2px solid #FFFFFF;
  outline-offset: 2px;
  color:          #FFFFFF;
}
:is(.seeker-box, .SeekerBox):has(.visitor_widget) .visitor-widget__dismiss:active {
  background: rgba(255, 255, 255, 0.22);
  transition: background 80ms ease;
}


/* =========================================================================
   3.21  ACCOUNT PANEL  --  .account-toggle + .account-panel
   -------------------------------------------------------------------------
   Inline pill button in the header (between nav and search) that opens
   a side panel with login/register/logout/web-office links.

   The toggle button + panel are both injected by JS
   (setupAccountPanel) -- Adam doesn't add anything to body Contents.
   The panel content is built dynamically from links found inside the
   (visually hidden) .statusbar element so we always reflect Hubb's
   current login state without separate state management.
   ========================================================================= */

/* Toggle button -- small pill matching the body language without
   competing with the primary nav. Person icon + label. */
.account-toggle {
  display:         inline-flex;
  align-items:     center;
  gap:             var(--space-2);
  padding:         var(--space-2) var(--space-4);
  background:      transparent;
  border:          1px solid var(--rule);
  border-radius:   999px;
  color:           var(--ink);
  font-family:     var(--font-body);
  font-size:       var(--text-xs);
  font-weight:     700;
  letter-spacing:  0.1em;
  text-transform:  uppercase;
  cursor:          pointer;
  transition:      background var(--transition), border-color var(--transition), color var(--transition);
  flex-shrink:     0;
}
.account-toggle:hover,
.account-toggle:focus-visible {
  background:   var(--accent);
  border-color: var(--accent);
  color:        var(--accent-ink);
}
.account-toggle:focus-visible {
  outline:        2px solid var(--ink);
  outline-offset: 2px;
}
.account-toggle__icon {
  width:  16px;
  height: 16px;
  flex-shrink: 0;
}

/* Panel content styling -- uses generic .side-panel pattern + extras */
.account-panel .side-panel__content {
  /* Slightly more vertical breathing room than default */
  padding-top: var(--space-16);
}
.account-panel__title {
  font-family:    var(--font-display);
  font-size:      clamp(1.5rem, 3vw, 1.875rem);
  font-style:     italic;
  font-weight:    400;
  color:          var(--ink);
  margin:         0 0 var(--space-2);
  letter-spacing: -0.01em;
  line-height:    1.2;
}
.account-panel__subtitle {
  font-family: var(--font-body);
  font-size:   var(--text-sm);
  color:       var(--ink-soft);
  margin:      0 0 var(--space-8);
  line-height: 1.5;
}
.account-panel__actions {
  display:        flex;
  flex-direction: column;
  gap:            var(--space-3);
}
.account-panel__primary {
  display:         inline-flex;
  align-items:     center;
  justify-content: center;
  padding:         var(--space-3) var(--space-6);
  background:      var(--accent);
  color:           var(--accent-ink) !important; /* beats Hubb a { color } -- this pill is an <a> */
  font-family:     var(--font-body);
  font-size:       var(--text-xs);
  font-weight:     700;
  letter-spacing:  0.1em;
  text-transform:  uppercase;
  text-decoration: none;
  border:          0;
  border-radius:   999px;
  cursor:          pointer;
  transition:      background var(--transition);
}
.account-panel__primary:hover,
.account-panel__primary:focus-visible {
  background: var(--accent-dark);
}
.account-panel__secondary {
  display:         inline-flex;
  align-items:     center;
  justify-content: center;
  padding:         var(--space-3) var(--space-6);
  background:      transparent;
  color:           var(--ink) !important; /* beats Hubb a { color } -- this pill is an <a> */
  font-family:     var(--font-body);
  font-size:       var(--text-xs);
  font-weight:     700;
  letter-spacing:  0.1em;
  text-transform:  uppercase;
  text-decoration: none;
  border:          1px solid var(--rule);
  border-radius:   999px;
  cursor:          pointer;
  transition:      background var(--transition), border-color var(--transition);
}
.account-panel__secondary:hover,
.account-panel__secondary:focus-visible {
  background:   var(--bg);
  border-color: var(--ink);
}

/* v0.149: .account-panel__link styled as an outlined pill, matching
   .account-panel__secondary visually and the header ACCOUNT button.
   Web Office (and any future "My Area" or similar nav links) now
   reads as a button rather than a plain underlined text link, which
   matches Adam's reference: "the Web Office button should be a
   featured pill button like Account".

   The semantic split between __link and __primary stays:
     .account-panel__link      -> nav links (Web Office, My Area, ...)
     .account-panel__primary   -> primary destructive action (Log out)
   Both render as pills; __link is outlined, __primary is filled. */
.account-panel__link {
  display:         inline-flex;
  align-items:     center;
  justify-content: center;
  padding:         var(--space-3) var(--space-6);
  background:      transparent;
  color:           var(--ink) !important; /* beats Hubb a { color } -- this pill is an <a> */
  font-family:     var(--font-body);
  font-size:       var(--text-xs);
  font-weight:     700;
  letter-spacing:  0.1em;
  text-transform:  uppercase;
  text-decoration: none;
  border:          1px solid var(--rule);
  border-radius:   999px;
  cursor:          pointer;
  transition:      background var(--transition), border-color var(--transition);
}
.account-panel__link:hover,
.account-panel__link:focus-visible {
  background:   var(--bg);
  border-color: var(--ink);
}
.account-panel__link:active {
  background:   var(--rule);
  border-color: var(--ink);
  transition:   background 80ms ease, border-color 80ms ease;
}

/* Nav wrapper for one or more .account-panel__link buttons. Stacks
   vertically with the same gap as .account-panel__actions, so the
   nav links and the destructive log-out below the divider read as
   one continuous column of pills. */
.account-panel__links {
  display:        flex;
  flex-direction: column;
  gap:            var(--space-3);
  margin:         0;
  padding:        0;
}

/* The <hr> divider between nav links and destructive actions.
   Hubb / browser default is a chunky inset rule; we want a single
   hairline with a bit of vertical breathing room either side. */
.account-panel__divider {
  border:        0;
  border-top:    1px solid var(--rule);
  margin-block:  var(--space-6);
}

/* Touch feedback for account panel buttons: 80ms fast transition
   on :active so the press is felt immediately. */
.account-toggle:active {
  background:   var(--accent-dark);
  border-color: var(--accent-dark);
  color:        var(--accent-ink);
  transition:   background 80ms ease, border-color 80ms ease, color 80ms ease;
}
.account-panel__primary:active {
  background: var(--ink);
  transition: background 80ms ease;
}
.account-panel__secondary:active {
  background:   var(--rule);
  border-color: var(--ink);
  transition:   background 80ms ease, border-color 80ms ease;
}


/* =========================================================================
   3.23  EXPLORE CHURCH GALLERY  --  .explore-church + .explore_church
   -------------------------------------------------------------------------
   Featured-block grid linking visitors to the key areas of the
   church website (Small Groups, Get Involved, Youth, etc.). It's
   Hubb's Image Gallery module in unordered-list format, where each
   item is a clickable image that links somewhere via Hubb's gallery
   target settings. The image alt text is authored in the format
   "Title*Subtitle" -- JS splits on the asterisk and lays out
   separate caption elements over the image.

   Body Contents pattern:
     <section class="explore-church">
       <h2 class="explore-church__heading">Explore Our Church</h2>
       <module alt="Gallery" type_id="84" />   <- CSS Class: explore_church
     </section>

   Hubb-rendered structure inside the module wrapper:
     <div class="explore_church">
       <ul class="gallery_ul">
         <li class="gallery_li">
           <a href="..."><img alt="Title*Subtitle"></a>
         </li>
         ... 4 items typically
       </ul>
     </div>

   Visual treatment: 4-up grid of vertical tiles, full-bleed images
   with a dark gradient at the bottom for caption legibility, subtle
   image zoom on hover. Caption (title + subtitle) is built by JS
   from the image's alt text and absolutely positioned over the
   bottom of the tile.
   ========================================================================= */

.slice--explore-church,
.explore-church {
  /* Narrower than the full --container-width (1600px) -- the 2x2
     grid of large tiles felt too dominant on wider screens. 1200px
     keeps each tile around 580px wide which is generous but not
     overwhelming. The narrower frame also visually presents the
     section as a curated module rather than a full-bleed feature. */
  max-width:      1200px;
  margin-inline:  auto;
  padding-inline: var(--container-gutter);
  padding-block:  var(--space-12);
}

/* Heading is styled by the shared section heading rule (alongside
   .latest-news__heading et al), which gives it the typography +
   accent-coloured straight rule beneath. Add a small bottom
   margin override so the gallery sits closer to the heading
   than other sections -- these tiles are highly visual and don't
   need extra breathing room from the heading above. */
.explore-church__heading {
  margin-bottom: var(--space-6);
}

/* The Hubb module wrapper div. CSS Class "explore_church" on the
   Gallery module produces <div class="explore_church"> as the
   wrapper around <ul class="gallery_ul">. */
.explore_church {
  margin: 0;
}

.explore_church .gallery_ul {
  display:               grid;
  grid-template-columns: repeat(2, 1fr);
  gap:                   var(--space-8);
  list-style:            none;
  margin:                0;
  padding:               0;
}

/* Each tile -- wider 16:10 landscape ratio (was 4:5 portrait, but
   most source photos are shot landscape so the portrait crop was
   chopping off important content). With 2 tiles per row, each is
   ~750px wide on desktop, so 16:10 gives ~470px tall -- visible at
   a single glance, generous enough to show a full scene. Background
   ink colour shows through during image lazy-load and behind the
   gradient overlay. */
.explore_church .gallery_li {
  position:      relative;
  overflow:      hidden;
  border-radius: var(--radius);
  aspect-ratio:  16 / 10;
  background:    var(--ink);
  /* Subtle shadow so tiles read as elevated cards on the cream
     page background. */
  box-shadow:    0 4px 12px -4px rgba(26, 22, 18, 0.15);
  transition:    box-shadow var(--transition);
}
.explore_church .gallery_li:hover {
  box-shadow: 0 12px 32px -8px rgba(26, 22, 18, 0.3);
}

/* The clickable anchor fills the entire tile so the whole card
   is the hit target, not just the image. */
.explore_church .gallery_li > a {
  display:         block;
  position:        relative;
  width:           100%;
  height:          100%;
  text-decoration: none;
  color:           #FFFFFF;
}

.explore_church .gallery_li img {
  width:      100%;
  height:     100%;
  object-fit: cover;
  display:    block;
  /* Slow, generous easing for the hover zoom -- this is a deliberate
     "discover this area" interaction, not a quick utility action. */
  transition: transform 600ms cubic-bezier(0.16, 1, 0.3, 1);
}
.explore_church .gallery_li > a:hover img {
  transform: scale(1.06);
}

/* Gradient overlay on top of the image for caption legibility.
   Heavier opacity at the bottom (where the caption sits), fades to
   transparent above so the upper portion of the image stays clean. */
.explore_church .gallery_li > a::after {
  content:        "";
  position:       absolute;
  inset:          0;
  background:     linear-gradient(
                    to top,
                    rgba(26, 22, 18, 0.85) 0%,
                    rgba(26, 22, 18, 0.35) 35%,
                    rgba(26, 22, 18, 0)    65%
                  );
  pointer-events: none;
  z-index:        1;
}

/* Caption -- built by JS from the image's "Title*Subtitle" alt text.
   Absolutely positioned over the bottom of the tile, sits above the
   gradient overlay (z-index:2 vs gradient's z-index:1). Generous
   padding + larger title typography to suit the wider 2-up tiles. */
.explore-church__caption {
  position:       absolute;
  bottom:         0;
  left:           0;
  right:          0;
  padding:        var(--space-6);
  z-index:        2;
  display:        flex;
  flex-direction: column;
  gap:            var(--space-1);
}
.explore-church__title {
  font-family:    var(--font-display);
  font-size:      var(--text-xl);
  font-weight:    500;
  color:          #FFFFFF;
  letter-spacing: -0.01em;
  line-height:    1.2;
}
.explore-church__subtitle {
  font-family: var(--font-body);
  font-size:   var(--text-base);
  color:       rgba(255, 255, 255, 0.9);
  line-height: 1.4;
}

/* Focus state -- keyboard users get a clear focus ring on the tile.
   Use outline rather than border so it doesn't change layout. */
.explore_church .gallery_li > a:focus-visible {
  outline:        3px solid var(--accent);
  outline-offset: 3px;
}


/* =========================================================================
   3.23b  EXPLORE CHURCH GALLERY 2  --  .explore_church_2
   -------------------------------------------------------------------------
   Settings -> CSS Class = "explore_church_2"

   Editorial card variant of the Explore Our Church module. Where the
   original .explore_church uses a 2x2 grid of full-bleed image tiles
   with overlay text, this variant uses a 4-up horizontal row of
   editorial cards with image-on-top + text-below.

   Distinct from .explore_church in three ways:
     - Layout:    1x4 horizontal row (vs 2x2 grid)
     - Aspect:    strict 2 / 1 image ratio (vs 16 / 10 with crop)
     - Treatment: card style, text below image (vs overlay text on image)

   Reuses the same setupExploreGallery JS function (selector extended
   to include .explore_church_2) which builds the caption from the
   image's "Title*Subtitle" alt text. Reuses the .explore-church__
   caption / __title / __subtitle classes too -- the variant's CSS
   overrides the absolute positioning + white text from the original
   via parent-context selectors (.explore_church_2 .explore-church__
   caption etc.).

   Body Contents pattern -- same outer wrapper as the original, only
   the cssClass on the module differs:

     <section class="explore-church">
       <h2 class="explore-church__heading">Explore Our Church</h2>
       <module alt="Gallery" type_id="84" />   <- CSS Class: explore_church_2
     </section>
   ========================================================================= */

/* Wider container than the 1200px used by the original 2x2 -- a 4-up
   row needs more horizontal room. :has() detection means this only
   applies when a section actually contains the variant module; a
   page with both layouts on it gets the right width for each. */
.slice--explore-church:has(> .explore_church_2),
.explore-church:has(> .explore_church_2) {
  max-width: 1400px;
}

.explore_church_2 {
  margin: 0;
}

.explore_church_2 .gallery_ul {
  display:               grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap:                   var(--space-6);
  list-style:            none;
  margin:                0;
  padding:               0;
}

.explore_church_2 .gallery_li {
  position:      relative;
  background:    var(--surface);
  border-radius: var(--radius);
  overflow:      hidden;
  box-shadow:    0 2px 8px rgba(26, 22, 18, 0.08);
  transition:    box-shadow 300ms ease, transform 300ms ease;
}
.explore_church_2 .gallery_li:hover {
  box-shadow: 0 12px 28px -8px rgba(26, 22, 18, 0.20);
  transform:  translateY(-4px);
}

.explore_church_2 .gallery_li > a {
  display:         block;
  text-decoration: none;
  color:           inherit;
  position:        relative;
}

/* Stretched-link overlay -- the whole card (image + caption block)
   becomes the click target, not just the image. */
.explore_church_2 .gallery_li > a::before {
  content:  "";
  position: absolute;
  inset:    0;
  z-index:  1;
}

.explore_church_2 .gallery_li img {
  width:        100%;
  aspect-ratio: 2 / 1;     /* matches the source images (800x400) */
  object-fit:   cover;
  display:      block;
  /* Slow, generous easing -- "discover this area" interaction, not a
     quick utility action. Same easing curve as the original variant
     for visual family consistency. */
  transition:   transform 600ms cubic-bezier(0.16, 1, 0.3, 1);
}
.explore_church_2 .gallery_li > a:hover img {
  transform: scale(1.05);
}

/* Caption -- positioned beneath the image (override the absolute
   positioning that the original variant uses for overlay text).
   Generous padding so the text feels like a separate "label
   panel" beneath the image, not crammed against it. */
.explore_church_2 .explore-church__caption {
  position:       static;
  padding:        var(--space-5);
  display:        flex;
  flex-direction: column;
  gap:            var(--space-1);
}

.explore_church_2 .explore-church__title {
  font-family:    var(--font-display);
  font-size:      var(--text-lg);
  font-weight:    500;
  line-height:    1.2;
  letter-spacing: -0.01em;
  /* Override the white colour the original sets for overlay text --
     here the caption sits on a white card, so the title needs the
     standard dark ink. */
  color:          var(--ink);
  /* Animated underline that expands on card hover. The
     background-image / background-size approach (rather than a
     border-bottom) is necessary because the underline needs to
     transition smoothly from 0% width to 100% width -- borders can't
     animate width. align-self: start so the underline only spans the
     title's text width, not the full caption-block width. */
  background-image:    linear-gradient(var(--accent), var(--accent));
  background-size:     0% 2px;
  background-repeat:   no-repeat;
  background-position: 0 100%;
  transition:          background-size 400ms ease;
  align-self:          start;
}
.explore_church_2 .gallery_li:hover .explore-church__title {
  background-size: 100% 2px;
}

.explore_church_2 .explore-church__subtitle {
  font-family: var(--font-body);
  font-size:   var(--text-sm);
  /* Override the white-with-alpha colour from the original variant. */
  color:       var(--ink-soft);
  line-height: 1.4;
}

/* Focus state -- keyboard users get a clear focus ring on the card. */
.explore_church_2 .gallery_li > a:focus-visible {
  outline:        3px solid var(--accent);
  outline-offset: 3px;
}

/* =========================================================================
   3.23c  EXPLORE CHURCH GALLERY 3 -- Aspire-style overlay blocks
           --  .explore_church_3
   3.23d  EXPLORE CHURCH GALLERY 4 -- 3-up 2:1 grid (two rows of three)
           --  .explore_church_4
   -------------------------------------------------------------------------
   Two more Explore variants, both 3-up overlay galleries (same Gallery
   module, type_id 84, and the same "Title*Subtitle" alt-text caption the JS
   splits -- setupExploreGallery now includes both). They reuse the original
   .explore_church overlay treatment (full-bleed image, dark gradient, white
   caption overlaid at the bottom via the shared .explore-church__caption /
   __title / __subtitle elements):
     explore_church_3  -> three bold SQUARE blocks, larger title (Aspire look)
     explore_church_4  -> three columns of 2:1 (800x400) tiles -> two rows of 3

   Authoring: each gallery image's caption/alt is "Title*Subtitle"
   (e.g. "Children's Ministry*Age-related groups and activities").

   Body Contents pattern (wrap in the explore-church slice, as A/B do):
     <section class="slice slice--explore-church">
       <div class="slice__head slice__head--centred">
         <h2 class="slice__heading explore-church__heading">Explore Our Church</h2>
       </div>
       <module alt="Gallery" module_id="..." type_id="84" />   <- CSS Class: explore_church_3 (or _4)
     </section>
   ------------------------------------------------------------------------- */

/* Wider frame for a 3-up row (matches the explore_church_2 treatment). The
   :has() detection means it only applies to sections that actually contain
   the variant, so a page can mix layouts and each gets the right width. */
.slice--explore-church:has(> .explore_church_3),
.explore-church:has(> .explore_church_3),
.slice--explore-church:has(> .explore_church_4),
.explore-church:has(> .explore_church_4) {
  max-width: 1400px;
}

.explore_church_3,
.explore_church_4 { margin: 0; }

/* 3-up grid (shared) */
:is(.explore_church_3, .explore_church_4) .gallery_ul {
  display:               grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap:                   var(--space-6);
  list-style:            none;
  margin:                0;
  padding:               0;
}

/* Shared overlay tile -- mirrors the original .explore_church: full-bleed
   image, dark bottom gradient, white caption overlaid (the global
   .explore-church__caption / __title / __subtitle handle the text). */
:is(.explore_church_3, .explore_church_4) .gallery_li {
  position:      relative;
  overflow:      hidden;
  border-radius: var(--radius);
  background:    var(--ink);
  box-shadow:    0 4px 12px -4px rgba(26, 22, 18, 0.15);
  transition:    box-shadow var(--transition);
}
:is(.explore_church_3, .explore_church_4) .gallery_li:hover {
  box-shadow: 0 12px 32px -8px rgba(26, 22, 18, 0.3);
}
:is(.explore_church_3, .explore_church_4) .gallery_li > a {
  display:         block;
  position:        relative;
  width:           100%;
  height:          100%;
  text-decoration: none;
  color:           #FFFFFF;
}
:is(.explore_church_3, .explore_church_4) .gallery_li img {
  width:      100%;
  height:     100%;
  object-fit: cover;
  display:    block;
  transition: transform 600ms cubic-bezier(0.16, 1, 0.3, 1);
}
:is(.explore_church_3, .explore_church_4) .gallery_li > a:hover img {
  transform: scale(1.06);
}
:is(.explore_church_3, .explore_church_4) .gallery_li > a::after {
  content:        "";
  position:       absolute;
  inset:          0;
  background:     linear-gradient(
                    to top,
                    rgba(26, 22, 18, 0.85) 0%,
                    rgba(26, 22, 18, 0.35) 35%,
                    rgba(26, 22, 18, 0)    65%
                  );
  pointer-events: none;
  z-index:        1;
}
:is(.explore_church_3, .explore_church_4) .gallery_li > a:focus-visible {
  outline:        3px solid var(--accent);
  outline-offset: 3px;
}

/* Variant 3 -- bold square blocks, larger title. */
.explore_church_3 .gallery_li { aspect-ratio: 1 / 1; }
.explore_church_3 .explore-church__title { font-size: var(--text-2xl); }

/* Variant 4 -- 2:1 tiles (800x400); six images make two rows of three. */
.explore_church_4 .gallery_li { aspect-ratio: 2 / 1; }



/* =========================================================================
   3.24  MODULE: Content Article  --  .content_article  (Article module)
   -------------------------------------------------------------------------
   Settings -> CSS Class = "content_article"

   The "default" article-page treatment: long-form mixed content
   (headings, paragraphs, images, inline links, embedded galleries,
   tables, etc.) that ships when an author drops a single Article
   module on an internal page like About Us, Safeguarding,
   Beliefs, etc.

   Body Contents pattern -- the article is the entire page body
   between the header and the footer-safeguarding section. Wrap in
   a <main> for a11y so the skip-to-content link has a target:

     <main>
       <module alt="Article" module_id="..." type_id="10"
               cssClass="content_article" />
     </main>

   Width -- constrained to ~800px for readable line lengths (~70
   characters at base font size). Wider would push reading speed
   down; narrower would feel cramped. Centred via auto margins.

   The Hubb article editor produces messy HTML -- inline widths,
   layout tables, stray <br>s, &nbsp; runs, h-tags wrapping images.
   We don't try to fight all of it; we just style the common
   patterns sensibly and reset the most disruptive author-set
   inline styles.

   Rendered structure (typical):
     div.content_article
       div.ArticleBody
         h1 (page title)
         h2/h3/h4 (subsections, occasionally containing images)
         p (prose)
         a (inline links)
         a.ArrowToRight (CTA-styled "next page" link)
         img (full-width or inline)
         table (often used for layout)
         div.bx-wrapper (embedded gallery from a Gallery module
                         dropped inside the article)
       div.ArticleRotaSignupLink (Hubb cruft -- empty unless rotas active)
   ========================================================================= */

.content_article {
  max-width:      820px;        /* ~70ch at base size; readable
                                   line-length sweet spot */
  margin:         var(--space-16) auto;
  padding-inline: var(--container-gutter);
}

.content_article .ArticleBody {
  font-family: var(--font-body);
  font-size:   var(--text-base);
  line-height: 1.65;            /* slightly looser than 1.5 because
                                   prose reads better with more
                                   vertical breathing room */
  color:       var(--ink);
}

/* ---- Headings ---- */

.content_article .ArticleBody h1 {
  font-family:    var(--font-display);
  font-size:      var(--text-3xl);
  font-weight:    var(--display-weight, 400);
  line-height:    1.05;
  letter-spacing: -0.01em;
  color:          var(--ink);
  margin:         0 0 var(--space-6) 0;
  padding-bottom: var(--space-4);
  position:       relative;
}
/* Brand-accent rule beneath the page title -- matches the
   section-heading rule used elsewhere on the site so internal
   pages feel of a piece with the homepage. */
.content_article .ArticleBody h1::after {
  content:        "";
  position:       absolute;
  left:           0;
  bottom:         0;
  width:          64px;
  height:         4px;
  background:     var(--accent);
  border-radius:  2px;
}

.content_article .ArticleBody h2,
.content_article .ArticleBody h3,
.content_article .ArticleBody h4,
.content_article .ArticleBody h5,
.content_article .ArticleBody h6 {
  font-family:    var(--font-display);
  font-weight:    var(--display-weight, 400);
  color:          var(--ink);
  line-height:    1.2;
  letter-spacing: -0.01em;
  /* Generous top margin so subsections breathe; tighter bottom
     margin so the heading sits close to its first paragraph. */
  margin:         var(--space-10) 0 var(--space-3) 0;
}
.content_article .ArticleBody h2 { font-size: var(--text-2xl); }
.content_article .ArticleBody h3 { font-size: var(--text-xl);  }
.content_article .ArticleBody h4 { font-size: var(--text-lg);  }
.content_article .ArticleBody h5,
.content_article .ArticleBody h6 { font-size: var(--text-base); }

/* When an editor wraps an image inside an h-tag (it happens --
   the WYSIWYG produces this), strip the heading's text margins
   so the image doesn't sit in awkward whitespace. The image
   styling below takes over for the visual treatment. */
.content_article .ArticleBody h1:has(> img:only-child),
.content_article .ArticleBody h2:has(> img:only-child),
.content_article .ArticleBody h3:has(> img:only-child),
.content_article .ArticleBody h4:has(> img:only-child) {
  margin: var(--space-6) 0;
}

/* In-page anchor scroll-margin so when a user clicks an in-page
   #section link, the heading lands below any sticky elements
   (we don't have a sticky header right now, but the margin
   gives breathing room above the target heading). */
.content_article .ArticleBody :target {
  scroll-margin-top: var(--space-16);
}

/* ---- Paragraphs and inline content ---- */

.content_article .ArticleBody p {
  margin: 0 0 var(--space-4) 0;
}

.content_article .ArticleBody strong,
.content_article .ArticleBody b {
  font-weight: 700;
  color:       var(--ink);
}
.content_article .ArticleBody em,
.content_article .ArticleBody i {
  font-style: italic;
}

/* ---- Inline links ---- */

.content_article .ArticleBody a {
  color:                 var(--accent);
  text-decoration:       underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  font-weight:           500;
  transition:            color var(--transition),
                         text-decoration-thickness var(--transition);
}
.content_article .ArticleBody a:hover {
  color:                     var(--accent-dark);
  text-decoration-thickness: 2px;
}
.content_article .ArticleBody a:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 3px;
}

/* The "ArrowToRight" link class -- a CTA-styled "next step" link
   the Hubb editor inserts via its CTA dropdown. Style as a
   filled button matching the homepage's primary CTAs. The
   actual arrow glyph is added via ::after so the rendered HTML
   stays clean. */
.content_article .ArticleBody a.ArrowToRight {
  display:          inline-flex;
  align-items:      center;
  gap:              var(--space-2);
  padding:          var(--space-3) var(--space-5);
  background:       var(--accent);
  color:            var(--accent-ink);
  border-radius:    var(--radius-lg);
  text-decoration:  none;
  font-weight:      600;
  margin-top:       var(--space-6);
  box-shadow:       0 4px 12px -2px color-mix(in srgb, var(--accent) 40%, transparent);
  transition:       background var(--transition),
                    transform  var(--transition),
                    box-shadow var(--transition);
}
.content_article .ArticleBody a.ArrowToRight::after {
  content:     "\2192";  /* right arrow, escaped for pure-ASCII file (Anchises lesson) */
  font-family: var(--font-body);   /* keep arrow consistent
                                      regardless of inherited font */
  transition:  transform var(--transition);
}
.content_article .ArticleBody a.ArrowToRight:hover {
  background:  var(--accent-dark);
  color:       var(--accent-ink);
  text-decoration: none;
  transform:   translateY(-2px);
  box-shadow:  0 8px 20px -4px color-mix(in srgb, var(--accent) 50%, transparent);
}
.content_article .ArticleBody a.ArrowToRight:hover::after {
  transform: translateX(3px);
}

/* ---- Images ---- */

.content_article .ArticleBody img {
  display:        block;
  width:          100%;
  height:         auto;
  border-radius:  var(--radius-lg);
  margin:         var(--space-6) 0;
}
/* Author-set inline `style="max-width: 100%"` from the WYSIWYG
   doesn't conflict with our base `width: 100%` -- they cooperate.
   And the `width="..."` attribute is just a hint that loses to
   any explicit CSS width. So we DON'T need to override author
   widths globally -- the base rule above takes care of normal
   prose images. Table images opt out via the `.table img` rule
   below. Heading images stay constrained via the heading-img
   rule below. Signature images opt out via the `[alt^=...]`
   rule below.

   Earlier versions of this CSS had a broad `img[width]` rule
   that forced everything to 100%, but it was blowing 75px
   pastor-portrait thumbnails up to full column width because
   the WYSIWYG always emits width attributes. Removed; specific
   opt-outs handle the cases that matter. */
/* Image inside a heading or link -- no extra vertical margin,
   the parent's spacing handles it. */
.content_article .ArticleBody h1 img,
.content_article .ArticleBody h2 img,
.content_article .ArticleBody h3 img,
.content_article .ArticleBody h4 img,
.content_article .ArticleBody a > img {
  margin-block: 0;
}

/* The signature image at the foot of pastor-authored pages.
   Hubb's WYSIWYG names it `signature{hash}.png`, so any image
   with an alt starting with "signature" is the pastor signature.
   Constrain to a sensible signature width -- otherwise it inherits
   the base rule's `width: 100%` and stretches to the full 820px
   article column.

   IMPORTANT: this needs to win against the base `img { width: 100% }`
   rule, so width is !important. max-width: 280px caps the visible
   size. */
.content_article .ArticleBody img[alt^="signature"] {
  width:      auto !important;
  max-width:  280px;
  margin:     var(--space-6) 0 var(--space-2) 0;
  border-radius: 0;            /* signatures look weird with rounded corners */
}

/* ---- Lists ---- */

.content_article .ArticleBody ul,
.content_article .ArticleBody ol {
  margin:        0 0 var(--space-4) 0;
  padding-left:  var(--space-6);
}
.content_article .ArticleBody li {
  margin-bottom: var(--space-2);
}
.content_article .ArticleBody li::marker {
  color: var(--accent);
}

/* ---- Blockquote ---- */

.content_article .ArticleBody blockquote {
  margin:         var(--space-6) 0;
  padding:        var(--space-2) var(--space-6);
  border-left:    4px solid var(--accent);
  font-style:     italic;
  color:          var(--ink);
  font-size:      var(--text-lg);
  line-height:    1.5;
}

/* ---- Horizontal rules ---- */

.content_article .ArticleBody hr {
  border:        0;
  border-top:    1px solid var(--rule);
  margin:        var(--space-10) 0;
}

/* ---- Tables ----
   Authors use tables for two purposes: (a) actual data tables, and
   (b) layout tables (e.g. "two pastor portraits side-by-side").
   We can't tell which is which from CSS alone, so we apply a
   sensible default that works reasonably for both:
     - Strip author-set inline `width="..."` and `align="left"`
       (they wreck mobile and don't respect the article's reading
       width)
     - Sensible cell padding
     - Borders on real-looking tables (those with <th> rows)
     - Stack to single column on phones */
.content_article .ArticleBody table {
  width:           auto !important;       /* override editor's `style="width:175px"` */
  max-width:       100%;
  margin:          var(--space-6) 0;
  border-collapse: collapse;
  float:           none !important;       /* override editor's `align="left"` */
}
.content_article .ArticleBody td,
.content_article .ArticleBody th {
  padding:        var(--space-3) var(--space-4);
  text-align:     left;
  vertical-align: top;
}
.content_article .ArticleBody th {
  background:  var(--accent-soft);
  font-weight: 700;
}
/* For real data tables (with <th>), draw borders. For layout
   tables (no <th>), no borders -- they're just visual scaffolding. */
.content_article .ArticleBody table:has(th) td,
.content_article .ArticleBody table:has(th) th {
  border-bottom: 1px solid var(--rule);
}
/* Reset the inline width on images-inside-tables so the layout-
   table use case (small portrait thumbnails sitting side-by-side)
   keeps the author's intended size rather than blowing them out
   to full article width. Author typically sets the thumbnail
   width="75" attribute -- we honour that here. */
.content_article .ArticleBody table img {
  width:      auto !important;
  max-width:  100%;
  margin:     0;
  border-radius: var(--radius);
}

/* ---- Embedded BxSlider gallery ----
   When an author drops a Gallery module inside an article body,
   Hubb renders it with the BxSlider plugin -- a div.bx-wrapper
   containing div.bx-viewport > ul.gallery_ul > li.gallery_li.
   Style the whole thing to fit cleanly inside the article. */
.content_article .ArticleBody .bx-wrapper {
  max-width:    100% !important;
  margin:       var(--space-8) 0;
  position:     relative;
  border-radius: var(--radius-lg);
  overflow:     hidden;            /* clip the bx-clone slides that
                                       sit beyond the viewport */
}
.content_article .ArticleBody .bx-viewport {
  border-radius: var(--radius-lg);
}
.content_article .ArticleBody .bx-wrapper img {
  border-radius: 0;
  margin:        0;
  width:         100%;
}
/* Pager dots -- restyle to match brand */
.content_article .ArticleBody .bx-pager {
  position:    absolute;
  bottom:      var(--space-4);
  left:        0;
  right:       0;
  text-align:  center;
  z-index:     2;
}
.content_article .ArticleBody .bx-pager-item {
  display:     inline-block;
  margin:      0 var(--space-1);
}
.content_article .ArticleBody .bx-pager-link {
  display:        inline-block;
  width:          10px;
  height:         10px;
  border-radius:  50%;
  background:     rgba(255, 255, 255, 0.6);
  text-indent:    -9999px;             /* hide the "1", "2", etc. */
  overflow:       hidden;
  transition:     background var(--transition);
}
.content_article .ArticleBody .bx-pager-link.active,
.content_article .ArticleBody .bx-pager-link:hover {
  background: var(--accent);
}
/* Prev/next arrows -- hide them; pager dots + swipe handle nav */
.content_article .ArticleBody .bx-controls-direction {
  display: none;
}

/* ---- Hubb article cruft ---- */

.content_article .ArticleRotaSignupLink:empty {
  display: none;
}
.content_article .ArticleRotaSignupLink {
  margin-top: var(--space-8);
}

/* Stray `&nbsp;` runs the editor sometimes emits as block-level
   whitespace -- we can't target the entity directly, but if the
   pattern shows up wrapped in something we can target, it'd live
   here. Currently a no-op placeholder for future defensive rules. */



/* =========================================================================
   3.25  SHOP HOMEPAGE  --  sidebar layout  (.shop-layout family)
   -------------------------------------------------------------------------
   v0.163: rebuilt from the pill-row landing layout to the classic
   shop pattern (reference: baptist.org.uk/shop): full-width search/
   basket strip on top, then a two-column grid -- category sidebar
   left, content (featured tiles, product cards, info article) right.
   The sidebar persists naturally onto category pages later.

   CRITICAL TARGETING LESSON (v0.163): Town's shop modules render
   BARE, CLASSLESS div wrappers. The .ShopSearch / .ShopBasket /
   .ShopNavCatMenu classes seen on the Baptist reference install are
   per-install configuration, NOT built-in module output. Every rule
   below therefore scopes through wrapper classes WE author in the
   Body Contents (.shop-utility__search, .shop-cat-nav, etc.) and
   targets module internals structurally. Never rely on Hubb shop
   module classes existing.

   Body Contents pattern (see SHOP-BODY-CONTENTS-townchurch-v3.html):

     section.slice.slice--shop-utility
       div.shop-utility
         div.shop-utility__search   [shop search module]
         div.shop-utility__basket   [shopping trolley module]

     div.slice.shop-layout
       aside.shop-sidebar
         h2.shop-sidebar__title  "Shop Categories"
         nav.shop-cat-nav        [category navigation module]
       div.shop-content
         section + h2.shop-section__heading + [gallery module]
         section + h2.shop-section__heading + div.shop-hotpicks
                                             [hotpicks module]
         section.slice--shop-info            [article module]

   Hubb module output this is written against (Town install, real
   rendered markup 12 Jun 2026):
     Shop search  -> bare div with TWO tables; rows: "Find:" text
                     input (tr[mode="simple"], inline display set by
                     Hubb's inline script), "In:" category select
                     (tr, no mode attr), "Search sub-categories"
                     checkbox in a NESTED table (tr, no mode attr),
                     and a Go submit (input.sitebutton, inline
                     width:40px) in the second table.
     Trolley      -> bare div with text ("Your basket is empty") or
                     links when items are present.
     Category nav -> bare div with ul#ShopCatNavRoot; flat list on
                     Town today, but the Baptist install shows Hubb
                     CAN nest subcategory uls -- nested styling is
                     included below for when categories deepen.
   ========================================================================= */

/* ---- Utility strip: search | basket ------------------------------------
   v0.166: full-bleed tinted band (was a white strip between two
   hairlines, which left a lot of dead white space under the header
   -- Adam, with the reference site's solid-colour bar alongside).
   Light tint chosen over dark: the page is a light editorial canvas
   where dark is reserved for the footer; and because the band reads
   from --accent-soft, branded themes retint it automatically (the
   reference site's bar is exactly this band in their brand blue).
   The slice opts out of the container to paint edge-to-edge and
   zeroes its top margin to sit flush under the header; the inner
   .shop-utility row reattaches to container width. */
.slice--shop-utility {
  max-width:      none;
  padding-inline: 0;
  margin-block:   0;
  background:     var(--accent-soft);
}
.shop-utility {
  display:        flex;
  align-items:    center;
  gap:            var(--space-6);
  max-width:      var(--container-width);
  margin-inline:  auto;
  padding-inline: var(--container-gutter);
  padding-block:  var(--space-4);
}

.shop-utility__crumbs { flex-shrink: 0; }
.shop-utility__crumbs:empty { display: none; }
/* Breadcrumb trail: the Breadcrumbs module renders a bare div containing
   a.BreadcrumbLink elements with " > " text between. Quiet meta styling. */
.shop-utility__crumbs { font-size: var(--text-sm); color: var(--ink-soft); }
.shop-utility__crumbs a { color: var(--ink-soft); text-decoration: none; }
.shop-utility__crumbs a:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
/* ---- Breadcrumb row BELOW the utility band (reference: Baptist shop,
   "Shop location: ..." under the search bar). The Breadcrumbs module moves
   out of the utility strip into its own .slice--shop-crumbs section in Body
   Contents; this styles it as a quiet container-width row sitting snug under
   the band. Links pick up the accent on hover. */
/* Tight breadcrumb spacing: the crumb row's own margins are small, and we
   collapse the big default margins on its neighbours specifically when a
   crumb row sits between them -- the utility band's space-12 bottom and the
   shop-layout's inherited .slice space-16 top -- so the crumb hugs both at a
   tidy ~16px. Sibling :has()/+ on our own authored sections (no Hubb wrapping
   between top-level slices). Homepage has no crumb row, so it's untouched. */
/* Fixed crumb zone so the shop layout starts at the SAME Y on every shop
   page (the homepage shows no breadcrumb, category pages do) -- the sidebar
   never jumps. The band sits flush; .shop-layout reserves the zone via its
   top margin (used on the no-crumb homepage); when a crumb row IS present it
   fills the zone (min-height + vertical centring) and the layout drops the
   reserved margin, so the net layout position is identical either way. */
:root { --shop-crumb-zone: 3rem; }
.slice--shop-crumbs {
  margin-block: 0;
  min-height:   var(--shop-crumb-zone);
  display:      flex;
  align-items:  center;
}
.slice--shop-crumbs + .shop-layout { margin-top: 0; }
/* .slice--shop-crumbs already provides the .slice container (max-width +
   gutter); .shop-crumbs must NOT re-apply it or the text indents a second
   gutter past the sidebar/cards. Type styling only. */
.shop-crumbs { font-size: var(--text-sm); color: var(--ink-soft); }
/* Clickable ancestors read darker + medium weight; the current page (a plain
   text node, no <a>) stays muted -- so clickable vs current is obvious. */
.shop-crumbs a { color: var(--ink); font-weight: 500; text-decoration: none; }
.shop-crumbs a:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.shop-crumbs:empty, .slice--shop-crumbs:has(> .shop-crumbs:empty) { display: none; }

/* ---- Shop Terms & Conditions --------------------------------------------
   The Terms module renders a single link (opens the T&Cs modal via Hubb's
   OpenShopTermsAndConds()) inside a full-width table. Flatten the table and
   present it as a quiet, centred page-foot link with a hairline above, sat
   below the listing rather than in the sidebar (only one link -- a sidebar
   block would look thin). */
.slice--shop-terms { display: none; }   /* T&Cs moved into the Shop menu (/Shop/TermsAndConditions.aspx); hidden, NOT removed -> keeps the module's stored config */
/* No T&Cs band on checkout now, so tuck the footer up to the section. */
body:has(.slice--shop-checkout) .site-footer { margin-top: var(--space-8); }
.shop-terms table,
.shop-terms tbody,
.shop-terms tr,
.shop-terms td { display: block; width: auto; }
.shop-terms {
  max-width:      var(--container-width);
  margin-inline:  auto;
  padding-inline: var(--container-gutter);
  padding-block:  var(--space-6);   /* symmetric -> centres the T&Cs link between the hairlines */
  border-top:     1px solid var(--rule);
  text-align:     center;
}
.shop-terms small { font-size: var(--text-sm); }
.shop-terms a {
  color:                 var(--ink-soft);
  text-decoration:       underline;
  text-underline-offset: 3px;
  transition:            color var(--transition);
}
.shop-terms a:hover { color: var(--accent); }

/* ---- Shop Terms & Conditions standalone page ----------------------------
   /Shop/TermsAndConditions.aspx -- a Hubb system page that already loads our
   CSS (headings render in Fraunces) but ships raw layout (a default blockquote,
   system layout tables, in-content Print/Close buttons). Now linked from the
   Shop menu (the inline Terms module above is hidden). All rules scoped to the
   page's body class `_shop_termsandconditions_aspx` so they cannot leak; the
   same class is on the T&Cs dialog iframe body, so this also tidies the pop-up.
   `height: auto !important` beats the layout table's inline height:100%. */
._shop_termsandconditions_aspx .pagebox,
._shop_termsandconditions_aspx .pagebox > tbody,
._shop_termsandconditions_aspx .pagebox > tbody > tr,
._shop_termsandconditions_aspx .pagebox td.body {
  display: block;
  width:   auto;
  height:  auto !important;
}
._shop_termsandconditions_aspx blockquote {
  max-width:      62rem;
  margin:         var(--space-12) auto var(--space-16);
  padding-inline: var(--container-gutter);
}
._shop_termsandconditions_aspx blockquote > h1 { margin: 0 0 var(--space-4); }
._shop_termsandconditions_aspx blockquote h3 {
  margin:         var(--space-8) 0 var(--space-3);
  padding-bottom: var(--space-2);
  border-bottom:  1px solid var(--rule);
}
._shop_termsandconditions_aspx blockquote p { margin: 0; }   /* the &nbsp; spacer paragraphs */
._shop_termsandconditions_aspx blockquote table {
  margin:          var(--space-3) 0 0;
  border-collapse: collapse;
}
._shop_termsandconditions_aspx blockquote table td { padding: var(--space-1) var(--space-3); vertical-align: top; }
/* In-content Print/Close are redundant (browser print/back on the page; the
   dialog has its own titlebar close) -- hide them and the divider above. */
._shop_termsandconditions_aspx hr,
._shop_termsandconditions_aspx .SiteButton { display: none; }

/* =========================================================================
   3.26  SHOP ITEM (product detail page)   .shop-item family
   -------------------------------------------------------------------------
   Rendered modules (Town install; all scoped through OUR wrapper classes,
   never Hubb's per-install output classes):
     .shop-item__gallery  Item Thumbnail -> table with an 80px thumbnail <a>
                          whose zoom link carries the FULL image URL;
                          setupShopItemDetail() swaps the <img> to the full
                          image and these rules frame it as the product shot.
     .shop-item__title    Item Title    -> h1 > span
     .shop-item__price    Price         -> table: "<n> in stock" + a
                          .detailPrice span ("Price now: ..").
     .shop-item__actions  Buy Buttons   -> table: Qty input + submit .button
                          + wishlist link; tiny inline styles overridden.
     .shop-item__detail   Description / Item Data (author, type) / Contents.
   Layout: image | buy box on top, full-width detail beneath; single column on
   mobile. Sits in the content column beside the persistent category sidebar,
   so the shop chrome stays consistent with the category page. */
.shop-item {
  display:               grid;
  grid-template-columns: minmax(0, 280px) minmax(0, 1fr);
  grid-template-areas:   "gallery buy"
                         "detail  detail";
  gap:                   var(--space-8);
  align-items:           start;
}
.shop-item__gallery { grid-area: gallery; }
.shop-item__buy     { grid-area: buy; }
.shop-item__detail  { grid-area: detail; }

/* Gallery: flatten the thumbnail table; frame the (JS-upsized) product image
   on the soft tint, click-to-zoom preserved. */
.shop-item__gallery table, .shop-item__gallery tbody,
.shop-item__gallery tr,    .shop-item__gallery td { display: block; width: auto; }
.shop-item__gallery a {
  display:       block;
  background:    var(--accent-soft);
  border-radius: var(--radius);
  padding:       var(--space-5);
  cursor:        zoom-in;
}
.shop-item__gallery a > img {
  display:    block;
  width:      100%;
  height:     auto;
  max-height: 380px;
  object-fit: contain;
}
/* The module appends a tiny "zoom" caption gif under the thumb -- drop it. */
.shop-item__gallery a > div { display: none; }

/* Title */
.shop-item__title h1 {
  font-family:    var(--font-display);
  font-weight:    400;
  font-size:      var(--text-xl);
  line-height:    1.15;
  letter-spacing: -0.01em;
  color:          var(--ink);
  margin:         0;
}

/* Price: flatten table; stock line muted, price prominent. .detailPrice is a
   Hubb shop class (same lean as .Price/.listingPrice in the card builder). */
.shop-item__price { margin-top: var(--space-4); }
.shop-item__price table, .shop-item__price tbody,
.shop-item__price tr,    .shop-item__price td { display: block; width: auto; }
.shop-item__price td > div {
  font-size:     var(--text-sm);
  color:         var(--ink-soft);
  margin-bottom: var(--space-1);
}
.shop-item__price .detailPrice {
  font-family: var(--font-display);
  font-size:   var(--text-lg);
  font-weight: 500;
  color:       var(--ink);
}
.shop-item__price .detailPrice b { font-weight: 500; }
.shop-item__price nobr { white-space: normal; }

/* Actions: flatten table into a flex row. The module sets tiny inline styles
   on the Qty input and button (width:25px, font-size:9px, padding:3px) -- we
   beat the inline attribute with !important. */
.shop-item__actions {
  margin-top:  var(--space-5);
  padding-top: var(--space-5);
  border-top:  1px solid var(--rule);
}
.shop-item__actions table, .shop-item__actions tbody,
.shop-item__actions tr,    .shop-item__actions td { display: block; width: auto; }
/* Normal flow: "Qty: [n] (at GBP [price]) [Add to basket]" inline on one row;
   the module's <br> drops the wishlist link to its own line, left-aligned
   under the row. (Flex was right-aligning the wishlist link before.) */
.shop-item__actions td { line-height: 2; }
.shop-item__actions input[type="text"] {
  display:        inline-block;
  vertical-align: middle;
  width:          4.25rem !important;
  height:         auto !important;
  margin:         0 var(--space-2);
  font-size:      var(--text-sm) !important;
  padding:        var(--space-2) var(--space-3) !important;
  border:         1px solid var(--rule);
  border-radius:  var(--radius);
  font-family:    var(--font-body);
  color:          var(--ink);
}
.shop-item__actions input[type="submit"] {
  display:        inline-block;
  vertical-align: middle;
  margin-left:    var(--space-2);
  font-family:    var(--font-body);
  font-size:      var(--text-sm) !important;
  font-weight:    500 !important;
  letter-spacing: 0.02em;
  color:          var(--accent-ink);
  background:     var(--accent);
  border:         1px solid var(--accent);
  border-radius:  999px;
  padding:        var(--space-2) var(--space-6) !important;
  cursor:         pointer;
  transition:     opacity var(--transition);
}
.shop-item__actions input[type="submit"]:hover { opacity: 0.85; }
.shop-item__actions a {
  display:               inline-block;
  margin-top:            var(--space-4);
  font-family:           var(--font-body);
  font-size:             var(--text-sm);
  font-weight:           400 !important;
  color:                 var(--ink-soft);
  text-decoration:       underline;
  text-underline-offset: 3px;
  padding:               0 !important;
}
.shop-item__actions a:hover { color: var(--accent); }

/* Detail block (full width below): description prose + author/type meta. */
.shop-item__detail {
  border-top:  1px solid var(--rule);
  padding-top: var(--space-8);
}
.shop-item__description { color: var(--ink); line-height: 1.7; max-width: 68ch; }
.shop-item__description p { margin: 0 0 var(--space-4); }
.shop-item__description p:last-child { margin-bottom: 0; }
.shop-item__data {
  margin-top: var(--space-5);
  font-size:  var(--text-sm);
  color:      var(--ink-soft);
}
.shop-item__data b { color: var(--ink); font-weight: 600; }

/* Cross-sell + reviews: setupShopItemDetail() card-ifies Related Items and
   Also Bought into .shop-cards grids and hides any section with no items (so
   a stray heading never shows). These rules just space the labelled blocks. */
.shop-item__section { margin-top: var(--space-12); }
.shop-item__section .shop-section__heading { margin-bottom: var(--space-6); }
/* Cross-sell cards sit in a narrower column; keep the buy label on one line
   (the uppercase + wide letter-spacing was wrapping "Add to basket"). */
.shop-item__section .shop-card__buy {
  letter-spacing: 0.04em;
  padding:        var(--space-2) var(--space-4);
  white-space:    nowrap;
}
.shop-item__section .shop-card__footer { flex-wrap: wrap; }

/* ---- Toast (add-to-basket / wishlist confirmation) ----------------------
   Add-to-basket and wishlist trigger an ASP.NET postback (full reload), so
   setupShopActionToasts() flags the intent on click and shows the toast after
   the reload -- fixed at the bottom so it's visible wherever the page lands. */
:root { --toast-success: #1F7A44; --danger: #C0392B; }
.tab-toast {
  position:       fixed;
  left:           50%;
  bottom:         var(--space-8);
  transform:      translateX(-50%) translateY(1rem);
  z-index:        2000;
  max-width:      calc(100vw - var(--space-8));
  padding:        var(--space-3) var(--space-6);
  border-radius:  999px;
  background:     var(--toast-success);
  color:          #FFFFFF;
  font-family:    var(--font-body);
  font-size:      var(--text-sm);
  font-weight:    500;
  box-shadow:     0 8px 24px rgba(0, 0, 0, 0.18);
  opacity:        0;
  transition:     opacity var(--transition), transform var(--transition);
  pointer-events: none;
}
.tab-toast.is-visible { opacity: 1; transform: translateX(-50%) translateY(0); }
@media (prefers-reduced-motion: reduce) {
  .tab-toast { transition: opacity var(--transition); transform: translateX(-50%); }
  .tab-toast.is-visible { transform: translateX(-50%); }
}
/* Inline confirmation (item page): green text with a CSS tick, sat next to the
   Add-to-basket button / wishlist link. Card adds still use the fixed toast
   above (no stable inline anchor after their postback). */
.shop-added {
  display:        inline-block;
  margin-left:    var(--space-3);
  vertical-align: middle;
  color:          var(--toast-success);
  font-family:    var(--font-body);
  font-size:      var(--text-sm);
  font-weight:    600;
  opacity:        0;
  transition:     opacity var(--transition);
}
.shop-added.is-visible { opacity: 1; }
.shop-added::before {
  content:      "";
  display:      inline-block;
  width:        0.4em;
  height:       0.7em;
  margin-right: 0.45em;
  border:       solid var(--toast-success);
  border-width: 0 2px 2px 0;
  transform:    rotate(45deg) translateY(-0.1em);
}

/* ---- Hubb image lightbox (third-party lightbox.js) close button ----------
   The zoom popup (#lightbox, injected at click time) ships a red close GIF.
   Hide it and draw a clean circular Tabernacle close in the top-right. Markup
   is fixed ids; #lightbox is position:absolute so an absolute child anchors to
   it. !important beats the third-party stylesheet on the hidden GIF. */
#bottomNavClose img { display: none !important; }
#bottomNavClose {
  position:      absolute;
  top:           var(--space-3);
  right:         var(--space-3);
  width:         var(--space-8);
  height:        var(--space-8);
  border-radius: 999px;
  background:    var(--surface);
  border:        1px solid var(--rule);
  box-shadow:    0 2px 10px rgba(0, 0, 0, 0.15);
  z-index:       10;
}
#bottomNavClose::before,
#bottomNavClose::after {
  content:       "";
  position:      absolute;
  top:           50%;
  left:          50%;
  width:         14px;
  height:        2px;
  background:    var(--ink);
  border-radius: 1px;
}
#bottomNavClose::before { transform: translate(-50%, -50%) rotate(45deg); }
#bottomNavClose::after  { transform: translate(-50%, -50%) rotate(-45deg); }
#bottomNavClose:hover { border-color: var(--ink); background: var(--accent-soft); }

/* =========================================================================
   3.27  SHOP MY ORDERS   .shop-orders / .shop-draft-orders
   -------------------------------------------------------------------------
   My Orders renders <h2>My Orders</h2> + a <table> (first <tr> = sortable
   header, rest = order rows) + a "Shop home" button. My Draft Orders renders
   the same shape, or empty -> hidden by setupShopItemDetail(). Scoped through
   OUR wrappers; rows targeted structurally (tr:first-child = header) rather
   than via Hubb's grid classes. */
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) { overflow-x: auto; }
/* Some grids ship border="1"/rules="rows"; neutralise those attribute
   borders so only our hairlines show. */
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) table.gridstyle,
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) table.gridstyle td { border: 0; }
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) h2 {
  font-family:    var(--font-display);
  font-weight:    400;
  font-size:      var(--text-xl);
  letter-spacing: -0.01em;
  color:          var(--ink);
  margin:         0 0 var(--space-6);
}
/* v0.230  Scoped down -- originally this also covered .shop-checkout, but
   the Delivery step uses real <br>s for address line breaks AND the
   totals stack (Delivery / Basket / Grand). The voucher-prompt br that
   was the original reason for this rule is still hidden by the targeted
   .shop-checkout #divVoucherCode rule below. The other three page types
   keep the broad behaviour until we see them needing brs. */
:is(.shop-orders, .shop-draft-orders, .shop-wishlist) br { display: none; }
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) table.gridstyle {
  width:           100%;
  min-width:       460px;        /* keep columns legible; wrapper scrolls if narrower */
  border-collapse: collapse;
  font-family:     var(--font-body);
  font-size:       var(--text-sm);
  margin-bottom:   var(--space-6);
}
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) table.gridstyle td {
  text-align:     left;          /* normalise the module's align="center" cells */
  vertical-align: middle;
}
/* Header row */
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) table.gridstyle tr:first-child td {
  padding:        var(--space-2) var(--space-3);
  border-bottom:  1px solid var(--rule);
  font-size:      var(--text-xs);
  font-weight:    700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color:          var(--ink);
  white-space:    nowrap;
}
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) table.gridstyle tr:first-child a {
  display:         inline-flex;   /* arrow inline beside the label, not stacked above */
  flex-direction:  row-reverse;   /* arrow AFTER the label so the label stays flush left under the title */
  align-items:     center;
  gap:             var(--space-3);
  color:           var(--ink);
  text-decoration: none;
}
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) table.gridstyle tr:first-child a:hover { color: var(--accent); }
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) table.gridstyle tr:first-child img {
  margin:  0 !important;   /* drop the module's inline margin-right; gap handles spacing */
  opacity: 0.45;           /* sort arrow */
}
/* Order rows */
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) table.gridstyle tr:not(:first-child) td {
  padding:       var(--space-3);
  border-bottom: 1px solid var(--rule);
  color:         var(--ink);
}
/* First column flush-left so the header label (and the data beneath it) line
   up with the page title, which sits at the content-column edge. */
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) table.gridstyle tr:first-child td:first-child,
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) table.gridstyle tr:not(:first-child) td:first-child { padding-left: 0; }
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) table.gridstyle tr:not(:first-child) a:not(:has(img)):not(.shop-checkout__remove) {
  color:                 var(--ink);
  text-decoration:       underline;
  text-underline-offset: 2px;
}
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) table.gridstyle tr:not(:first-child) a:not(.shop-checkout__remove):hover { color: var(--accent); }
/* "Shop home" button (parent div centres it). !important beats the inline
   width:100px that was clipping the label. */
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) input[type="button"] {
  width:          auto !important;
  display:        inline-block;
  font-family:    var(--font-body);
  font-size:      var(--text-sm);
  font-weight:    500;
  letter-spacing: 0.02em;
  color:          var(--ink);
  background:     transparent;
  border:         1px solid var(--rule);
  border-radius:  999px;
  padding:        var(--space-2) var(--space-6);
  cursor:         pointer;
  transition:     border-color var(--transition), color var(--transition);
}
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) input[type="button"]:hover { border-color: var(--ink); color: var(--accent); }
/* ---- Wishlist + Draft Orders: borderless "when adding items" options table,
   accent checkboxes, and the primary "Add items to basket" submit. */
:is(.shop-wishlist, .shop-draft-orders) table:not(.gridstyle) {
  margin:          var(--space-6) 0;
  border-collapse: collapse;
  font-family:     var(--font-body);
  font-size:       var(--text-sm);
  color:           var(--ink);
}
:is(.shop-wishlist, .shop-draft-orders) table:not(.gridstyle) td { padding: var(--space-1) var(--space-2); border: 0; }
:is(.shop-wishlist, .shop-draft-orders) table:not(.gridstyle) b { font-weight: 600; }
:is(.shop-wishlist, .shop-draft-orders) input[type="checkbox"] { accent-color: var(--accent); }
:is(.shop-wishlist, .shop-draft-orders) input[type="submit"] {
  display:        inline-block;
  font-family:    var(--font-body);
  font-size:      var(--text-sm);
  font-weight:    500;
  letter-spacing: 0.02em;
  color:          var(--accent-ink);
  background:     var(--accent);
  border:         1px solid var(--accent);
  border-radius:  999px;
  padding:        var(--space-2) var(--space-6);
  cursor:         pointer;
  transition:     opacity var(--transition);
}
:is(.shop-wishlist, .shop-draft-orders) input[type="submit"]:hover { opacity: 0.85; }
/* Replace Hubb's dated red delete.png with a clean remove glyph. Matches on
   the image (the icon sits in a different column on the item grids vs the
   draft-orders list), not a column. JS stamps an aria-label so the link
   keeps an accessible name once the <img> is hidden. */
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) a:has(img[src*="delete.png"]) {
  display:         inline-flex;
  align-items:     center;
  justify-content: center;
  width:           1.5rem;
  height:          1.5rem;
  border-radius:   999px;
  color:           var(--ink-soft);
  text-decoration: none;
  transition:      color var(--transition), background var(--transition);
}
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) a:has(img[src*="delete.png"]) img { display: none; }
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) a:has(img[src*="delete.png"])::before {
  content:     "\00d7";   /* multiplication sign -- neutral, modern remove glyph */
  font-size:   1.25rem;
  line-height: 1;
}
:is(.shop-orders, .shop-draft-orders, .shop-wishlist, .shop-checkout) a:has(img[src*="delete.png"]):hover {
  color:      var(--accent);
  background: var(--accent-soft);
}
/* Per-item "Buy" (last-column text link, NOT the remove icon) -> compact
   ghost pill, so it reads as an action and matches the rest of the shop. */
:is(.shop-draft-orders, .shop-wishlist) table.gridstyle td:last-child a:not(:has(img)) {
  display:         inline-flex;
  align-items:     center;
  font-family:     var(--font-body);
  font-size:       var(--text-xs);
  font-weight:     500;
  letter-spacing:  0.04em;
  text-transform:  uppercase;
  color:           var(--ink);
  text-decoration: none;
  border:          1px solid var(--rule);
  border-radius:   999px;
  padding:         var(--space-1) var(--space-4);
  white-space:     nowrap;
  transition:      border-color var(--transition), color var(--transition);
}
:is(.shop-draft-orders, .shop-wishlist) table.gridstyle td:last-child a:not(:has(img)):hover {
  border-color: var(--ink);
  color:        var(--accent);
}

/* =========================================================================
   3.28  SHOP CHECKOUT  --  .shop-checkout (basket / ShowCart view)
   The Checkout module renders the basket review: a progress strip, the cart
   items grid (editable qty/price + remove), a vouchers block, and the action
   buttons. The cart grid is in the .shop-orders grid :is() list above for
   header/rows/hairlines/remove-glyph/first-col-flush; the rules here add the
   checkout-only pieces. Styled in place -- never rebuilt -- so the inputs and
   ASP.NET postbacks stay intact. Delivery/Payment/Confirm are later steps.
   ========================================================================= */
/* Page title (module emits <h1>; a global hero-h1 makes it huge) -> match the
   other shop page titles. Vouchers <h2> sits a step smaller. */
.shop-checkout h1 {
  font-family:    var(--font-display);
  font-weight:    400;
  font-size:      var(--text-xl);
  letter-spacing: -0.01em;
  color:          var(--ink);
  margin:         0 0 var(--space-4);
  padding:        0;
}
.shop-checkout h2 { font-size: var(--text-lg); margin: 0 0 var(--space-3); }
/* Progress strip: drop the dated cart icon + gif arrows (a global
   img{display:block} was stacking them), lay the steps out inline. */
.shop-checkout td:has(> img[src*="CartLarge"]) { display: none; }
.shop-checkout img[src*="CheckoutArrow"] { display: inline-block; vertical-align: middle; width: 16px; height: auto; opacity: 0.6; }   /* soft grey arrows between step pills (was 0.4 -- too faint) */
.shop-checkout .CartProgressHighlighted,
.shop-checkout .CartProgressFaded {
  display:        inline-block;
  font-family:    var(--font-body);
  font-size:      var(--text-xs) !important;   /* beat the module's inline <style> px sizes */
  font-weight:    600;
  letter-spacing: 0.04em;
  text-transform: capitalize;   /* "basket" -> "Basket" */
  padding:        var(--space-1) var(--space-4);
  border-radius:  999px;
}
/* Step pills: faded = light grey, current = accent fill, so the stage is
   obvious at a glance (replaces the faint chevrons; inline span margins space them). */
.shop-checkout .CartProgressFaded       { background: var(--section-light); color: var(--ink-soft); }
.shop-checkout .CartProgressHighlighted { background: var(--accent);         color: var(--accent-ink); }
/* Editable qty / price / voucher text fields (inline widths from the module
   are kept). Scoped to .shop-checkout so the utility-bar search is untouched. */
.shop-checkout input[type="text"] {
  font-family:   var(--font-body);
  font-size:     var(--text-sm);
  color:         var(--ink);
  background:    var(--surface);
  border:        1px solid var(--rule);
  border-radius: var(--radius);
  padding:       var(--space-1) var(--space-2);
}
.shop-checkout input[type="text"]:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 1px;
  border-color:   var(--accent);
}
/* Totals: a nested table in the cart footer. Pull it right + quiet it. Doubled
   .shop-checkout out-specifies the generic grid-row td rule above, which would
   otherwise hairline + indent these nested cells. */
.shop-checkout table.gridstyle table { margin-left: auto; font-size: var(--text-sm); }
.shop-checkout.shop-checkout table.gridstyle table td {
  padding:     var(--space-1) var(--space-2);
  border:      0;
  text-align:  right;
  white-space: nowrap;
  text-transform: capitalize;   /* "basket Total" -> "Basket Total" */
}
/* Hide the "x = remove item" legend (bare delete.png in a div) -- the clean
   remove glyph is self-explanatory. */
.shop-checkout div:has(> img[src*="delete.png"]) { display: none; }
/* Vouchers: hide the empty voucher grid (header only, no item rows); space it. */
.shop-checkout table.gridstyle:not(:has(tr.griditemrow)) { display: none; }
.shop-checkout #divVoucherCode {
  margin-top:    var(--space-8);
  background:    var(--section-light);
  padding:       var(--space-6) var(--space-8) !important;   /* beat module inline padding:10px 0 8px 0 -- restores left gap + symmetry */
  border-radius: var(--radius-lg);
}
.shop-checkout #divVoucherCode h2 { display: none; }   /* streamlined: drop the "Vouchers" title */
.shop-checkout #divVoucherCode b { margin: 0; }   /* flex gap handles spacing now */
/* Action buttons. Base = ghost pill; "Checkout Now" = solid accent (primary),
   targeted by stable control-name suffix. Disabled "Update" stays muted. */
.shop-checkout input[type="submit"] {
  display:        inline-block;
  vertical-align: middle;
  font-family:    var(--font-body);
  font-size:      var(--text-sm);
  font-weight:    500;
  letter-spacing: 0.02em;
  color:          var(--ink);
  background:     transparent;
  border:         1px solid var(--rule);
  border-radius:  999px;
  padding:        var(--space-2) var(--space-5);
  cursor:         pointer;
  transition:     border-color var(--transition), color var(--transition), opacity var(--transition);
}
.shop-checkout input[type="submit"]:hover { border-color: var(--ink); color: var(--accent); }
.shop-checkout input[name$="btnContinueCheckout"] {
  color:        var(--accent-ink);
  background:   var(--accent);
  border-color: var(--accent);
}
.shop-checkout input[name$="btnContinueCheckout"]:hover { color: var(--accent-ink); opacity: 0.85; }
.shop-checkout input[type="submit"][disabled],
.shop-checkout input[type="submit"]:disabled { opacity: 0.4; cursor: not-allowed; }
/* ---- v0.215 checkout refinements -------------------------------------- */
/* Drop the leftover category sidebar + empty items/results that ship from the
   cloned template on the checkout page. Scoped via the sibling checkout
   section so every other shop page keeps its sidebar. */
.shop-layout:has(~ .slice--shop-checkout) { display: none; }
.slice--shop-crumbs:has(~ .slice--shop-checkout) { display: none; }   /* empty crumb on checkout */
.slice--shop-checkout { margin-top: var(--space-8); margin-bottom: var(--space-8); }   /* tight to search bar + T&Cs */
/* Editor-only price field shipped at 45px (clipped); beat the inline width. */
.shop-checkout input[name*="price_"] { width: 4.5rem !important; }
/* Total column right-aligned; the remove control (moved out of the leading
   column by JS) sits under the line total as light-red text. */
.shop-checkout table.gridstyle tr.gridheader td:last-child,
.shop-checkout table.gridstyle tr.griditemrow td:last-child,
.shop-checkout table.gridstyle tr.altgriditemrow td:last-child { text-align: right; }
.shop-checkout__remove {
  display:         block;
  margin-top:      var(--space-1);
  font-family:     var(--font-body);
  font-size:       var(--text-xs);
  font-weight:     500;
  color:           var(--danger);
  text-decoration: none;
  white-space:     nowrap;
}
.shop-checkout__remove:hover { text-decoration: underline; }
/* Voucher block: the global br-hide collapsed the question onto the prompt and
   the field/button were touching. Restore the break + add breathing room. */
.shop-checkout #divVoucherCode br { display: none; }        /* one line: question + field + Apply */
.shop-checkout #divVoucherCode > div { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-3); padding: 0 !important; line-height: normal; }   /* drop inline padding-top:10px */
.shop-checkout #divVoucherCode input[type="text"] {
  margin:         0;
  padding:        var(--space-2) var(--space-3);   /* match the Apply pill height */
  vertical-align: middle;
}
.shop-checkout #divVoucherCode input[type="submit"] { background: var(--surface); vertical-align: middle; }
/* ---- v0.216 checkout refinements -------------------------------------- */
/* Drop the redundant "Total" header row in the footer; keep Basket Total. */
.shop-checkout table.gridstyle table tr:first-child { display: none; }
/* Update button: hard left + solid fill (was floating + ghost). */
.shop-checkout center { text-align: left; }
.shop-checkout input[name$="btnUpdateQuantities"] {
  color:        var(--accent-ink);
  background:   var(--ink-soft);
  border-color: var(--ink-soft);
}
.shop-checkout input[name$="btnUpdateQuantities"]:hover {
  color:        var(--accent-ink);
  background:   var(--ink);
  border-color: var(--ink);
}
/* Continue Shopping is reachable from the nav -- drop it from the panel. */
.shop-checkout input[name$="btnContinueShopping"] { display: none; }
/* Stack Checkout Now + Save (Save spaced below) on the right, under Basket Total. */
.shop-checkout [id$="_pnlButtons"] {
  display:         flex;
  flex-direction:  row;
  flex-wrap:       wrap;
  justify-content: flex-end;
  align-items:     center;
  gap:             var(--space-4);
  margin-top:      var(--space-8);
}
.shop-checkout input[name$="btnSaveDraft"] { order: -1; }   /* Save sits left of Checkout Now */
.shop-checkout [id$="_pnlButtons"] p { display: none; }
.shop-checkout [id$="_pnlButtons"] input[type="submit"] { margin: 0 !important; }
/* ---- v0.227 checkout refinements -------------------------------------- */
/* Hide the artist/author byline Hubb wraps in a bare <div> directly after
   the item link. Streamlines the basket view -- the title alone is enough,
   the author name below it (e.g. "The Town Church Band", "Randall Schmidt")
   is redundant noise. Targets only div siblings of product links so any
   other content in the item cell (none today, defensive) is untouched. */
.shop-checkout table.gridstyle tr:is(.griditemrow, .altgriditemrow) td > a[href*="/Shop/Products/"] + div {
  display: none;
}
/* ---- v0.228 checkout refinements -------------------------------------- */
/* On the checkout page, hide the top-strip mini-basket from .slice--shop-
   utility. The user is already at the cart -- a second summary above the
   editable basket is redundant clutter, and is also where setupShopBasket
   has been observed to silently exit (Hubb appears to render the basket
   module differently here, often without the ShowCart links the rebuilder
   needs to find a cart URL). Keep the search input visible -- still useful. */
body:has(.slice--shop-checkout) .shop-utility__basket {
  display: none;
}
/* Defensive fallback for the seeker-box visitor widget when the inner
   module hasn't been configured with the .visitor_widget CSS Class in
   Hubb's UI. Without that opt-in, our :has(.visitor_widget) docking
   rules at 8564+ don't fire -- so we'd be relying on Hubb's own
   .HideElement { display: none } stylesheet to keep the .seeker-box
   hidden. That default has gone missing on this template (Image 2: the
   <aside> is rendering in flow at the bottom of the page, overlapping
   the Westpoint partner logos). Force display:none here so the page
   doesn't leak the unconfigured widget. Excludes the configured-and-
   docked case (:has(.visitor_widget)) so we don't fight ourselves. */
:is(.seeker-box, .SeekerBox).HideElement:not(:has(.visitor_widget)) {
  display: none !important;
}
/* When no discount is applied to any line, hide the entire Discount
   column. Adam's request: the column-of-zeros is noise. JS adds the
   .shop-checkout--no-discount class to the items grid when every line's
   Discount cell parses to 0; this rule then drops the second-to-last
   cell on the header + item rows. nth-last-child(2) is robust whether
   or not setupShopCheckout has collapsed the leading X column (Discount
   stays second-from-right either way). The footer/totals row (no
   griditemrow/altgriditemrow/gridheader class) is untouched -- its
   colspan layout is unrelated. */
.shop-checkout table.gridstyle.shop-checkout--no-discount tr:is(.gridheader, .griditemrow, .altgriditemrow) > *:nth-last-child(2) {
  display: none;
}
/* ---- v0.229 voucher row layout ----------------------------------------- */
/* Hubb's voucher gridstyle table renders 2 columns -- X delete | code text
   -- but inherits two rules tuned for the items table that worked against
   it here: the table is stretched to 100% width with a 460px min-width
   floor (right for items, wrong for vouchers), and tr td:last-child is
   right-aligned (correct for the items Total column; wrong for the
   voucher code, which ended up far right with the X far left and a wide
   gap between them). The "CODE" header is also redundant for a single-
   data-column table where the X and code self-describe.

   Tighten: drop the header, let the table size to content, and beat the
   global right-alignment so the code sits next to the X. Result: one
   compact left-aligned row per voucher (X then code), with the "Have a
   voucher code?" prompt row below unchanged. */
.shop-checkout #divVoucherCode table.gridstyle {
  width:     auto;
  min-width: 0;
}
.shop-checkout #divVoucherCode table.gridstyle tr.gridheader {
  display: none;
}
.shop-checkout #divVoucherCode table.gridstyle tr:is(.griditemrow, .altgriditemrow) > td:nth-child(2) {
  text-align:   left;
  padding-left: var(--space-3);
}
/* ---- v0.230 delivery step polish ------------------------------------------ */
/* The Delivery step (Hubb's DeliveryASCX module, rendered at /Shop/Checkout/
   Delivery.aspx) drops a completely different shape of markup into our
   .shop-checkout wrapper than the cart did: a 2-column <table class=
   "delivery-container"> with address + phone radios on the left and
   method + charge + totals on the right, plus a series of
   AddressList_AddressTable child tables for the radio lists.
   
   Two structural fixes get most of the readability back:
   (a) Removed .shop-checkout from the over-broad br-hide rule above so
       address line breaks and the totals stack actually render.
   (b) Style the two-column container with a real column gap + dividing
       hairline, and tidy the address/phone radio tables so the radio
       and label don't run together.
   Stack columns on mobile -- the side-by-side breaks down below ~720px
   when the address strings and phone widget compete for width. */
/* v0.237  TWO column-cards (was four section-cards). Kenton's simpler
   layout groups each column into one card -- left = Address + Phone, right
   = Method + Charge -- instead of a card per section. Flatten the table's
   tbody/tr with display:contents so the two <td>s become flex items, then
   style each <td> as the card. The inner tables (below) drop their own card
   backgrounds so we don't double-box. */
.shop-checkout .delivery-container {
  display:     flex;
  gap:         var(--space-6);
  align-items: flex-start;   /* content-height cards, not stretched to equal height */
  margin-top:  var(--space-4);
}
.shop-checkout .delivery-container > tbody,
.shop-checkout .delivery-container > tbody > tr {
  display: contents;
}
.shop-checkout .delivery-container > tbody > tr > td {
  flex:          1 1 0;       /* equal-width columns */
  min-width:     0;           /* allow shrink below intrinsic content width */
  background:    var(--section-light);
  border-radius: var(--radius-lg);
  padding:       var(--space-6);
}
/* Balance the card's bottom space: drop the trailing margin of the last
   section so bottom padding alone sets the gap (matches the top). */
.shop-checkout .delivery-container > tbody > tr > td > *:last-child {
  margin-bottom: 0;
}
/* Address + Phone Number lists. Hubb uses the same .AddressList_
   AddressTable class for both -- one rule covers both blocks. */
.shop-checkout .delivery-container .AddressList_AddressTable {
  width:           100%;
  border-collapse: separate;
  border-spacing:  0;
  margin:          0 0 var(--space-4);   /* v0.237  card bg/padding removed -- the column <td> is the card now */
}
.shop-checkout .AddressList_AddressTable > tbody > tr > td {
  padding:        var(--space-1) var(--space-2);   /* v0.238  tighter vertical -- phone options were too far apart */
  vertical-align: top;
}
.shop-checkout .AddressList_AddressTable > tbody > tr > td:first-child {
  width:         1%;            /* v0.238  shrink the radio cell to the radio so the address sits close */
  white-space:   nowrap;
  padding-left:  0;
  padding-right: var(--space-2);
}
.shop-checkout .AddressList_AddressTable > tbody > tr > td + td {
  padding-left: 0;              /* v0.238  address/number sits right after the radio gap */
}
/* Phone-number rows specifically (Hubb names their radios *_phone_number_rb_*,
   vs *_address_rb_* for addresses): zero the vertical padding so the numbers
   sit close together. Scoped via :has() so multi-address lists -- where rows
   SHOULD stay separated -- are untouched. */
.shop-checkout .AddressList_AddressTable > tbody > tr:has(label[for*="_phone_number_rb_"]) > td {
  padding-top:    0;
  padding-bottom: 0;
}
.shop-checkout .AddressList_Address label {
  cursor:      pointer;
  display:     inline-block;
  line-height: 1.3;        /* v0.242  tighter -- multi-line address lines + phone rows sit closer */
}
/* The "Add Address" link comes with a tiny Hubb +/- GIF as a background
   image (/Images/TP.gif). Drop it -- the low-res GIF doesn't sit well
   alongside our typography; the underlined text reads clearly as an
   action link on its own. */
.shop-checkout .AddressList_AddAddressLink {
  background-image:      none;
  padding-left:          0;
  color:                 var(--accent);
  text-decoration:       underline;
  text-underline-offset: 3px;
}
.shop-checkout .AddressList_AddAddressLink:hover {
  text-decoration: none;
}
/* Delivery Charge breakdown: Hubb's Net/VAT/Rate/Gross mini-table sits
   inside a deprecated <div align="right">. v0.232 left-aligns it so it
   sits under "for Shop items:" within the charge card, consistent with
   the rest of the (left-aligned) page; space the cells so numbers don't
   run together. */
.shop-checkout .delivery-container div[align="right"] {
  text-align:   left;
  margin:       0 !important;        /* v0.240  the indent is on the wrapper, not the table (the table's margin/float were already zeroed in v0.238 and the header IS styled, proving our rules reach it) */
  padding-left: 0 !important;
}
.shop-checkout .delivery-container div[align="right"] > table {
  margin-top:      var(--space-2);
  margin-left:     0 !important;     /* v0.238  legacy align="right" floated the table to the card edge; force it flush-left under "for Shop items:" */
  margin-right:    auto !important;
  float:           none !important;
  border-collapse: collapse;
  font-size:       var(--text-sm);
}
.shop-checkout .delivery-container div[align="right"] > table td {
  padding: var(--space-1) var(--space-2);
}
/* Mobile: stack the two column-cards. tbody/tr stay display:contents;
   flex-direction:column stacks the <td> cards, align-items:stretch makes
   them full width. */
@media (max-width: 720px) {
  .shop-checkout .delivery-container {
    flex-direction: column;
    align-items:    stretch;
  }
}
/* ---- v0.231 delivery step polish (cont.) -------------------------------- */
/* Three small wins on the Delivery step:
   (1) Hubb writes "basket Total" with a lowercase b -- jarring next to
       "Delivery Total" and "Grand Total". Capitalize every <b> label
       that immediately precedes a .Price (only "basket" actually changes
       visually; already-capitalised ones pass through unchanged).
   (2) Grand Total is the figure the customer cares about. v0.231 lifted it
       with the display serif at a larger size; v0.236 dials that back to
       the same size as Delivery/Basket Total (consistent charge sizing per
       request) and distinguishes it by weight + accent colour instead.
       Targeted via the stable ASP.NET id suffix on the inner span, since the
       three totals are siblings within the same <td> separated by <br>s --
       no parent we can scope to.
   (3) [superseded in v0.232] section spacing between Delivery Method and
       Delivery Charge is now handled by the heading + card margins in the
       v0.232 block below, not a stopgap margin on the h2+table. */
.shop-checkout b:has(+ .Price) {
  text-transform: capitalize;
}
.shop-checkout b:has(+ .Price > [id$="lblGrandTotal"]),
.shop-checkout .Price:has([id$="lblGrandTotal"]) {
  font-weight: 700;
  color:       var(--accent);
}
/* Delivery Method dropdown: Hubb ships a bare native <select> which
   reads as raw next to our styled text inputs. Match the input look
   so they sit together on the same row. */
.shop-checkout .delivery-container select {
  font-family:   var(--font-body);
  font-size:     var(--text-sm);
  color:         var(--ink);
  background:    var(--surface);
  border:        1px solid var(--rule);
  border-radius: var(--radius);
  padding:       var(--space-1) var(--space-2);
  cursor:        pointer;
}
.shop-checkout .delivery-container select:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 1px;
  border-color:   var(--accent);
}
/* ---- v0.232 delivery step: grouped cards + alignment ------------------- */
/* Group each section into a light-grey card matching the basket page's
   voucher panel (--section-light bg, --radius-lg). Left column = Delivery
   Address + Phone Number cards; right column = Delivery Method + Delivery
   Charge cards. Columns are 50/50 (table-layout: fixed above) with the
   central hairline removed -- the cards do the grouping now. Headings sit
   above their card as quiet section labels on a consistent rhythm.
   v0.237: collapsed to ONE card per column (see container rule above); the
   headings now sit INSIDE the card and the per-section card styling below
   is stripped. */

/* Section headings sit inside the column card. The first heading in a column
   sits at the card's top padding; the second (Phone Number / Delivery Charge)
   gets a top margin to separate the two sections. Targeted via h2 ~ h2 (the
   2nd h2 in a column) so we don't depend on the heading being a direct child
   of the <td>. Normalise Hubb's inline padding-top on the Phone heading. */
.shop-checkout .delivery-container h2 {
  margin:      0 0 var(--space-4);
  padding-top: 0 !important;
}
.shop-checkout .delivery-container h2 ~ h2 {
  margin-top: var(--space-6);
}

/* "Add Address" sits between the address list and the Phone Number heading,
   inside the left card -- a little rhythm below it. */
.shop-checkout .delivery-container .AddressList_AddAddressLink {
  display: inline-block;
  margin:  0 0 var(--space-2);
}

/* Right-column tables: Delivery Method (1st) and Delivery Charge (2nd).
   v0.237  card bg/padding removed -- the column <td> is the card now. */
.shop-checkout .delivery-container > tbody > tr > td + td > table {
  width:           100%;
  border-collapse: separate;
  border-spacing:  0;
  margin:          0 0 var(--space-4);
}
/* Delivery Method rows: muted label cell, normal value cell, a little
   vertical breathing room. */
.shop-checkout .delivery-container > tbody > tr > td + td > table:first-of-type td {
  padding:        var(--space-1) var(--space-3) var(--space-1) 0;
  vertical-align: baseline;
}
.shop-checkout .delivery-container > tbody > tr > td + td > table:first-of-type td:first-child {
  color:       var(--ink-soft);
  white-space: nowrap;
}

/* Delivery Charge card: left-align everything. The totals cell ships with
   an inline text-align:right that floated the figures to the page edge;
   !important is needed to beat that inline style. A hairline separates the
   Net/VAT breakdown row from the totals row, and the leading <br> at the
   top of each row is dropped so content sits to the card padding. */
.shop-checkout .delivery-container > tbody > tr > td + td > table:nth-of-type(2) td {
  text-align: left !important;
}
.shop-checkout .delivery-container > tbody > tr > td + td > table:nth-of-type(2) td > br:first-child {
  display: none;
}
.shop-checkout .delivery-container > tbody > tr > td + td > table:nth-of-type(2) > tbody > tr + tr > td {
  border-top:  1px solid var(--rule);
  padding-top: var(--space-4);
}

/* Promote the Delivery step's "Continue >>" to the solid-accent primary,
   matching "Checkout Now" on the basket page. btnBack stays the ghost
   pill (secondary). The [id$="_pnlButtons"] flex bar already right-aligns
   and pills both buttons, so only the fill needs setting here. The
   "$btnContinue" suffix excludes btnContinueCheckout / btnContinueShopping. */
.shop-checkout input[name$="$btnContinue"] {
  color:        var(--accent-ink);
  background:   var(--accent);
  border-color: var(--accent);
}
.shop-checkout input[name$="$btnContinue"]:hover {
  color:   var(--accent-ink);
  opacity: 0.85;
}
/* ---- v0.235 Add Address form (Delivery step) --------------------------- */
/* Clicking "Add Address" reveals #divAddAddress: a bare label/input
   table (.tblAddress) + a centred submit, with no container -- it reads
   as flaky markup dropped between the address card and Phone heading.
   Wrap it in the same light-grey card as the saved-address list, left-
   align the labels (they ship inline text-align:right), give the rows
   breathing room, and left-align the Add Address button to match the
   rest of the page. Scoped to .delivery-container so the login-page
   registration form (also .tblAddress, but in pnlLoginControls) is
   untouched -- that's a separate, deferred task. */
.shop-checkout .delivery-container [id$="_divAddAddress"] {
  margin:      var(--space-2) 0 var(--space-4);
  padding-top: var(--space-4);
  border-top:  1px solid var(--rule);   /* v0.237  was a grey card; now a hairline-topped inset within the column card */
}
.shop-checkout .delivery-container .tblAddress {
  width:           100%;
  border-collapse: separate;
  border-spacing:  0;
}
.shop-checkout .delivery-container .tblAddress > tbody > tr > td {
  padding:        var(--space-2) var(--space-3) var(--space-2) 0;
  vertical-align: middle;
}
/* Label cells: right-aligned so the colons line up against the field column
   regardless of the leading "* " on required labels (those asterisks are
   baked into Hubb's label text, so they can't be split off -- right-aligning
   the colon edge is the tidy fix). Muted, shrunk to content. The
   "(* Required data)" foot-note below explains the asterisks. */
.shop-checkout .delivery-container .tblAddress > tbody > tr > td:first-child {
  text-align:    right !important;
  white-space:   nowrap;
  width:         1%;
  color:         var(--ink-soft);
  padding-right: var(--space-4);
}
/* Consistent field width for the text lines; postcode keeps its own short
   inline width (width:100px beats this external width, which is correct --
   a postcode field shouldn't be 260px wide). max-width !important beats the
   inline max-width:200px so the lines aren't needlessly pinched. */
.shop-checkout .delivery-container .tblAddress input[type="text"] {
  width:     260px;
  max-width: 100% !important;
}
/* "(* Required data)" foot-note row: quiet it down. */
.shop-checkout .delivery-container .tblAddress > tbody > tr:last-child td {
  color:     var(--ink-soft);
  font-size: var(--text-sm);
}
/* The Add Address submit ships in a <div style="text-align:center"> --
   left-align it to match everything else. The button itself already
   inherits the ghost-pill submit styling. */
.shop-checkout .delivery-container [id$="_divAddAddress"] > div {
  text-align: left !important;
  margin-top: var(--space-4);
}
/* Mobile: let the field inputs fill the card width when stacked. */
@media (max-width: 720px) {
  .shop-checkout .delivery-container .tblAddress input[type="text"] {
    width: 100%;
  }
}
/* ---- v0.236 Add Address polish + Delivery Charge table ----------------- */
/* (a) The Add Address submit shipped as a ghost pill with a --rule border
   that's nearly invisible on the --section-light card. Give it an accent
   outline so it's clearly actionable; it stays secondary to the page's
   filled "Continue" (which is accent-filled). */
.shop-checkout input[name$="$btnAddAddress"] {
  border-color: var(--accent);
  color:        var(--accent);
}
.shop-checkout input[name$="$btnAddAddress"]:hover {
  background:   var(--accent);
  color:        var(--accent-ink);
  border-color: var(--accent);
}
/* (b) "Cancel" button injected beside "Add Address" (setupAddAddressCancel)
   -- a quiet text button so the accent-outlined Add Address reads primary. */
.shop-checkout .tab-addr-cancel {
  margin-left:           var(--space-4);
  border:                0;
  background:            transparent;
  font-family:           var(--font-body);
  font-size:             var(--text-sm);
  color:                 var(--ink-soft);
  cursor:                pointer;
  text-decoration:       underline;
  text-underline-offset: 3px;
  padding:               var(--space-2) var(--space-1);
}
.shop-checkout .tab-addr-cancel:hover {
  color: var(--accent);
}
/* ---- v0.238 Add Address modal dialog ----------------------------------- */
/* setupAddAddressDialog promotes the form to a centred fixed dialog so it
   doesn't expand the page. The form stays in the DOM (inside the ASP.NET
   <form>) -- only its positioning changes via this class -- so the "Add
   Address" postback still carries the field values. */
.shop-checkout .delivery-container [id$="_divAddAddress"].tab-addr-modal {
  position:      fixed;
  top:           50%;
  left:          50%;
  transform:     translate(-50%, -50%);
  z-index:       1001;
  width:         min(92vw, 460px);
  max-height:    90vh;
  overflow-y:    auto;
  margin:        0;
  padding:       var(--space-6);   /* v0.244  even padding; the title's extra top breathing room now lives on the heading element itself (robust to scroll) */
  background:    var(--surface);
  border:        0;                 /* override the v0.237 hairline-top inset */
  border-radius: var(--radius-lg);
  box-shadow:    0 20px 60px -15px rgba(0, 0, 0, 0.4);
}
/* Real title element (injected by setupAddAddressDialog, replacing the
   v0.238 ::before). Hidden by default; shown only when the form is in modal
   mode so it never appears if the form is ever rendered inline. */
.shop-checkout .delivery-container [id$="_divAddAddress"] .tab-addr-modal__heading {
  display: none;
}
.shop-checkout .delivery-container [id$="_divAddAddress"].tab-addr-modal .tab-addr-modal__heading {
  display:       block;
  margin:        0 0 var(--space-5);
  padding-top:   var(--space-3);   /* v0.244  breathing room above the title, intrinsic to the heading box */
  font-family:   var(--font-display);
  font-size:     var(--text-lg);
  color:         var(--ink);
}
.tab-addr-backdrop {
  position:   fixed;
  inset:      0;
  z-index:    1000;
  background: rgba(0, 0, 0, 0.45);
}
/* (c) Net/VAT/Rate/Gross breakdown: make it read as a proper little table.
   Muted uppercase header row with a hairline under it, values in body ink,
   wider column gaps than v0.230's default cell padding. Header is assumed
   to be the first row (Hubb emits both rows in <tbody>). */
.shop-checkout .delivery-container div[align="right"] > table td {
  padding-right: var(--space-5);
}
.shop-checkout .delivery-container div[align="right"] > table td:last-child {
  padding-right: 0;
}
.shop-checkout .delivery-container div[align="right"] > table td:first-child {
  display: none;          /* v0.241  Hubb emits an empty leading spacer <td> in both rows; it was the whole indent. Drop it so Net/VAT/Rate/Gross start flush-left. */
}
.shop-checkout .delivery-container div[align="right"] > table td:first-child + td {
  padding-left: 0;        /* the now-first visible column (NET / its value) sits flush */
}
.shop-checkout .delivery-container div[align="right"] > table > tbody > tr:first-child td {
  font-size:      var(--text-xs);
  font-weight:    600;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color:          var(--ink-soft);
  border-bottom:  1px solid var(--rule);
  padding-bottom: var(--space-1);
}
.shop-checkout .delivery-container div[align="right"] > table > tbody > tr + tr td {
  color:       var(--ink);
  padding-top: var(--space-2);
}
/* Collapsible breakdown (setupDeliveryBreakdown wraps the table in <details>):
   default view is just the inc-VAT total; the Net/VAT/Rate/Gross table is one
   click away. */
.tab-charge-breakdown {
  margin-top: var(--space-2);
}
.tab-charge-breakdown__summary {
  cursor:      pointer;
  list-style:  none;                 /* drop the native disclosure triangle; we add our own affordance */
  display:     inline-flex;
  align-items: baseline;
  gap:         var(--space-3);
  font-size:   var(--text-sm);
  color:       var(--ink);
}
.tab-charge-breakdown__summary::-webkit-details-marker { display: none; }   /* legacy WebKit marker */
.tab-charge-breakdown__amount {
  font-weight: 600;
}
.tab-charge-breakdown__summary::after {
  content:               "View breakdown";
  font-size:             var(--text-xs);
  color:                 var(--accent);
  text-decoration:       underline;
  text-underline-offset: 2px;
}
.tab-charge-breakdown[open] .tab-charge-breakdown__summary::after {
  content: "Hide breakdown";
}
.tab-charge-breakdown[open] > div[align="right"] {
  margin-top: var(--space-2);        /* gap between summary and the revealed table */
}
/* ---- v0.245 Payment step ----------------------------------------------- */
/* The Payment page ships a Stripe card form plus a billing-address form. The
   checkout header, progress pills, native text inputs, the "Add card" /
   Continue / Back buttons and the button bar are ALL already handled by the
   shared .shop-checkout rules above -- this block only adds the bits unique
   to this page. The card-number / expiry / CVC fields are Stripe-controlled
   iframes inside .element-container; we never touch those (no width, height
   or injection) -- only the layout and labels around them, and the native
   Name / Postcode inputs (which already inherit our input styling). */

/* Card-entry area: a defined panel echoing the Delivery step's cards, so the
   form reads as one intentional block rather than loose fields on the page. */
.shop-checkout .new-card-panel {
  background:    var(--section-light);
  border-radius: var(--radius-lg);
  padding:       var(--space-6);
  max-width:     420px;
}
/* Empty saved-card <ul> (the "New card" radio is hidden by the platform):
   strip its default list indent/bullets so it adds no stray space. */
.shop-checkout .stripe-card-list {
  list-style: none;
  margin:     0;
  padding:    0;
}
/* Consistent vertical rhythm between rows. */
.shop-checkout .new-card-panel .form-row {
  margin-bottom: var(--space-4);
}
/* Field labels: sit on their own line just above the field, at the same size
   as the input text. Direct-child span only, so the nested "what's this?" in
   the CVC label stays inline. */
.shop-checkout .new-card-panel .form-row > label {
  display: block;
}
.shop-checkout .new-card-panel .form-row > label > span {
  display:       block;
  margin-bottom: var(--space-1);
  font-size:     var(--text-sm);
  color:         var(--ink);
}
/* CVC "what's this?" helper: keep it inline + quiet, not a block label. */
.shop-checkout .new-card-panel .whats-this {
  display:     inline;
  margin-left: var(--space-1);
  font-size:   var(--text-xs);
  color:       var(--ink-soft);
  cursor:      help;
}
/* The "Save these card details" row reads as one aligned line, not a stacked
   label. (Billing "Same as delivery address" is handled further down.) */
.shop-checkout .form-row.card-reuse {
  display:       flex;
  align-items:   center;
  gap:           var(--space-2);
  margin-bottom: 0;
}
.shop-checkout .form-row.card-reuse label {
  margin:    0;
  font-size: var(--text-sm);
}
/* Payment note: a quiet aside, set off with a hairline so it doesn't compete
   with the fields above it. */
.shop-checkout .payment-note-panel {
  margin-top:  var(--space-4);
  padding-top: var(--space-3);
  border-top:  1px solid var(--rule);
  font-size:   var(--text-sm);
  color:       var(--ink-soft);
  line-height: 1.5;
}
/* "Add card" sits clear of the note above it. */
.shop-checkout .new-card-panel .submit-btn {
  margin-top: var(--space-5);
}

/* Country / state dropdowns (class "text") in the address forms: match the
   text inputs so the whole form is one family. Applies to both this billing
   form and the Delivery Add Address form -- intentionally, for consistency. */
.shop-checkout select.text {
  font-family:   var(--font-body);
  font-size:     var(--text-sm);
  color:         var(--ink);
  background:    var(--surface);
  border:        1px solid var(--rule);
  border-radius: var(--radius);
  padding:       var(--space-1) var(--space-2);
}
.shop-checkout select.text:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 1px;
  border-color:   var(--accent);
}

/* Billing Address block. The revealed cardholder-address form (shown when
   "Same as delivery address" is unchecked) gets the SAME section-light panel
   as the card-entry block above it, so the two read as a matched pair instead
   of a bare table jammed against the page edge. pnlCardholderAddress is
   Payment-only, so this can't reach the Delivery Add Address modal. */
.shop-checkout [id$="_pnlCardholderAddress"] {
  background:    var(--section-light);
  border-radius: var(--radius-lg);
  padding:       var(--space-6);
  max-width:     420px;
}
.shop-checkout [id$="_pnlAddressDisplay"] .tblAddress {
  border-collapse: separate;
  border-spacing:  0;
}
.shop-checkout [id$="_pnlAddressDisplay"] .tblAddress > tbody > tr > td {
  padding:        var(--space-1) var(--space-2);
  vertical-align: middle;
}
/* Label column sits flush with the panel's inner-left edge, with a clear gap
   before its field so the two columns aren't crunched together. */
.shop-checkout [id$="_pnlAddressDisplay"] .tblAddress > tbody > tr > td:first-child {
  padding-left:  0;
  padding-right: var(--space-3);
  white-space:   nowrap;
}
/* "(* Required data)" foot-note row: quiet it down, matching the other form. */
.shop-checkout [id$="_pnlAddressDisplay"] .tblAddress > tbody > tr:last-child td {
  color:     var(--ink-soft);
  font-size: var(--text-sm);
}
/* "Same as delivery address": align the checkbox with its label on one line. */
.shop-checkout [id$="_cbCardholderAddressSameAsDelivery"] {
  vertical-align: middle;
  margin-right:   var(--space-2);
}
.shop-checkout label[for$="_cbCardholderAddressSameAsDelivery"] {
  vertical-align: middle;
  font-size:      var(--text-sm);
}
/* Mobile: both panels drop their desktop max-width and tighten padding so the
   fixed-width Stripe boxes (card panel) have as much room as possible. */
@media (max-width: 720px) {
  .shop-checkout .new-card-panel,
  .shop-checkout [id$="_pnlCardholderAddress"] {
    max-width: 100%;
    padding:   var(--space-4);
  }
}
/* ---- v0.233 shop login choice page ------------------------------------- */
/* The ShopLogin step (shown to not-logged-in customers before Delivery)
   ships its two choice buttons -- "Returning Customer / Website Member"
   and "New Customer" -- stacked and centred, with "Back" centred below.
   Left-align them to sit under the Checkout heading + progress pills, put
   the two choices on one row, and drop "Back" onto its own line beneath.
   Scoped via the stable _pnlLoginControls id suffix (ShopLoginASCX), so no
   other checkout step is touched. The module's inline text-align:center
   needs !important to beat. */
.shop-checkout [id$="_pnlLoginControls"] > table,
.shop-checkout [id$="_pnlLoginControls"] > table > tbody > tr > td {
  text-align: left !important;
}
/* The empty spacer <p> between the choices and the Back row: zero it, the
   Back row carries its own margin-top below. */
.shop-checkout [id$="_pnlLoginControls"] > p {
  margin: 0;
}
/* Two choice buttons + the "or" inline on one row. #pnlWebsiteMember wraps
   the Returning Customer button in two nested divs (one centred w/ a
   padding-top) -- flatten both to inline-block and left-align. */
.shop-checkout [id$="_pnlWebsiteMember"],
.shop-checkout [id$="_pnlWebsiteMember"] > div {
  display:        inline-block;
  vertical-align: middle;
  padding-top:    0 !important;
  text-align:     left !important;
}
.shop-checkout [id$="_pnlLoginControls"] > table > tbody > tr > td > p {
  display:        inline-block;
  vertical-align: middle;
  margin:         0 var(--space-3);
  color:          var(--ink-soft);
}
.shop-checkout [id$="_pnlLoginControls"] #btnNewCustomer {
  vertical-align: middle;
}
/* "Back" (and the hidden "Create account") sit on their own line below,
   left-aligned. */
.shop-checkout [id$="_pnlLoginControls"] > div {
  text-align: left !important;
  margin-top: var(--space-6);
}
/* Mobile: stack the two choices, "or" between, each on its own line. */
@media (max-width: 720px) {
  .shop-checkout [id$="_pnlWebsiteMember"],
  .shop-checkout [id$="_pnlLoginControls"] #btnNewCustomer {
    display: block;
  }
  .shop-checkout [id$="_pnlLoginControls"] > table > tbody > tr > td > p {
    display: block;
    margin:  var(--space-2) 0;
  }
}
/* ---- v0.234 checkout header spacing + login lead-in -------------------- */
/* (a) More air between the "Checkout" title and the progress pills. The
   module stamps an inline margin-bottom:10px on the <h1>, which beats the
   .shop-checkout h1 rule earlier -- !important to override. Shared header,
   so this applies to every checkout step and keeps the rhythm consistent. */
.shop-checkout h1 { margin-bottom: var(--space-6) !important; }
/* (b) Login choice page only: push the choice block down off the pills and
   introduce a lead-in line above the buttons (injected via ::before since
   the module output isn't hand-authorable). */
.shop-checkout [id$="_pnlLoginControls"] {
  margin-top: var(--space-6);
}
.shop-checkout [id$="_pnlLoginControls"]::before {
  content:     "Please choose one of the following options:";
  display:     block;
  font-family: var(--font-body);
  font-size:   var(--text-base);
  color:       var(--ink);
  margin:      0 0 var(--space-6);
}
/* =========================================================================
   CONFIRM DIALOG  --  .tab-confirm (centred; replaces the native confirm())
   ========================================================================= */
.tab-confirm {
  position: fixed; inset: 0; z-index: 1000;
  display: flex; align-items: center; justify-content: center;
  padding: var(--space-4);
  background: rgba(0, 0, 0, 0.45);
}
.tab-confirm__dialog {
  background:    var(--surface);
  border-radius: var(--radius-lg);
  padding:       var(--space-8);
  max-width:     28rem; width: 100%;
  box-shadow:    0 20px 50px rgba(0, 0, 0, 0.25);
}
.tab-confirm__msg {
  margin:      0 0 var(--space-6);
  font-family: var(--font-body);
  font-size:   var(--text-base);
  color:       var(--ink);
  line-height: 1.5;
}
.tab-confirm__actions { display: flex; justify-content: flex-end; gap: var(--space-3); }
.tab-confirm__btn {
  font-family: var(--font-body); font-size: var(--text-sm); font-weight: 500;
  border-radius: 999px; padding: var(--space-2) var(--space-5);
  cursor: pointer; border: 1px solid transparent;
  transition: opacity var(--transition), border-color var(--transition), color var(--transition);
}
.tab-confirm__btn--ghost  { background: transparent; color: var(--ink); border-color: var(--rule); }
.tab-confirm__btn--ghost:hover  { border-color: var(--ink); }
.tab-confirm__btn--danger { background: var(--danger); color: var(--accent-ink); }
.tab-confirm__btn--danger:hover { opacity: 0.88; }

/* =========================================================================
   SLIDE-OUT PANEL  --  .tab-panel (right-hand sheet)
   -------------------------------------------------------------------------
   Reusable right-hand slide-out. Currently used for the shop Terms &
   Conditions (item injected into the Shop menu by setupShopTermsMenu(), which
   fetches the T&Cs content from /Shop/TermsAndConditions.aspx and renders it
   here). z-index sits above Hubb's jQuery-UI dialogs (100001).
   ========================================================================= */
.tab-panel { position: fixed; inset: 0; z-index: 100050; }
.tab-panel[hidden] { display: none; }
.tab-panel__backdrop {
  position: absolute; inset: 0;
  background: rgba(0, 0, 0, 0.45);
  opacity: 0; transition: opacity var(--transition);
}
.tab-panel.is-open .tab-panel__backdrop { opacity: 1; }
.tab-panel__sheet {
  position: absolute; top: 0; right: 0; height: 100%;
  width: min(36rem, 92vw);
  background: var(--surface);
  display: flex; flex-direction: column;
  box-shadow: -16px 0 48px -24px rgba(0, 0, 0, 0.45);
  transform: translateX(100%);
  transition: transform var(--transition) var(--ease-out-soft, ease);
}
.tab-panel.is-open .tab-panel__sheet { transform: translateX(0); }
.tab-panel__head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-4);
  padding: var(--space-5) var(--space-6);
  border-bottom: 1px solid var(--rule);
  flex-shrink: 0;
}
.tab-panel__title { margin: 0; font-family: var(--font-display); font-weight: 500; font-size: var(--text-xl); color: var(--ink); }
.tab-panel__close {
  display: inline-flex; align-items: center; justify-content: center;
  width: 2.25rem; height: 2.25rem; flex-shrink: 0;
  border: 1px solid var(--rule); border-radius: 999px;
  background: transparent; color: var(--ink); font-size: 1.5rem; line-height: 1;
  cursor: pointer; transition: border-color var(--transition);
}
.tab-panel__close:hover { border-color: var(--ink); }
.tab-panel__body { flex: 1; overflow-y: auto; -webkit-overflow-scrolling: touch; }
.tab-panel__doc { padding: var(--space-6); }
.tab-panel__doc blockquote { margin: 0; }   /* neutralise the system page's default blockquote indent */
.tab-panel__loading { color: var(--ink-soft); }
body.tab-panel-open { overflow: hidden; }   /* scroll lock while the sheet is open */
@media (prefers-reduced-motion: reduce) {
  .tab-panel__backdrop, .tab-panel__sheet { transition: none; }
}
@media (max-width: 720px) {
  .tab-panel__sheet { width: 100vw; }
}
/* v0.165: was flex: 1, which stretched the input across the whole
   strip minus the basket pill -- a runway on wide screens (Adam:
   "really long - too long?"). Capped at 440px: roughly 40
   characters of query, matching the compact field on the reference
   site, and an input's width should signal the expected input
   length. Still shrinkable when space tightens; the 720px mobile
   rule overrides flex-basis to 100% for the stacked layout. */
.shop-utility__search { flex: 0 1 440px; min-width: 0; }
.shop-utility__basket { flex-shrink: 0; margin-left: auto; }

/* Flatten the search module's table layout so the input and Go
   button become direct flex items. display:contents removes the
   table boxes from layout entirely; supported in all target
   browsers. The module wrapper div is classless (see header note)
   so we scope from our own wrapper and flatten structurally. */
.shop-utility__search > div {
  display:     flex;
  align-items: center;
  gap:         var(--space-3);
}
.shop-utility__search table,
.shop-utility__search tbody,
.shop-utility__search tr,
.shop-utility__search td {
  display: contents;
}
/* !important: Hubb's inline updateAdvancedSearchDisplay() script
   stamps display:table-row as an INLINE style on rows carrying a
   mode attribute; only !important beats an inline style. */
/* Only the simple-search row is shown; the advanced rows (Title, By,
   Code/ISBN/ISMN, Type) carry mode="advanced" with an inline display:none
   that our previous blanket tr[mode] !important rule was overriding back to
   visible -- which is why a category with an item type leaked the advanced
   fields while the homepage (no advanced rows) looked clean. Keep simple
   flattened, force advanced hidden. !important beats Hubb's inline display. */
.shop-utility__search tr[mode="simple"] {
  display: contents !important;
}
.shop-utility__search tr[mode="advanced"] {
  display: none !important;
}
/* Hide the secondary search controls: the "In:" category dropdown
   row and the "Search sub-categories" checkbox row. Their defaults
   (current category + subcategories ticked) mean "search the whole
   shop" on the landing page, which is exactly what the single
   search box should do -- so nothing of value is lost, and the bar
   reads as one clean input + Go like the reference design.
   :has() in DESCENDANT form deliberately (Hubb wraps things; the
   direct-child form has burned us before). !important because these
   rows must also beat the display:contents flatten rules above and
   any future inline display Hubb stamps on them. */
.shop-utility__search tr:has(select),
.shop-utility__search tr:has(input[type="checkbox"]) {
  display: none !important;
}
/* Hide the "Find:" label cell (width attr is its only hook -- the
   markup is attribute-soup, there are no classes). The input gets
   aria-label + placeholder from setupShopSearch() in script.js,
   which is a net a11y improvement over a bare table-cell label.
   The width=60 cell is an empty spacer in the Go table. */
.shop-utility__search td[width="32"],
.shop-utility__search td[width="60"] {
  display: none;
}

/* The text input -- Tabernacle field styling.
   !important on width: ships with inline style="width:100%", which
   fights the flex sizing we want. */
.shop-utility__search input[type="text"] {
  width:         auto !important;
  flex:          1;
  min-width:     0;
  font-family:   var(--font-body);
  font-size:     var(--text-sm);
  color:         var(--ink);
  background:    var(--surface);
  border:        1px solid var(--rule);
  border-radius: 999px;
  padding:       var(--space-2) var(--space-4);
  transition:    border-color var(--transition);
}
.shop-utility__search input[type="text"]:focus {
  outline:      none;
  border-color: var(--ink);
}
.shop-utility__search input[type="text"]::placeholder {
  color: var(--ink-soft);
}

/* The Go button -- compact ghost pill matching .section-link
   language. !important on width: ships with inline
   style="width:40px" which crushes the label once padding applies. */
.shop-utility__search input[type="submit"],
.shop-utility__search input[type="button"] {
  width:          auto !important;
  font-family:    var(--font-body);
  font-size:      var(--text-xs);
  font-weight:    700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color:          var(--ink);
  /* Surface fill (not transparent): a ghost pill disappears against
     the tinted band; white-on-tint matches the input beside it. */
  background:     var(--surface);
  border:         1px solid var(--rule);
  border-radius:  999px;
  padding:        var(--space-2) var(--space-5);
  cursor:         pointer;
  transition:     background var(--transition), border-color var(--transition);
}
.shop-utility__search input[type="submit"]:hover,
.shop-utility__search input[type="button"]:hover {
  background:   var(--bg);
  border-color: var(--ink-soft);
}
.shop-utility__search input[type="submit"]:focus-visible,
.shop-utility__search input[type="button"]:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 2px;
}

/* Basket -- setupShopBasket() in script.js parses the module output
   (empty: a text-only div; with items: a layout table of title/
   price/qty rows + total + a Checkout input) and rebuilds it as the
   compact .shop-basket-pill below: basket icon + "N items" + total,
   the whole pill linking to checkout. The raw output is hidden ONLY
   after a successful rebuild (the shop-basket--enhanced class), so
   a JS failure leaves a small but functional basket table. v0.166,
   replacing the v0.163 pill which dumped Hubb's full line-item
   table into a five-word pill when anything was in the basket. */
.shop-basket--enhanced > div {
  display: none;
}
/* No-JS fallback: keep the raw output small and unobtrusive. */
.shop-utility__basket {
  font-size: var(--text-xs);
}
.shop-utility__basket a {
  color: var(--ink);
}

/* The compact pill (JS-built <a>, or <span> when empty) */
.shop-basket-pill {
  display:         inline-flex;
  align-items:     center;
  gap:             var(--space-2);
  font-size:       var(--text-xs);
  font-weight:     700;
  letter-spacing:  0.12em;
  text-transform:  uppercase;
  color:           var(--ink);
  text-decoration: none;
  background:      var(--surface);
  border:          1px solid var(--rule);
  border-radius:   999px;
  padding:         var(--space-2) var(--space-5);
  white-space:     nowrap;
  transition:      border-color var(--transition);
}
a.shop-basket-pill:hover {
  border-color: var(--ink-soft);
}
a.shop-basket-pill:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 2px;
}
.shop-basket-pill--empty {
  color: var(--ink-soft);
}
/* Basket glyph -- inline SVG mask so it recolours with the theme
   via currentColor, same technique as the footer social icons. */
.shop-basket-pill::before {
  content:            "";
  width:              16px;
  height:             16px;
  flex-shrink:        0;
  background-color:   currentColor;
  -webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M8 7V6a4 4 0 1 1 8 0v1h3.6a1 1 0 0 1 1 1.1l-1.2 12A2 2 0 0 1 17.4 22H6.6a2 2 0 0 1-2-1.9L3.4 8.1A1 1 0 0 1 4.4 7H8zm2 0h4V6a2 2 0 1 0-4 0v1z'/></svg>");
          mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'><path d='M8 7V6a4 4 0 1 1 8 0v1h3.6a1 1 0 0 1 1 1.1l-1.2 12A2 2 0 0 1 17.4 22H6.6a2 2 0 0 1-2-1.9L3.4 8.1A1 1 0 0 1 4.4 7H8zm2 0h4V6a2 2 0 1 0-4 0v1z'/></svg>");
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size:   contain;
          mask-size:   contain;
}

/* ---- Two-column shop layout: sidebar + content -------------------------
   Joins the .slice base rule for container width/gutters; the grid
   is this wrapper's own concern. 280px sidebar matches the
   reference proportions and holds category names comfortably. */
.shop-layout {
  display:               grid;
  grid-template-columns: 280px minmax(0, 1fr);
  gap:                   var(--space-12);
  align-items:           start;
  /* Reserve the breadcrumb zone so the layout starts level with the
     category pages even when no breadcrumb is shown (the shop homepage). */
  margin-top:            var(--shop-crumb-zone);
}

/* ---- Sidebar: boxed category menu --------------------------------------- */
.shop-sidebar {
  border:        1px solid var(--rule);
  border-radius: var(--radius);
  background:    var(--surface);
  overflow:      hidden;   /* clip child hover fills to the radius */
  /* Sticky within the column so categories stay reachable while
     scrolling long product listings on category pages. The offset
     reads the header's REAL rendered height, published as
     --site-header-height by initStickyHeader() in script.js --
     v0.166's hardcoded 96px let the panel slide behind the menu
     whenever the header was in its taller (uncompacted) state,
     because the header height varies with compaction and viewport.
     96px fallback covers the no-JS case. */
  position:      sticky;
  top:           calc(var(--site-header-height, 96px) + var(--space-4));
}

.shop-sidebar__title {
  font-family:    var(--font-body);
  font-size:      var(--text-xs);
  font-weight:    700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color:          var(--ink);
  margin:         0;
  padding:        var(--space-4) var(--space-5);
  border-bottom:  1px solid var(--rule);
  background:     var(--accent-soft);
}

/* Category list -- module renders ul#ShopCatNavRoot inside a
   classless div; scope from our nav wrapper. Hairline separators
   between items, full-row hover fill. */
.shop-cat-nav ul {
  list-style: none;
  margin:     0;
  padding:    0;
}
.shop-cat-nav li {
  margin: 0;
}
.shop-cat-nav li + li {
  border-top: 1px solid var(--rule);
}
.shop-cat-nav a {
  display:         inline;
  color:           inherit;          /* row colour; the <li> is the styled row */
  text-decoration: none;
  transition:      color var(--transition);
}
.shop-cat-nav a:hover {
  color: var(--accent);
}
.shop-cat-nav a:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: -2px;   /* inward -- the row is flush to the panel edge */
}
/* Nested subcategories (Town is flat today; the Baptist install
   proves Hubb nests these when categories deepen): indent and step
   down, no separator doubling. */
.shop-cat-nav ul ul {
  border-top: 1px solid var(--rule);
}
.shop-cat-nav ul ul li {
  border-top: 0;
}
.shop-cat-nav ul ul a {
  padding-left: var(--space-8);
  font-size:    var(--text-xs);
  color:        var(--ink-soft);
}

/* ---- Category nav: the ACTUAL Town render -------------------------------
   The module renders classless divs (no <ul>): each category is
   div[style="margin-left:Npx"] > li, where the name is an <a class="subnav">
   followed by a "(n)" count as a plain text node. The CURRENT category's link
   is additionally wrapped in <b>. We neutralise the inline indent, make the
   <li> the padded row and the link inline (so name + count stay on one line),
   add full-row hover, hairline separators (each <li> sits in its own div, so
   li+li never matches -- we separate the wrapper divs), and tint the current
   category (the row whose link is wrapped in <b>) as "you are here". */
.shop-cat-nav div[style*="margin-left"] { margin-left: 0 !important; }
.shop-cat-nav div[style*="margin-left"] + div[style*="margin-left"] {
  border-top: 1px solid var(--rule);
}
.shop-cat-nav li {
  list-style:  none;
  display:     block;
  margin:      0;
  padding:     var(--space-3) var(--space-5);
  font-family: var(--font-body);
  font-size:   var(--text-sm);
  color:       var(--ink);
}
.shop-cat-nav li:hover  { background: var(--accent-soft); }
.shop-cat-nav li:has(b) { background: var(--accent-soft); }   /* current category */
.shop-cat-nav li b { font-weight: 700; }

/* ---- Content column ------------------------------------------------------ */
.shop-content {
  display:        flex;
  flex-direction: column;
  gap:            var(--space-12);
  min-width:      0;   /* let grid children shrink; stops wide tables
                          inside module output blowing out the column */
}

/* Section headings inside the content column: the shared
   .shop-section__heading rule (centred, 32-52px, accent rule below)
   is homepage-scale. Next to a sidebar the headings belong to the
   column, so: left-aligned, one step down, rule snaps left. */
.shop-content .shop-section__heading {
  text-align: left;
  font-size:  var(--text-xl);
  margin:     0 0 var(--space-6);
}
.shop-content .shop-section__heading::after {
  margin-inline: 0;   /* accent rule left-aligned under the text */
}
.shop-content .slice__head--centred {
  text-align:   left;
  margin-block: 0 var(--space-6);
}

/* ---- Featured Categories: image tiles (3-up in the column) -------------- */
.shop_featured_categories .gallery_ul {
  display:               grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap:                   var(--space-6);
  list-style:            none;
  margin:                0;
  padding:               0;
}
.shop_featured_categories .gallery_li {
  position:      relative;
  overflow:      hidden;
  border-radius: var(--radius);
  aspect-ratio:  16 / 10;
  background:    var(--ink);
  box-shadow:    0 4px 12px -4px rgba(26, 22, 18, 0.15);
  transition:    box-shadow var(--transition);
}
.shop_featured_categories .gallery_li:hover {
  box-shadow: 0 12px 32px -8px rgba(26, 22, 18, 0.3);
}
.shop_featured_categories .gallery_li > a {
  display:         block;
  position:        relative;
  width:           100%;
  height:          100%;
  text-decoration: none;
  color:           #FFFFFF;
}
.shop_featured_categories .gallery_li img {
  width:      100%;
  height:     100%;
  object-fit: cover;
  display:    block;
  transition: transform 600ms cubic-bezier(0.16, 1, 0.3, 1);
}
.shop_featured_categories .gallery_li > a:hover img {
  transform: scale(1.06);
}
.shop_featured_categories .gallery_li > a::after {
  content:        "";
  position:       absolute;
  inset:          0;
  background:     linear-gradient(
                    to top,
                    rgba(26, 22, 18, 0.85) 0%,
                    rgba(26, 22, 18, 0.35) 35%,
                    rgba(26, 22, 18, 0)    65%
                  );
  pointer-events: none;
  z-index:        1;
}
/* Caption reuses the shared .explore-church__caption element the JS
   builds; title steps down for the narrower in-column tiles. */
.shop_featured_categories .explore-church__title {
  font-size: var(--text-lg);
}
.shop_featured_categories .gallery_li > a:focus-visible {
  outline:        3px solid var(--accent);
  outline-offset: 3px;
}

/* ---- Featured Products: hotpicks rebuilt as cards -----------------------
   setupShopHotpicks() in script.js parses the module output inside
   .shop-hotpicks and rebuilds it as the .shop-cards grid, then adds
   `shop-hotpicks--enhanced` to the wrapper. The raw output is hidden
   ONLY under that class -- a JS failure leaves products visible.
   NOTE: parser written against the Baptist install's hotpick markup;
   Town's hotpick output is UNVERIFIED until products are flagged as
   hotpicks (the module currently renders empty on Town). */
.shop-hotpicks--enhanced > :not(.shop-cards) {
  display: none;
}

/* ---- Category page: Items List rebuilt as cards ------------------------
   setupShopItems() parses the Items List output inside .shop-items and
   rebuilds it as the same .shop-cards grid used for Hotpicks, then adds
   shop-items--enhanced. The raw table output (including the Order By
   picker) is hidden ONLY under that class -- a JS failure leaves the real
   products visible and on sale. The grid reuses every .shop-card and
   .shop-cards rule above, including the responsive 3->2->1 breakpoints. */
.shop-items--enhanced > :not(.shop-cards) { display: none; }
/* ---- Search results: module output rebuilt as cards --------------------
   setupShopResults() reuses buildShopItemCard (same mini-layout hooks as the
   Items List) to rebuild each result as a .shop-card, prepends a clean count
   line in place of the duplicated "Results X-Y of Z" chrome, and hides the
   raw output under .shop-results--enhanced. A parse failure or zero results
   leaves the raw output visible (real products stay reachable). */
.shop-results--enhanced > :not(.shop-cards):not(.shop-results__count) { display: none; }
.shop-results__count {
  margin:      0 0 var(--space-6);
  font-family: var(--font-body);
  font-size:   var(--text-sm);
  color:       var(--ink-soft);
}
/* The Add-to-basket control is a <button> forwarding its click to Hubb's
   hidden submit, so normalise native button rendering -- the .shop-card__buy
   <a> rules supply the colour, border, shape and hover. */
button.shop-card__buy {
  appearance:         none;
  -webkit-appearance: none;
  background:         transparent;
  font-family:        var(--font-body);
  cursor:             pointer;
}


.shop-cards {
  display:               grid;
  /* 3-up: the content column is ~1100px wide at full container
     (1600 minus sidebar and gap), so 3 cards of ~340px each. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap:                   var(--space-8);
}

.shop-card {
  display:        flex;
  flex-direction: column;
  position:       relative;   /* anchor for the stretched title link */
  background:     var(--surface);
  border:         1px solid var(--rule);
  border-radius:  var(--radius);
  overflow:       hidden;
  transition:     box-shadow var(--transition);
}
.shop-card:hover {
  box-shadow: 0 12px 32px -8px rgba(26, 22, 18, 0.2);
}

/* Cover stage -- fixed-height panel, image CONTAINED not cropped.
   Product shots (book covers especially) carry text and must never
   be cropped the way photographic tiles are. */
.shop-card__media {
  display:         flex;
  align-items:     center;
  justify-content: center;
  height:          240px;
  padding:         var(--space-6);
  background:      var(--accent-soft);
}
.shop-card__media img {
  max-width:  100%;
  max-height: 100%;
  width:      auto;
  height:     auto;
  object-fit: contain;
  display:    block;
  box-shadow: 0 6px 16px -6px rgba(26, 22, 18, 0.35);
}

.shop-card__body {
  display:        flex;
  flex-direction: column;
  gap:            var(--space-2);
  padding:        var(--space-5);
  flex:           1;   /* equal-height cards: footer rows align */
}

.shop-card__title {
  font-family:    var(--font-display);
  font-size:      var(--text-lg);
  font-weight:    500;
  line-height:    1.25;
  letter-spacing: -0.01em;
  margin:         0;
}
.shop-card__title a {
  color:           var(--ink);
  text-decoration: none;
}
/* No hover underline on card titles -- keeps the minimal-hover aesthetic and
   stays consistent across category + cross-sell cards (the whole card links). */
/* Stretched-link: the whole card is the click target for the product
   page; the Buy link re-raises itself above the overlay. */
.shop-card__title a::after {
  content:  "";
  position: absolute;
  inset:    0;
}

.shop-card__author {
  font-size: var(--text-sm);
  color:     var(--ink-soft);
  margin:    0;
}

.shop-card__desc {
  font-size:   var(--text-sm);
  color:       var(--ink-soft);
  line-height: 1.5;
  margin:      0;
  /* Clamp long blurbs so card heights stay rhythmic; full text is
     on the product page one click away. */
  display:            -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow:           hidden;
}

/* Footer row: price left, Buy pill right; pinned to card bottom */
.shop-card__footer {
  display:         flex;
  align-items:     center;
  justify-content: space-between;
  gap:             var(--space-3);
  margin-top:      auto;
  padding-top:     var(--space-3);
}
.shop-card__price {
  font-size:   var(--text-base);
  font-weight: 700;
  color:       var(--ink);
}
.shop-card__buy {
  position:        relative;  /* sits above the stretched-link overlay */
  z-index:         1;
  font-size:       var(--text-xs);
  font-weight:     700;
  letter-spacing:  0.12em;
  text-transform:  uppercase;
  color:           var(--ink);
  text-decoration: none;
  border:          1px solid var(--rule);
  border-radius:   999px;
  padding:         var(--space-2) var(--space-5);
  transition:      background var(--transition), border-color var(--transition);
}
.shop-card__buy:hover {
  background:   var(--accent-soft);
  border-color: var(--ink-soft);
}
.shop-card__buy:focus-visible,
.shop-card__title a:focus-visible {
  outline:        2px solid var(--accent);
  outline-offset: 2px;
}

/* ---- Shop info article ---------------------------------------------------
   The Article module (CSS Class: content_article) reuses internal-
   page prose styling. Shop-context adjustments: hide the article's
   date stamp (delivery info isn't dated editorial), and unwind the
   standalone-page geometry -- .content_article's 820px cap + auto
   margins + gutter padding + 64px block margins are right when the
   article IS the page, but inside the shop content column they
   centre the prose at a visible indent (Adam's side-by-side
   screenshot vs the reference: "Delivery Costs" started well right
   of "Featured Categories"). Here the column provides the width,
   spacing rhythm, and gutters; the article just fills it. */
.slice--shop-info .content_article {
  max-width:      none;
  margin:         0;
  padding-inline: 0;
}
.slice--shop-info .content_article .author {
  display: none;
}


/* =========================================================================
   3.26  MODULE GROUP: Feed Split -- two stacked feeds side by side
          --  .slice--feed-split
   -------------------------------------------------------------------------
   A two-column section that places two existing stacked feeds next to each
   other -- typically Upcoming Events (.upcoming_events_3) on the left and
   Latest Talks/Sermons (.latest_media_4) on the right, each with its own
   heading and a "view all" link beneath. Packaged as a homepage section in
   its own right.

   It REUSES the stacked row variants unchanged: set the left module's CSS
   Class to upcoming_events_3 and the right module's to latest_media_4 in
   Hubb. All row styling, date/play badges, stretched links and JS
   (structureEventDates, media badges, reveals) are inherited from those
   variants -- this section only supplies the two-column shell and undoes the
   variants' 75% desktop width cap so each feed fills its column.

   Body Contents shell (add two NEW modules in Hubb so each gets a fresh
   config object; the placeholders below are prose only -- never put a module
   tag inside an HTML comment):

     section.slice.slice--feed-split
       div.feed-split__col
         h2.feed-split__heading  "Upcoming Events"
         [ Calendar module -- CSS Class: upcoming_events_3 ]
         a.section-link          "View the calendar"
       div.feed-split__col
         h2.feed-split__heading  "Latest Talks"
         [ Media List module -- CSS Class: latest_media_4 ]
         a.section-link          "Full media library"

   Collapses to one column at 1024px (see section 4) -- below that the
   horizontal rows are too wide for half a viewport.
   ========================================================================= */
.slice--feed-split {
  display:               grid;
  grid-template-columns: 1fr 1fr;
  gap:                   clamp(var(--space-8), 4vw, var(--space-16));
  align-items:           stretch;   /* both columns share the tallest height */
}
.feed-split__col {
  display:        flex;
  flex-direction: column;
  /* Container so each card can reflow to image-on-top based on the COLUMN's
     own width (see the container query at the end of this section) -- not the
     viewport, so it works whether there are two columns or one. */
  container-type: inline-size;
  container-name: feedcol;
}
/* Column heading -- left-aligned with a short left-aligned accent rule (the
   centred section headings use a centred rule; columns get their own). */
.feed-split__heading {
  font-family:    var(--font-display);
  font-size:      clamp(1.5rem, 2.5vw, 2.25rem);
  font-weight:    400;
  letter-spacing: -0.01em;
  line-height:    1.1;
  margin:         0 0 var(--space-6);
  color:          var(--ink);
}
.feed-split__heading::after {
  content:          "";
  display:          block;
  width:            clamp(48px, 5vw, 72px);
  height:           2px;
  margin:           var(--space-3) 0 0;
  background-color: var(--accent);
  opacity:          0.85;
  border-radius:    1px;
}
/* Each feed fills its column AND splits into equal-height card slots, so every
   card is the same height and the two columns line up row-for-row at every
   width. align-items:stretch (above) makes the two columns equal height; the
   feeds then grow to fill that height (flex:1) and grid-auto-rows:1fr divides
   it into equal rows. Both modules show the same item count, so a slot on the
   left equals a slot on the right. (width:100% also overrides the 75% desktop
   cap the standalone variants apply -- the column does the narrowing now.)
   Media nests one level deeper (.latest_media_4 > span.MediaList > cards), so
   .latest_media_4 is made a flex column that lets .MediaList fill it. */
.slice--feed-split .upcoming_events_3 {
  flex:                  1;
  display:               grid;
  grid-template-columns: 1fr;
  grid-auto-rows:        1fr;
  gap:                   var(--space-5);
  width:                 100%;
  margin-inline:         0;
}
.slice--feed-split .latest_media_4 {
  flex:           1;
  display:        flex;
  flex-direction: column;
}
.slice--feed-split .latest_media_4 .MediaList {
  flex:                  1;
  display:               grid;
  grid-template-columns: 1fr;
  grid-auto-rows:        1fr;
  gap:                   var(--space-5);
  width:                 100%;
  margin-inline:         0;
}
/* Clamp card titles to two lines so one long title can't make every (now
   equal-height) box balloon. Applied to the <a> so the title's accent rule
   (::after) is unaffected and the whole-card stretched-link (::before on the
   same <a>) still works. */
.slice--feed-split .eventListTitle a,
.slice--feed-split .mediaListTitle a {
  display:            -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp:         2;
  overflow:           hidden;
}
/* Min gap between a feed and its link (the taller column has no slack for
   margin-top:auto to expand into, so guarantee a gap here). */
.slice--feed-split .upcoming_events_3,
.slice--feed-split .latest_media_4 {
  margin-bottom: var(--space-6);
}
/* "View all" link drops to the bottom of its column so the two links line up
   across columns of unequal length (align-items:stretch above gives the
   columns equal height; this pins the link to the shared bottom edge). */
.feed-split__col .section-link {
  margin-top: auto;
  align-self: start;
}

/* --- List (horizontal) card tuning ----------------------------------------
   In the wide/list layout: a smaller title that uses the full card width (the
   pill no longer takes its own column), and a smaller action pill tucked into
   the bottom-right corner. Scoped to feed-split and placed BEFORE the container
   query so the narrow-column vertical rules below still win at <=620px. */
.slice--feed-split .upcoming_events_3 .eventListTitle,
.slice--feed-split .latest_media_4 .mediaListTitle {
  font-size: var(--text-lg);   /* down from the standalone --text-xl so titles wrap rather than truncate */
}
.slice--feed-split .upcoming_events_3 > div {
  grid-template-columns: minmax(0, 240px) 1fr;   /* drop the pill column -- content spans the rest */
  grid-template-rows:    auto auto auto 1fr;       /* last row flexes -> pill drops to the bottom */
  align-items:           start;
}
.slice--feed-split .upcoming_events_3 > div::after {
  grid-column:  2;
  grid-row:     4;
  align-self:   end;
  justify-self: start;      /* bottom-left, under the location line */
}
.slice--feed-split .latest_media_4 .MediaList > div {
  grid-template-columns: minmax(0, 240px) 1fr;
  grid-template-rows:    auto auto 1fr;
  align-items:           start;
}
.slice--feed-split .latest_media_4 .MediaList > div::after {
  grid-column:  2;
  grid-row:     3;
  align-self:   end;
  justify-self: start;      /* bottom-left, under the speaker line */
}
/* Smaller pill, both layouts. */
.slice--feed-split .upcoming_events_3 > div::after,
.slice--feed-split .latest_media_4 .MediaList > div::after {
  padding:   var(--space-2) var(--space-4);
  font-size: var(--text-xs);
}

/* --- Narrow-column reflow: image-on-top cards -----------------------------
   When a column gets narrow the horizontal row (image | content | pill) is
   too wide and the image stretches to match the text height, so titles wrap
   badly and images end up different heights. Reflow each card to image-on-top
   -- the SAME vertical layout the stacked variants already use at <=720px
   viewport -- but trigger it off the COLUMN's width via a container query.
   Because it responds to the column (not the viewport), the two columns can
   stay side by side far longer as the screen narrows; each card just switches
   to its tall form when its own column passes the threshold. The image gets a
   fixed 16:9 box (object-fit:cover, full width) so every card's image is the
   same size and shape regardless of title length. 620px chosen so a typical
   laptop column (e.g. a 14in 1920px screen at 100%, where the column lands
   around 650-690px) keeps the horizontal list rather than flipping to the tall
   cards; it lands close to the 1400px nav breakpoint, so cards become a list
   at roughly the same width the nav becomes desktop. Below 620px the column is
   too narrow for the horizontal row and it reflows to image-on-top. */
@container feedcol (max-width: 620px) {
  /* Upcoming Events 3 */
  .slice--feed-split .upcoming_events_3 > div {
    grid-template-columns: 1fr;
    grid-template-rows:    auto auto auto auto 1fr;   /* last row flexes -> pill drops to the bottom */
    align-items:           start;
    row-gap:               var(--space-1);
  }
  .slice--feed-split .upcoming_events_3 .eventListImage {
    grid-column:  1;
    grid-row:     1;
    align-self:   stretch;
    aspect-ratio: 16 / 9;
    min-height:   0;
  }
  .slice--feed-split .upcoming_events_3 .eventListTitle    { grid-column: 1; grid-row: 2; font-size: var(--text-xl); }
  .slice--feed-split .upcoming_events_3 .eventListDate     { grid-column: 1; grid-row: 3; }
  .slice--feed-split .upcoming_events_3 .eventListLocation { grid-column: 1; grid-row: 4; }
  .slice--feed-split .upcoming_events_3 > div::after {
    grid-column:  1;
    grid-row:     5;
    align-self:   end;       /* pin to the bottom of the equal-height card */
    justify-self: start;
  }
  /* Latest Media 4 */
  .slice--feed-split .latest_media_4 .MediaList > div {
    grid-template-columns: 1fr;
    grid-template-rows:    auto auto auto 1fr;   /* last row flexes -> pill drops to the bottom */
    align-items:           start;
    row-gap:               var(--space-1);
  }
  .slice--feed-split .latest_media_4 .mediaListImage {
    grid-column:  1;
    grid-row:     1;
    align-self:   stretch;
    aspect-ratio: 16 / 9;
    min-height:   0;
  }
  .slice--feed-split .latest_media_4 .mediaListTitle   { grid-column: 1; grid-row: 2; font-size: var(--text-xl); }
  .slice--feed-split .latest_media_4 .mediaListSpeaker { grid-column: 1; grid-row: 3; }
  .slice--feed-split .latest_media_4 .MediaList > div::after {
    grid-column:  1;
    grid-row:     4;
    align-self:   end;       /* pin to the bottom of the equal-height card */
    justify-self: start;
  }
}


/* =========================================================================
   4. MOBILE RESPONSIVENESS
   -------------------------------------------------------------------------
   Two breakpoints:

     <=1400px  -- hamburger pattern kicks in. With 7 verbose menu items
                ("New to Church?", "Get Involved", "Life Events" etc.)
                plus the brand block + search toggle, the desktop nav
                genuinely needs the full container width to fit on one
                line. Below 1400px the layout switches to the mobile
                drawer instead of letting the nav wrap to two lines.

                This means common laptop viewports (1366x768, 1440x900)
                will see the mobile drawer -- that's intentional, and a
                better experience than a wrapped two-line nav. Big
                desktops (1680+) get the horizontal nav.

                If a customer has fewer or shorter menu labels and
                fits on one line at lower widths, the breakpoint can
                be lowered per-customer. Search the file for "1400"
                -- it appears here, in the JS matchMedia calls, and
                in two doc comments.

     <=720px   -- phone-only adjustments: smaller logo, smaller site name,
                wrapped status bar, shorter slideshow, etc.

   Article cards use their own grid breakpoints below: 4 cols default,
   2 cols <=1024px, 1 col <=540px (never an orphan row). The card grid
   sticks at 1024 because there's no wrapping concern -- purely about
   visual density.

   Rule of thumb going forward: each new module added to this file
   should include its own mobile rules inside this section, grouped
   with a comment matching the module name.
   ========================================================================= */

/* -------------------------------------------------------------------------
   4a. <=1400px  --  hamburger pattern (everything below big desktop)
   ------------------------------------------------------------------------- */
@media (max-width: 1400px) {

  /* ---- Priority+ overflow nav is desktop-only: never show the "More"
     control inside the mobile drawer. The clean mobile nav replaces the
     whole menu below 1400, and initPriorityNav deactivates here too --
     this is the defensive belt-and-braces guard. */
  .main-nav .nav-more { display: none !important; }

  /* ---- Suppress the grey divider between the utility row and the brand
     row on mobile. The status bar's account links migrate into the
     slide-in menu (see setupResponsiveStatusbarLinks in script.js), so
     this row sits empty -- the border just looks like an orphan line
     above the logo. */
  .site-header__utility {
    border-bottom: 0;
    padding-block: 0;
  }

  /* ---- Mobile Menu: show the in-header menu button. margin-left:auto
     pushes it (and the search toggle that follows) to the right edge,
     so the layout becomes:  [logo + name] ........ [MENU] [[search]]  */
  .mobile-menu-toggle {
    display:     inline-flex;
    margin-left: auto;
  }

  /* ---- Site Header__nav: becomes a slide-in panel from the right.
     Reset the desktop flex/margin-auto behaviour -- the panel should be
     a normal block container so its children stack vertically. */
  .site-header__nav {
    margin-left:     0;
    display:         block;
    position:        fixed;
    top:             0;
    right:           0;
    bottom:          0;
    width:           min(85%, 360px);
    background:      var(--surface);
    box-shadow:      -4px 0 24px rgba(26, 22, 18, 0.12);
    transform:       translateX(100%);
    transition:      transform 0.3s ease;
    z-index:         9050;       /* above backdrop, below toggle button */
    /* Padding composes with device safe-area insets so the menu
       items at top clear notch / dynamic island, and account links
       at bottom clear the iPhone home indicator. */
    padding:         calc(var(--space-6) + var(--safe-top))
                     calc(var(--space-4) + var(--safe-right))
                     calc(var(--space-6) + var(--safe-bottom))
                     var(--space-4);
    overflow-y:      auto;
    border-bottom:   0;
  }
  .menu-open .site-header__nav { transform: translateX(0); }

  /* ---- Account links (login/logout/web office) -- appear at the BOTTOM
     of the panel above the search input. JS (setupResponsiveStatusbarLinks)
     moves them from the .statusbar into the panel at this breakpoint.
     Rendered as a row of small uppercase pills. The override block
     further down (after .mobile-menu-close) flips the border to a
     top border (separates them from the nav above) -- this base rule
     handles the row layout. */
  .mobile-account-links {
    display:        flex;
    flex-wrap:      wrap;
    gap:            var(--space-2) var(--space-4);
    padding:        var(--space-4) var(--space-4) 0;
  }
  .mobile-account-links a {
    font-family:     var(--font-body);
    font-size:       var(--text-xs);
    font-weight:     700;
    letter-spacing:  0.12em;
    text-transform:  uppercase;
    color:           var(--ink-soft);
    text-decoration: none;
    transition:      color var(--transition);
  }
  .mobile-account-links a:hover {
    color:   var(--accent);
    opacity: 1;
  }

  /* ---- HIDE Hubb's table-based nav on mobile.
     Trying to dress up nested HTML tables as a mobile menu produces
     edge cases at every turn (two-column expansion when a dropdown
     opens, text wrapping inside cells, JS positioning math gone
     wrong). On mobile we keep the original .main-nav present in the
     DOM but visually hidden -- its onclick handlers are still wired
     up by Hubb's JS, so our clean rebuild can forward clicks to it
     for actual navigation. ---- */
  .main-nav {
    display: none;
  }

  /* ---- CLEAN MOBILE NAV.
     Built fresh by JS (buildCleanMobileNav in script.js) by reading
     the labels and onclick handlers out of Hubb's table once at init,
     then constructing a proper <ul>/<li> structure. Looks like
     Tailwind Plus's mobile nav: top-level rows with chevron at the
     far right, smooth accordion expansion, sub-items stacked
     beneath the parent (no two-column anything).

     Structure:
       <ul.mobile-nav-clean>
         <li.mobile-nav-item>           <- top-level item
           <a.mobile-nav-link>...       <- if no submenu (Home)
         <li.mobile-nav-item.has-submenu>
           <button.mobile-nav-link>     <- expandable
             <span.mobile-nav-label>About Us
             <span.mobile-nav-chevron>  <- far right, rotates on expand
           <ul.mobile-nav-sub>          <- collapsed via max-height
             <li><a.mobile-nav-sublink>About Us</a></li>
             <li><a.mobile-nav-sublink>Our Team</a></li>
             ... ---- */

  .mobile-nav-clean {
    display:    block;
    list-style: none;
    padding:    0;
    margin:     0;
  }

  .mobile-nav-item {
    border-bottom: 1px solid var(--rule);
  }
  /* No top border on first item -- the menu header above carries
     enough visual weight, an extra rule would be noise. */
  .mobile-nav-item:last-child {
    border-bottom: 0;
  }

  /* Top-level row -- same look whether it's an <a> (no submenu) or
     a <button> (has submenu). Label left, chevron pinned right. */
  .mobile-nav-link {
    display:         flex;
    align-items:     center;
    justify-content: space-between;
    width:           100%;
    padding:         var(--space-4);
    background:      transparent;
    border:          0;
    text-align:      left;
    font-family:     var(--font-body);
    font-size:       var(--text-base);
    font-weight:     500;
    color:           var(--ink);
    text-decoration: none;
    cursor:          pointer;
    transition:      color var(--transition);
    -webkit-appearance: none;
    appearance:      none;
  }
  .mobile-nav-link:hover {
    color:   var(--accent);
    opacity: 1;
  }
  .mobile-nav-link:focus-visible {
    outline:        2px solid var(--accent);
    outline-offset: -2px;
  }
  .mobile-nav-label {
    flex: 1;          /* take remaining space, push chevron right */
  }

  /* Chevron -- pure CSS triangle on the far right of the row */
  .mobile-nav-chevron {
    display:       block;
    width:         9px;
    height:        9px;
    border-right:  2px solid currentColor;
    border-bottom: 2px solid currentColor;
    transform:     rotate(45deg) translate(-2px, -2px);
    transition:    transform 0.25s ease;
    opacity:       0.5;
    flex-shrink:   0;
    margin-left:   var(--space-3);
  }
  /* Chevron rotation when item is expanded.
     CRITICAL: use direct-child combinators, not descendant.
     `.is-expanded .chevron` (descendant) would also rotate the
     chevrons of nested children inside the expanded item -- which
     is what made "Sermon Series" appear pre-expanded in v0.29
     when its parent "What's On" was expanded. */
  .mobile-nav-item.is-expanded > .mobile-nav-link  > .mobile-nav-chevron,
  .mobile-nav-item.is-expanded > .mobile-nav-sublink--toggle > .mobile-nav-chevron {
    transform: rotate(-135deg) translate(-2px, -2px);
    opacity:   0.85;
  }

  /* Accent color on the expanded item's own label (not children's) */
  .mobile-nav-item.is-expanded > .mobile-nav-link,
  .mobile-nav-item.is-expanded > .mobile-nav-sublink--toggle {
    color: var(--accent);
  }

  /* Submenu -- hidden by default, expand on .is-expanded.
     Uses max-height for the smooth open/close transition. */
  .mobile-nav-sub {
    list-style:  none;
    padding:     0;
    margin:      0;
    max-height:  0;
    overflow:    hidden;
    background:  rgba(0, 0, 0, 0.04);
    background:  color-mix(in srgb, var(--ink) 6%, transparent);   /* faint accent-soft tint */
    transition:  max-height 0.35s ease;
  }
  .mobile-nav-item.is-expanded > .mobile-nav-sub {
    max-height: 1000px;
  }

  .mobile-nav-sublink {
    display:         block;
    width:           100%;
    padding:         var(--space-3) var(--space-4) var(--space-3) var(--space-8);
    background:      transparent;
    border:          0;
    text-align:      left;
    font-family:     var(--font-body);
    font-size:       var(--text-sm);
    font-weight:     500;
    color:           var(--ink);
    text-decoration: none;
    cursor:          pointer;
    transition:      background var(--transition), color var(--transition);
    -webkit-appearance: none;
    appearance:      none;
  }
  .mobile-nav-sublink:hover {
    color:      var(--accent);
    background: rgba(232, 213, 196, 0.4);
    opacity:    1;
  }

  /* Sub-item that itself has children (level-2 with a level-3 below).
     Renders as a row with label left, chevron right, just like the
     top-level expandable items but smaller. The expanded color and
     chevron rotation are handled by the combined rule earlier in
     this section (under the .mobile-nav-link rule). */
  .mobile-nav-sublink--toggle {
    display:         flex;
    align-items:     center;
    justify-content: space-between;
  }

  /* Level-3 sub-sub items -- deeper indent + slightly muted background
     so the nesting hierarchy is visually obvious. */
  .mobile-nav-sub--deep {
    list-style:  none;
    padding:     0;
    margin:      0;
    max-height:  0;
    overflow:    hidden;
    background:  rgba(232, 213, 196, 0.3);   /* slightly stronger tint */
    transition:  max-height 0.35s ease;
  }
  .mobile-nav-item.is-expanded > .mobile-nav-sub--deep {
    max-height: 800px;
  }
  .mobile-nav-sub--deep .mobile-nav-sublink {
    padding-left: var(--space-12);              /* deeper indent for level 3 */
    font-size:    calc(var(--text-sm) * 0.95);  /* fractionally smaller */
  }

  /* ---- MENU HEADER -- top row of the open drawer.
     Contains a "MENU" label on the left and the CLOSE button on the
     right. Built by JS (addMobilePanelHeader) and inserted as the
     first child of .site-header__nav. The header sits in normal flow
     (no absolute positioning) so the rest of the panel content flows
     naturally below it -- important now that the panel has more
     content (nav + account links + search) and content might
     overflow the viewport on small phones, requiring scroll. The
     padding-top on the panel itself is reduced when this header is
     in place because the header carries its own internal padding. ---- */
  .mobile-menu-header {
    display:         flex;
    align-items:     center;
    justify-content: space-between;
    gap:             var(--space-4);
    padding-bottom:  var(--space-4);
    margin-bottom:   var(--space-4);
  }

  .mobile-menu-label {
    font-family:     var(--font-body);
    font-size:       var(--text-xs);
    font-weight:     700;
    letter-spacing:  0.18em;
    text-transform:  uppercase;
    color:           var(--ink-soft);
    line-height:     1;
  }

  /* CLOSE button -- sits on the right of .mobile-menu-header.
     Pill button matching the .site-search-overlay__close pattern so
     close affordances feel like one family across the site. */
  .mobile-menu-close {
    display:         inline-flex;
    align-items:     center;
    gap:             var(--space-2);
    padding:         var(--space-2) var(--space-4);
    background:      transparent;
    border:          1px solid var(--rule);
    border-radius:   999px;
    cursor:          pointer;
    color:           var(--ink);
    font-family:     var(--font-body);
    font-size:       var(--text-xs);
    font-weight:     700;
    letter-spacing:  0.1em;
    text-transform:  uppercase;
    line-height:     1;
    transition:      background var(--transition), border-color var(--transition);
  }
  .mobile-menu-close__icon {
    font-size:   18px;
    font-weight: 300;
    line-height: 1;
  }
  .mobile-menu-close:hover {
    background:   var(--accent-soft);
    border-color: var(--accent);
  }
  .mobile-menu-close:focus-visible {
    outline:        2px solid var(--accent);
    outline-offset: 2px;
  }

  /* ---- Account links -- sit ABOVE the in-drawer search. No border
     above; visual separation comes from the spacing alone, matching
     the cleaner look the rest of the drawer now uses. ---- */
  .mobile-account-links {
    margin-top:  var(--space-6);
  }

  /* ---- In-drawer search.
     The .site-search element is moved here by JS (setupResponsiveSearch)
     when viewport drops below 1400px. Sits at the very bottom of the
     panel so users always know where to find it without scrolling
     past the nav. The .site-search-toggle icon in the header is
     hidden at this breakpoint -- duplicate entry points for the same
     thing creates ambiguity. ---- */
  .mobile-search {
    display:       block;
    margin-top:    var(--space-4);
    padding:       var(--space-4);
  }
  .mobile-search .site-search {
    display: flex;
    margin:  0;
    width:   100%;
    max-width: none;
  }
  .mobile-search .site-search .text {
    border-radius: var(--radius) 0 0 var(--radius);
    padding:       var(--space-3);
    font-size:     var(--text-sm);
    border:        1px solid var(--rule);
    background:    var(--surface);
  }
  .mobile-search .site-search .text:focus {
    border-color: var(--accent);
    outline:      none;
  }
  .mobile-search .site-search .button {
    border-radius: 0 var(--radius) var(--radius) 0;
    /* --space-6 (24px) horizontal padding ensures button width comfortably
       exceeds the 24px WCAG tap target minimum. Earlier version used
       --space-5 which is undefined -- silently invalid CSS, button shrank
       to 23px wide and failed Lighthouse target-size audit. */
    padding:       var(--space-3) var(--space-6);
    font-size:     var(--text-xs);
  }

  /* Hide the magnifying-glass toggle in the header at this breakpoint.
     The search input is now visible inline at the bottom of the
     drawer, so there's no need for the icon affordance. The overlay
     itself also stops being relevant on mobile -- it stays in DOM but
     can never be opened (no toggle). */
  .site-search-toggle { display: none; }

  /* ---- Account toggle: hide at this breakpoint. The hamburger drawer
     already absorbs the statusbar's account links via the existing
     setupResponsiveStatusbarLinks JS -- having a second account
     affordance in the header would be visually noisy. ---- */
  .account-toggle { display: none; }

  /* ---- Visitor widget on mobile.
     The desktop pattern (96px Hello! bubble + generous pill padding)
     consumes too much screen real estate on a phone, but the previous
     mobile rules over-corrected -- pill was tiny and the bubble photo
     shrunk to 28px made "Hello!" almost unreadable. Refined sizes:
     pill stays at readable size, bubble drops to 80px (still
     substantial), photo inside scales to 50px proportionally.
     Margins recalculated from the 80px bubble's reach. ---- */

  /* Wrapper bottom/right offset -- closer to viewport edge on mobile. */
  :is(.seeker-box, .SeekerBox):has(.visitor_widget) {
    bottom: calc(var(--space-4) + var(--safe-bottom));
    right:  calc(var(--space-4) + var(--safe-right));
  }

  /* Pill keeps the same padding as desktop (var(--space-3) var(--space-6))
     and inherits desktop margin-top/right. We do NOT override these
     here -- they were the previous source of "tiny pill, tiny text". */
  :is(.seeker-box, .SeekerBox):has(.visitor_widget) .seeker_box {
    /* Slightly less margin reservation than desktop because the
       smaller mobile bubble (80px) needs less clearance. */
    margin-top:   var(--space-8);    /* was --space-12 (48); now 32 for 80px bubble */
    margin-right: var(--space-4);    /* was --space-8 (32); now 16 for 12px poke */
  }

  /* Bubble: shrink from 96px to 80px and reposition for the new size.
     Bubble lifts ~40px above pill (half of 80) and pokes 12px past edge. */
  :is(.seeker-box, .SeekerBox):has(.visitor_widget) .seeker_box .HeadLink {
    width:  80px;
    height: 80px;
    top:    calc(var(--space-8) * -1);     /* -32px -- lift ~40% above pill */
    right:  calc(var(--space-3) * -1);     /* -12px -- slight poke past edge */
  }

  /* Hello! photo inside bubble: proportional to bubble size.
     50px image inside 80px bubble keeps the script "Hello!" clearly
     readable (was 28px previously which was illegibly small). */
  :is(.seeker-box, .SeekerBox):has(.visitor_widget) .seeker_box .HeadLink .Photo {
    width:  50px;
    height: 50px;
  }

  /* Article body padding-right: bubble extends 80-12 = 68px in from
     pill's right edge. Pill itself has 24px right padding. Need at
     least 68-24 = 44px of article body padding to keep text clear,
     plus a bit of breathing room. */
  :is(.seeker-box, .SeekerBox):has(.visitor_widget) .seeker_box .ArticleBody {
    padding-right: var(--space-12);  /* 48px -- clears bubble with 4px breathing */
  }

  /* Dismiss button on mobile -- slightly larger hit target than the
     24x24 desktop minimum because touch users can't hover-discover
     and the button is intentionally visually small. 28x28 is still
     compact enough to fit the smaller mobile pill without crowding
     the "Visitors click here" text beside it. */
  :is(.seeker-box, .SeekerBox):has(.visitor_widget) .visitor-widget__dismiss {
    width:  28px;
    height: 28px;
  }

  /* Side panel inset on narrow screens -- tighter than desktop.
     Safe-area-inset-top + bottom keep content clear of notch and
     home indicator on iPhones. */
  :is(.seeker-box, .SeekerBox):has(.visitor_widget) .FullSeekerArticle {
    padding: calc(var(--space-12) + var(--safe-top))
             var(--space-4)
             calc(var(--space-4) + var(--safe-bottom));
  }
  .side-panel__content {
    padding: calc(var(--space-12) + var(--safe-top))
             var(--space-4)
             calc(var(--space-4) + var(--safe-bottom));
  }
}

/* -------------------------------------------------------------------------
   4b. <=720px  --  phone-only tweaks
   ------------------------------------------------------------------------- */
@media (max-width: 720px) {

  /* ---- About carousel: narrower cards on phones so the next card peeks in,
     hinting the rail scrolls; tighten the overlaid content padding. ---- */
  .slice--about-carousel .gallery_li     { --about-card-w: 240px; }
  .slice--about-carousel .about-card__body { padding: var(--space-4); }
  .slice--about-carousel .gallery_ul     { --about-rail-inset: var(--space-4); }
  .slice--about-carousel .about-carousel__nav   { gap: var(--space-4); margin-top: var(--space-5); }
  .slice--about-carousel .about-carousel__arrow { width: 2rem; height: 2rem; }
  .slice--about-carousel .about-carousel__arrow svg { width: 18px; height: 18px; }
  .slice--about-carousel .about-carousel__dot   { width: 9px; height: 9px; }
  .slice--about-carousel .about-carousel__viewport::before,
  .slice--about-carousel .about-carousel__viewport::after { width: clamp(1rem, 8vw, 2rem); }

  /* ---- Two-row header strip: tighten for phones. Drop the greeting (saves
     width) and shrink the session links + Giving so they sit on one line. ---- */
  .site-header--tworow .site-header__utility-inner { padding-block: var(--space-1); gap: var(--space-3); }
  .site-header--tworow .statusbar { gap: var(--space-3); font-size: 0.7rem; }
  .site-header--tworow .statusbar .greeting_div { display: none; }
  .site-header--tworow .header-giving,
  .site-header--split  .header-giving { padding: var(--space-1) var(--space-4); }

  /* ---- Shop utility bar: stack on phones. Search goes full-width on
     its own row (the primary action); crumbs + basket share the row
     above it. ---- */
  .shop-utility {
    flex-wrap: wrap;
    gap:       var(--space-3) var(--space-4);
  }
  .shop-utility__crumbs { order: 1; }
  .shop-utility__basket { order: 2; }
  .shop-utility__search { order: 3; flex-basis: 100%; }

  /* ---- Shop product cards: shorter cover stage so a single-column
     card doesn't push its text below the fold. ---- */
  .shop-card__media { height: 200px; }

  /* ---- Container gutter: bump up slightly so content has more breathing
     room from the screen edges. 16px feels cramped on a phone. ---- */
  :root {
    --container-gutter: clamp(1.25rem, 5vw, 2rem);
  }

  /* ---- Site Header: tighter padding (brand left + menu button right) ---- */
  .site-header__main {
    gap:            var(--space-4);
    padding-block:  var(--space-4);
  }

  /* ---- Site Logo: slightly smaller ---- */
  .site-logo img { height: 40px; }

  /* ---- Feature Section: shrink the section a bit and scale down
     the title. Parallax still works on mobile via JS but at a
     reduced intensity (the JS in setupFeatureParallax checks
     viewport width).
     Critical: drop the desktop full-bleed treatment. On a phone the
     edge-to-edge behaviour means the section runs all the way to
     the device bezel with no gutter, which jars against every other
     section on the page. Keeping it within the container gives a
     consistent gutter and lets us add a radius for a polished card
     feel. The clamps in the base rule still apply for the inner
     padding -- these phone-specific values just tighten things
     further. ---- */
  feature {
    min-height:    clamp(360px, 50vh, 500px);
    /* --space-12 (48px) vertical, --space-6 (24px) horizontal. Earlier
       version used --space-14 / --space-5 which aren't in the scale,
       so the rule was silently invalid and inherited the desktop
       clamp() values which were too generous on phones. */
    padding:       var(--space-12) var(--space-6);
    margin-inline: 0;                /* contain -- drop full-bleed */
    margin-block:  var(--space-12);
    border-radius: var(--radius);    /* now that edges are visible */
  }
  .feature__title {
    font-size: clamp(2rem, 8vw, 2.75rem);
  }
  .feature__description {
    font-size: var(--text-sm);
  }

  /* ---- Site Name: smaller but still legible ---- */
  .site-name { font-size: var(--text-lg); }

  /* ---- Status Bar: simplify on phones -- hide the date so the
     login/register links aren't crowded. The session links matter
     more than seeing today's date. ---- */
  .statusbar {
    gap:             var(--space-3);
    flex-wrap:       wrap;
    justify-content: center;
  }
  .statusbar .greeting_div { display: none; }

  /* ---- Mobile Menu button: ensure 44x44px tap target (WCAG) ---- */
  .mobile-menu-toggle {
    min-height: 44px;
    min-width:  44px;
  }

  /* ---- Section spacing: tighter rhythm on mobile so users don't
     scroll through huge empty bands between sections ---- */
  .latest-news,
  .welcome_article,
  .upcoming-events {
    margin-block: var(--space-12);
  }

  /* ---- Welcome Article: nicer mobile typography ---- */
  .welcome_article .title3 {
    font-size:   var(--text-base);
    line-height: 1.6;
  }

  /* ---- Where & When: collapse the 2-column card to a single
     stacked layout. Columns turn off; the second <strong> stops
     forcing into a new column and instead just sits below the
     first section with a margin-top to give it breathing room.
     Padding tightens so the card doesn't feel oversized on a
     phone. ---- */
  .where_when {
    padding: var(--space-6);
  }
  .where_when .ArticleBody p,
  .where_when .ArticleBody:has(p + p) {
    column-count: 1;
    column-rule:  none;
  }
  .where_when .ArticleBody p strong:nth-of-type(2),
  .where_when .ArticleBody strong:nth-of-type(2) {
    break-before: auto;       /* no longer forcing into column 2 */
    margin-top:   var(--space-6);   /* visual gap between stacked sections */
  }

  /* where_when_2 strip: same collapse to a single stacked column on phones. */
  .where_when_2 .ArticleBody p,
  .where_when_2 .ArticleBody:has(p + p) { column-count: 1; column-rule: none; }
  .where_when_2 .ArticleBody p strong:nth-of-type(2),
  .where_when_2 .ArticleBody strong:nth-of-type(2) { break-before: auto; margin-top: var(--space-4); }

  /* ---- Announcement Banner: tighter vertical padding so it doesn't
     dominate the phone viewport, but still readable. Bumped from
     --text-xs to --text-sm in v0.128 -- xs (~13px) was fine-print
     territory, especially for a message users actually need to
     read. --text-sm (~14-15px) is small enough to leave room for
     the hero below without becoming a footnote. ---- */
  .announcement_banner {
    font-size:     var(--text-sm);
    padding-block: var(--space-2);
  }

  /* ---- Upcoming Events: bigger gap + hairline divider on mobile.
     v0.143: Adam's PDF showed cards reading as run-on -- the
     location text of card N sat 24px above the next card's image,
     and without any visual delimiter the eye couldn't tell where
     one card ended and the next began. Two coordinated changes:
     - Gap doubled from 24px to 48px for stronger visual breathing.
     - Subtle 1px hairline beneath each card (except the last one)
       gives a definitive "card ends here" boundary that the gap
       alone can't provide. The padding-bottom matches the gap
       value visually so the line sits centred in the gap. ---- */
  .upcoming_events { gap: var(--space-12); }
  .upcoming_events > div:not(:last-child) {
    padding-bottom: var(--space-6);
    border-bottom:  1px solid var(--rule);
  }

  /* ---- Latest Media 3: collapse the 2x2 grid to a single column on
     phones. With cards having an image + date + title + speaker, two
     across at phone width crams them too tight; one column gives each
     card full breathing room. ---- */
  .latest_media_3 .MediaList {
    grid-template-columns: 1fr;
  }

  /* ---- Carousel layouts (Upcoming Events 2 + Latest Media 2):
     Collapse the 2-col grid (intro | viewport) to a single column.
     Intro stacks on top, viewport takes full width beneath. The
     prev / next buttons hide -- swipe is the natural mobile
     interaction. The edge fades stay visible: they're the cleanest
     "more content this way" signal we have on a touch screen
     (combined with a partial-card spilling past the right edge).
     Narrowed to 40px so they don't obscure too much of a phone-
     width card.

     v0.143: added the `.slice--` prefixed selectors. The desktop
     rule above (line 3391-3394) already covers both
     `.slice--latest-media` AND the legacy `.latest-media` alias,
     but THIS mobile rule was only matching the legacy class -- so
     a section authored with `class="slice slice--latest-media"`
     (without the legacy alias) would keep its 2-col grid on mobile,
     which is exactly what Adam's PDF showed: intro column on the
     left taking most of the width, single carousel slide barely
     visible on the right edge of the screen. Mirroring the desktop
     selector list fixes this and keeps the rule resilient to
     however the section's classes are authored. */
  .slice--upcoming-events:has(> .upcoming-events-2__intro),
  .upcoming-events:has(> .upcoming-events-2__intro),
  .slice--latest-media:has(> .latest-media-2__intro),
  .latest-media:has(> .latest-media-2__intro) {
    grid-template-columns: 1fr;
    row-gap:               var(--space-6);
  }
  .upcoming_events_2 > div,
  .latest_media_2 .MediaList > div {
    flex-basis: 240px;     /* slightly narrower cards on phone */
  }
  .upcoming-events-2__heading,
  .latest-media-2__heading {
    font-size: var(--text-xl);
  }
  .carousel__prev,
  .carousel__next {
    display: none;
  }
  .carousel-viewport::before,
  .carousel-viewport::after {
    width: 40px;
  }

  /* ---- 3.6c Articles List feature variant: stack to one column ---- */
  .al-cards-feature {
    grid-template-columns: 1fr;
    row-gap:               var(--space-5);  /* safe here: lead is grid-row:auto on mobile */
  }
  .al-cards-feature > div:first-child {
    grid-column: 1;
    grid-row:    auto;
  }
  .al-cards-feature > div:first-child .articleListImage {
    aspect-ratio: 16 / 10;
    min-height:   0;
  }
  .al-cards-feature > div:not(:first-child) {
    grid-column:           1;
    grid-template-columns: 84px minmax(0, 1fr);
    padding-block:         var(--space-3);
  }

  /* ---- 3.6d Articles List rail variant: intro on top, full-bleed swipe.
     Arrow buttons + edge fades are already hidden/handled by the generic
     carousel mobile rules above. ---- */
  .slice--latest-news:has(> .latest-news-2__intro),
  .latest-news:has(> .latest-news-2__intro) {
    grid-template-columns: 1fr;
    row-gap:               var(--space-6);
  }
  .al-cards-rail > div {
    flex-basis: 240px;   /* match events/media rail cards on phone */
  }
  .latest-news-2__heading {
    font-size: var(--text-xl);
  }

  /* ---- 3.10f Upcoming Events 3: stack each card vertically ---- */
  .upcoming_events_3 > div {
    grid-template-columns: 1fr;
    grid-template-rows:    auto auto auto auto auto;
    align-items:           start;
    row-gap:               var(--space-1);
  }
  .upcoming_events_3 .eventListImage {
    grid-column:  1;
    grid-row:     1;
    align-self:   stretch;
    aspect-ratio: 16 / 9;
    min-height:   0;
  }
  .upcoming_events_3 .eventListTitle    { grid-column: 1; grid-row: 2; }
  .upcoming_events_3 .eventListDate     { grid-column: 1; grid-row: 3; }
  .upcoming_events_3 .eventListLocation { grid-column: 1; grid-row: 4; }
  .upcoming_events_3 > div::after {
    grid-column:  1;
    grid-row:     5;
    align-self:   start;
    justify-self: start;
    margin-top:   var(--space-3);
  }

  /* ---- 3.10g Latest Media 4: stack each card vertically ---- */
  .latest_media_4 .MediaList > div {
    grid-template-columns: 1fr;
    grid-template-rows:    auto auto auto auto;
    align-items:           start;
    row-gap:               var(--space-1);
  }
  .latest_media_4 .mediaListImage {
    grid-column:  1;
    grid-row:     1;
    align-self:   stretch;
    aspect-ratio: 16 / 9;
    min-height:   0;
  }
  .latest_media_4 .mediaListTitle   { grid-column: 1; grid-row: 2; }
  .latest_media_4 .mediaListSpeaker { grid-column: 1; grid-row: 3; }
  .latest_media_4 .MediaList > div::after {
    grid-column:  1;
    grid-row:     4;
    align-self:   start;
    justify-self: start;
    margin-top:   var(--space-3);
  }

  /* ---- 3.6e Articles List stacked: stack each card vertically ---- */
  .al-cards-stacked > div {
    grid-template-columns: 1fr;
    grid-template-rows:    auto auto auto auto;
    align-items:           start;
    row-gap:               var(--space-1);
  }
  .al-cards-stacked .articleListImage {
    grid-column:  1;
    grid-row:     1;
    align-self:   stretch;
    aspect-ratio: 16 / 9;
    min-height:   0;
  }
  .al-cards-stacked .articleListTitle   { grid-column: 1; grid-row: 2; }
  .al-cards-stacked .articleListSummary { grid-column: 1; grid-row: 3; }
  .al-cards-stacked .articleListLink {
    grid-column:  1;
    grid-row:     4;
    align-self:   start;
    justify-self: start;
    margin-top:   var(--space-3);
  }

  /* Stacked layouts go full width on mobile (the 75% desktop cap is undone) */
  .al-cards-stacked,
  .upcoming_events_3,
  .latest_media_4 .MediaList {
    width: 100%;
  }

  /* Feed Split (3.26): collapse to one column here. The cards already reflow
     to image-on-top via the 3.26 container query as each column narrows, so
     the two columns stay side by side all the way down to this 720 breakpoint
     (was 1024) before stacking. Below 720 the variants' own vertical rules in
     this block take over for the single full-width column. */
  .slice--feed-split { grid-template-columns: 1fr; gap: var(--space-12); }

  /* ---- 3.11b Feature Split: stack to image-on-top (both orientations) ---- */
  .slice--feature-split {
    grid-template-columns: 1fr;
    gap:                   var(--space-8);
  }
  .slice--feature-split--reverse .feature-split__media,
  .slice--feature-split--reverse .feature-split__text {
    order: 0;
  }

  /* ---- 3.11c Article Feature: stack image-on-top (both orientations) ---- */
  :is(.feature_1, .feature_2) > div {
    grid-template-columns: 1fr;
    grid-template-rows:    auto auto auto auto;
    align-items:           start;
    row-gap:               var(--space-4);
  }
  :is(.feature_1, .feature_2) .articleListImage   { grid-column: 1; grid-row: 1; aspect-ratio: 2 / 1; }
  :is(.feature_1, .feature_2) .articleListTitle   { grid-column: 1; grid-row: 2; }
  :is(.feature_1, .feature_2) .articleListSummary { grid-column: 1; grid-row: 3; }
  :is(.feature_1, .feature_2) .articleListLink    { grid-column: 1; grid-row: 4; }

  /* ---- Reveal animation: snappier on mobile so it doesn't lag
     behind quick scrolling. Shorter travel distance, faster
     duration, less stagger between staggered children. The JS
     observer also uses a generous +25% bottom rootMargin so
     animations start before elements enter view -- by the time the
     user sees them they're well underway. ---- */
  :root {
    /* v0.278: retuned against measured arrival timing rather than by feel.
       The observer fires 25% of a viewport early, so on a 720px screen an
       element is still 180px below the fold when its reveal starts. At an
       unhurried thumb scroll of roughly 900px/sec it reaches the fold 200ms
       later and sits comfortably in view around 533ms later. Against the
       previous 600ms duration that left the animation 89% finished before
       anyone looked at it, so only the tail was ever seen. 950ms leaves it
       56% done on arrival, which reads as motion rather than as a flicker.

       Distance up from 12px, which is below the threshold where movement
       registers at arm's length; past about 36px it starts to feel heavy.

       Stagger DOWN, which looks backwards until you account for v0.277:
       every revealed element now has its own trigger, and on a phone
       everything stacks, so items already cascade from their own scroll
       positions. The artificial delay is additive on top of that, and at
       150ms a fourth item waited 450ms after its own trigger before it
       started. 80ms keeps a hint of cascade where items genuinely share a
       row without holding anything back. Note the script reads this token
       once at init, so it reaches the stagger passes too. */
    --reveal-distance: 28px;
    --reveal-duration: 950ms;
    --reveal-stagger:  80ms;
  }

  /* ---- Site Footer: centre everything, stack the bottom strip ---- */
  .site-footer {
    text-align:  center;
    padding-top: var(--space-12);
  }
  /* Top contact area: collapse to single column, centred */
  .site-footer > .site-footer__inner {
    grid-template-columns: 1fr;
    gap:                   var(--space-4);
    justify-items:         center;
  }
  .site-footer__bottom > .site-footer__inner > div {
    justify-content: center;
  }

  /* ---- Social media icons: centre the flex row on mobile.
     The parent .site-footer__stack and .site-footer__inner are
     centred via text-align/justify-items above, but the inner
     .ArticleBody is itself a flex container -- flex children
     ignore text-align, so we need to set justify-content
     explicitly for the icons to centre. ---- */
  .site-footer .social_media_icons .ArticleBody {
    justify-content: center;
  }

  /* ---- Umbrella org logos: tighter spacing + smaller logos on mobile.
     Logos are already centred via the desktop rule (justify-content),
     just shrink the gap and max-height so they fit comfortably on
     a narrow viewport. ---- */
  .umbrella_org_logo {
    padding-block: var(--space-6);
  }
  .umbrella_org_logo .gallery_ul {
    gap: var(--space-6);
  }
  .umbrella_org_logo .gallery_li img {
    max-height: 40px;
    max-width:  180px;
  }
  /* Hubb logo: drop to the bottom of the stack on mobile, full width centre */
  .site-footer__bottom > .site-footer__inner > div > a.footer {
    margin-right: 0;
    width:        100%;
    order:        99;            /* push logo to the very end */
    margin-top:   var(--space-3);
  }
  /* Reset the desktop auto-margins on mobile -- wrapped centered layout
     doesn't want them, they'd create awkward gaps within rows */
  .site-footer__bottom > .site-footer__inner > div > a.footer + a {
    margin-left: 0;
  }
  .site-footer #footer_charity {
    margin-left: 0;
  }
  .site-footer .footer-logo {
    margin: 0 auto;
  }

  /* ---- Articles List: tighter gap on mobile ---- */
  .al-cards { gap: var(--space-6); }

  /* ---- Explore Church gallery: when the grid collapses to single
     column on phones (<540px via the shared rule above), wider
     landscape tiles read better than tall portrait ones. Switch
     aspect ratio so each tile fills the viewport without becoming
     awkwardly tall. Tighter gap matches other modules on mobile. ---- */
  .explore-church { padding-block: var(--space-8); }
  .explore_church .gallery_ul { gap: var(--space-4); }
  .explore_church .gallery_li { aspect-ratio: 16 / 10; }
  .explore-church__title { font-size: var(--text-base); }

  /* ---- Mobile body text readability bumps.
     On phones, --text-sm (~14px) starts to feel cramped for body
     copy -- particularly on news/event/sermon cards where readability
     matters more than density. Bump these specific selectors to
     --text-base (~16px) so content reads comfortably. iOS also
     skips its zoom-on-input-focus behaviour at >=16px, which is a
     bonus for any inline form fields elsewhere. ---- */
  .al-cards .articleListSummary,
  .upcoming_events .eventListDescription,
  .upcoming_events .eventListSummary,
  .latest_media .MediaList .description,
  .latest_media .MediaList .summary {
    font-size:   var(--text-base);
    line-height: 1.5;
  }

  /* ---- Hero Slideshow: TALL portrait-feel hero on mobile, like Aspire's
     site does. The wide landscape image gets cropped via object-fit cover
     so the subject fills the screen rather than being a thin band.
     clamp(min, ideal, max):
       - never less than 380px (substantial even on small landscape phones)
       - aim for 78% of viewport height (immersive -- feels like a real hero)
       - never more than 700px (don't get absurd on small tablets) ---- */
  .hero_slideshow {
    min-height: clamp(380px, 78vh, 700px);   /* match the placeholder */
  }
  .hero_slideshow .bx-viewport,
  .hero_slideshow .gallery_li {
    height: clamp(380px, 78vh, 700px) !important;
  }
  /* Image positioning: bias toward center-top of the source image --
     subjects in landscape photos are usually framed in the upper third,
     so this cropping keeps faces / focal points visible. */
  .hero_slideshow .gallery_li img {
    object-position: center 35%;
  }
  /* Caption side-padding has to clear the arrow buttons. The arrows
     sit at left/right var(--space-2) (8px) and are 40x40 -- they
     extend 8 + 40 = 48px from each edge. v0.143: caption padding
     bumped from var(--space-4) (16px) to var(--space-12) (48px) so
     subtitle text no longer bumps into the arrows on phone widths.
     Arrows reduced from 44 to 40 to keep a touch-friendly target
     while leaving the caption a bit more horizontal room. */
  .hero_slideshow .hero-caption {
    padding: 0 var(--space-12);
  }
  .hero_slideshow .hero-caption__title    { font-size: clamp(2.25rem, 10vw, 4rem); }
  .hero_slideshow .hero-caption__subtitle { font-size: 1.125rem; }
  /* Touch-friendly tap targets at 40x40 (was 44 before v0.143; the
     few px back gives the caption that extra horizontal headroom). */
  .hero_slideshow .bx-prev,
  .hero_slideshow .bx-next {
    width:  40px;
    height: 40px;
  }
  .hero_slideshow .bx-prev { left:  var(--space-2); }
  .hero_slideshow .bx-next { right: var(--space-2); }
  .hero_slideshow .bx-pager { bottom: var(--space-4); }

  /* ---- Event & Media dialogs: switch from side-drawer to bottom-sheet
     on mobile. The brief explicitly asks for "full-screen panel OR
     bottom-sheet style panel" rather than a narrow side drawer.
     Bottom-sheet is the modern app convention for media (Spotify,
     Apple Music, podcast apps), keyboard-reachable on phones because
     the close button is at the TOP of the panel which is the FAR side
     of a thumb stretch -- but the rest of the controls sit lower,
     within easy reach.

     We override the desktop pinned-right rules with !important so we
     beat both jQuery UI's inline styles and our own desktop !important
     declarations. */
  .endis-jquery-ui-dialog .ui-dialog {
    top:           auto !important;
    bottom:        0 !important;
    left:          0 !important;
    right:         0 !important;
    width:         100vw !important;
    max-width:     100vw !important;
    /* 92dvh leaves an 8% strip of dimmed page visible at the top so
       the sheet feels modal-but-not-fullscreen and the user keeps
       context of where they were. dvh respects the iOS URL bar
       collapse/expand so the sheet isn't clipped behind the bar. */
    height:        92vh !important;
    height:        92dvh !important;
    max-height:    92vh !important;
    max-height:    92dvh !important;
    border-radius: var(--radius-lg) var(--radius-lg) 0 0 !important;
    /* shadow on the TOP edge instead of the left, since the sheet
       meets dimmed page at the top, not the left. */
    box-shadow:    0 -16px 48px -12px rgba(26, 22, 18, 0.25) !important;
    /* Slide up from bottom rather than in from right. */
    animation:     dialogSlideUp 280ms cubic-bezier(0.16, 1, 0.3, 1) !important;
  }

  /* Drag-handle visual at the very top of the sheet -- a 40x4 pill
     centred on the top edge. Doesn't actually drag (Hubb's dialog
     doesn't support touch-drag close), but it's the universal visual
     signal "this is a bottom sheet". Helps the panel feel native. */
  .endis-jquery-ui-dialog .ui-dialog::before {
    content:       "";
    display:       block;
    position:      absolute;
    top:           var(--space-2);
    left:          50%;
    transform:     translateX(-50%);
    width:         40px;
    height:        4px;
    border-radius: 2px;
    background:    var(--rule);
    z-index:       11;       /* above the titlebar's z-index: 10 */
  }

  /* Mobile titlebar padding + height. v0.140:
     - Override base.min.css's `height: 46px` (which forced the
       titlebar to a fixed height regardless of our padding).
       v0.138 padding rule appeared to land but was rendered moot
       by the locked height; the close button sat ~16px from the
       popup top vs ~27px from the right (Adam's "still needs
       moving right" report). With height: auto our padding now
       actually creates the titlebar's height.
     - Padding made symmetric (8px top + 8px sides). Close glyph
       now lands ~19px from the popup's top edge AND ~19px from
       the right edge, equidistant.
     - Bottom stays at 12px so the title below sits close to the
       close-button row (same logic as desktop's v0.132 fix). */
  .endis-jquery-ui-dialog .ui-dialog-titlebar {
    height:     auto !important;
    min-height: auto !important;
    padding:    var(--space-2) var(--space-2) var(--space-3) !important;
  }
  .endis-jquery-ui-dialog .ui-dialog-content {
    /* v0.142: drop top padding from 16px to 4px so the title sits
       closer to the close-button row on mobile. Adam reported the
       title sitting too low under the titlebar; the gap was titlebar
       bottom-padding (12px) + content top-padding (16px) = 28px,
       which on a phone-sized panel reads as wasted space. Now 12+4
       = 16px, which still gives breathing room without the title
       drifting into the middle of the panel. Sides + bottom keep
       16px because that's where the dialog content needs room from
       the panel edges. */
    padding: var(--space-1) var(--space-4) var(--space-4) !important;
  }
  /* No mobile overrides needed for either dialog title: both use
     1.2em (em-based) as of v0.145, so they scale naturally with
     the parent body font-size which is itself responsive. */
  .calendar-event-dialog .event-summary-image,
  .media-player .media-summary-image {
    max-height: 220px;
  }
  /* Tighter file row padding on mobile -- but keep the 12px (--radius-lg)
     radius from desktop so the cards still read as cards. */
  .endis-jquery-ui-dialog .media-player .media-file {
    padding:    var(--space-3) var(--space-4) !important;
    gap:        var(--space-3);
    min-height: 64px;
  }

  /* ---- Empty states: tighter padding on phone ---- */
  .empty-state {
    padding: var(--space-8) var(--space-4);
  }
  .empty-state__icon {
    width:     40px;
    height:    40px;
    font-size: 20px;
  }

  /* ---- Forms: full-width on mobile, 16px font to defeat iOS zoom ---- */
  .hubb-form {
    max-width: 100%;
  }
  .hubb-form input[type="text"],
  .hubb-form input[type="email"],
  .hubb-form input[type="tel"],
  .hubb-form input[type="url"],
  .hubb-form input[type="password"],
  .hubb-form input[type="number"],
  .hubb-form input[type="date"],
  .hubb-form input:not([type]),
  .hubb-form textarea,
  .hubb-form select {
    /* Min font-size 16px prevents iOS Safari zoom-on-focus, which is
       a notorious mobile UX trap that resets the user's pan/zoom every
       time they tap a field. */
    font-size: 16px;
  }
  .hubb-form button[type="submit"],
  .hubb-form input[type="submit"] {
    /* Full-width on mobile so the primary action is unmissable */
    align-self: stretch;
    text-align: center;
  }

  /* ---- Error pages: less aggressive vertical centring on small
     screens, where 60vh feels weirdly tall ---- */
  .page-error,
  .page-not-found,
  .error-page {
    min-height: auto;
    padding:    var(--space-12) var(--space-4);
  }
  .page-error__actions {
    flex-direction: column;
    align-items:    stretch;
    width:          100%;
    max-width:      280px;
  }
  .page-error__actions a {
    justify-content: center;
    text-align:      center;
  }

  /* ---- Giving Advert (3.11d): stack the card -- photo on top, panel below.
     The background image stays absolute (100vw) behind everything; only the
     photo restacks. The photo goes position:relative (in-flow band) so the
     desktop absolute <img> still fills it -- no static-img workaround. ---- */
  .slice--giving-advert .ArticleBody {
    display:     block;
    min-height:  0;
    padding:     clamp(var(--space-6), 6vw, var(--space-8));
    padding-top: 0;
  }
  .slice--giving-advert .ArticleBody > div:nth-of-type(1) {
    position: relative;
    width:    auto;
    height:   clamp(180px, 45vw, 260px);
    /* negative inline margins bleed the photo to the card edges, above the padded copy */
    margin:   0 calc(-1 * clamp(var(--space-6), 6vw, var(--space-8)))
                 clamp(var(--space-6), 6vw, var(--space-8));
    border-radius: var(--radius-lg) var(--radius-lg) 0 0;
  }
  .slice--giving-advert .ArticleBody :is(h1, h2) {
    font-size: clamp(1.7rem, 7vw, 2.2rem);
  }
  .slice--giving-advert .ArticleBody td:first-child {
    font-size: clamp(1.5rem, 8vw, 2rem);
  }

  /* ---- Hero Banner (3.7c): shorter band, flatter scrim, full-width buttons ---- */
  .slice--hero-banner {
    min-height: clamp(380px, 68vh, 520px);
  }
  .slice--hero-banner::before {
    background: linear-gradient(180deg, rgba(0,0,0,0.5), rgba(0,0,0,0.55));
  }
  .slice--hero-banner .ArticleBody > * { max-width: none; }
  .slice--hero-banner .ArticleBody p { max-width: none; }
  .slice--hero-banner .ArticleBody :is(h1, h2) { font-size: clamp(2rem, 8vw, 2.6rem); }
  .slice--hero-banner .ArticleBody p:has(a) a { flex: 1 1 auto; justify-content: center; }
}

/* -------------------------------------------------------------------------
   4c. Cards: explicit grid breakpoints (4 / 2 / 1, never 3+1)
   Applied to .al-cards (Articles), .upcoming_events (Calendar), and
   .latest_media .MediaList (Sermons / Media List). Three modules
   share the same 4->2->1 collapse so module breakpoints stay in
   sync. (.explore_church is 2-up at desktop already and only needs
   the single-column collapse at the bottom breakpoint.)
   ------------------------------------------------------------------------- */
@media (max-width: 1024px) {
  .al-cards,
  .upcoming_events,
  .latest_media .MediaList,
  .shop-cards { grid-template-columns: repeat(2, 1fr); }
  /* Shop featured-categories: 3-up needs ~340px per tile; below 1024
     drop to 2-up (the third tile wraps to a second row). */
  .shop_featured_categories .gallery_ul { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* Shop sidebar layout: stack -- categories panel on top, content
     below (matches the reference site's mobile behaviour). Sticky
     comes off; a sticky full-width panel would cover the content. */
  .shop-layout { grid-template-columns: 1fr; gap: var(--space-8); }
  .shop-sidebar { position: static; }
  /* Checkout action buttons stack full-width on phones. */
  .shop-checkout [id$="_pnlButtons"] { flex-direction: column; align-items: stretch; }
  .shop-checkout [id$="_pnlButtons"] input[type="submit"] { width: 100%; }
  .shop-checkout input[name$="btnSaveDraft"] { order: 0; }   /* Checkout Now first on phones */
  /* explore_church_2 collapses from 4-up to 2x2 -- the 4-up needs
     ~310px per card to read well, which gets cramped below 1024. */
  .explore_church_2 .gallery_ul { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  :is(.explore_church_3, .explore_church_4) .gallery_ul { grid-template-columns: repeat(2, minmax(0, 1fr)); }

  /* ---- Testimonials: tighten the quote and controls on phones. ---- */
  .slice--testimonials .testimonial__quote { padding-top: var(--space-8); }
  .slice--testimonials .testimonial__quote::before { font-size: 3rem; }
  .slice--testimonials .testimonial__quote p { font-size: var(--text-base); }
  .slice--testimonials .testimonial-nav { gap: var(--space-4); margin-top: var(--space-6); }
  .slice--testimonials .testimonial-nav__btn { width: 2rem; height: 2rem; }
  .slice--testimonials .testimonial-nav__btn svg { width: 18px; height: 18px; }
  .slice--testimonials .testimonial-nav__btn--playpause svg { width: 15px; height: 15px; }
  .slice--testimonials .testimonial-nav__dot { width: 9px; height: 9px; }
}
@media (max-width: 540px) {
  .al-cards,
  .upcoming_events,
  .latest_media .MediaList,
  .explore_church .gallery_ul,
  .explore_church_2 .gallery_ul,
  .explore_church_3 .gallery_ul,
  .explore_church_4 .gallery_ul,
  .shop_featured_categories .gallery_ul,
  .shop-cards { grid-template-columns: 1fr; }
  /* Product detail collapses to a single column (image over buy box). */
  .shop-item {
    grid-template-columns: 1fr;
    grid-template-areas: "gallery" "buy" "detail";
    gap: var(--space-6);
  }
  .shop-item__gallery { max-width: 300px; }
}

/* 3-up Articles List: hold three across all the way down to the phone
   breakpoint, then go straight to one column. No 2-column step, because
   three items in two columns is the 2 + 1 orphan the block above avoids.
   Two class selectors so this beats the .al-cards rules above regardless of
   source order. */
@media (max-width: 720px) {
  .al-cards.al-cards-3up { grid-template-columns: 1fr; }
}