/*
 * animations.css — FRONT-END ONLY (not loaded in the Bricks builder)
 * ──────────────────────────────────────────────────────────────────────────
 * Every CSS rule tied to the animation system lives here:
 *   • "initial states" that hide / dim / pre-transform elements before the
 *     JavaScript reveals or animates them,
 *   • layout that only exists to support an animation (carousel, gallery
 *     stacking, injected overlays), and
 *   • the pure-CSS motion / hover effects.
 *
 * WHY IT'S SEPARATE FROM style.css:
 * functions.php enqueues this file INSIDE the builder-guarded loader
 * (theme_gsap_script()), so it loads ONLY on the live front-end — never in the
 * Bricks editor. The animation JavaScript also doesn't run in the editor, so if
 * these "start hidden" rules loaded there, animated elements (hero lines,
 * load-blur-words headings, offering images, etc.) would sit invisible/dimmed and
 * you couldn't see them while building the page. Keeping them here lets the
 * editor show everything normally while the live site still gets the full
 * animated treatment.
 *
 * Static design (typography, fonts, colours, scrollbar) lives in style.css,
 * which loads everywhere including the editor.
 */

/* ─── load-blur-words.js initial states ──────────────────────────────────── */

.load-blur-words {
    visibility: hidden;
}
.load-blur-words.animated {
    visibility: visible;
}
.load-blur-words .word {
    display: inline-block;
}

/* ─── load-fade-in.js initial state ──────────────────────────────────────── */

.load-fade-in {
    opacity: 0;
}

/* ─── Work grid (hover reveal) ───────────────────────────────────────────── */

.work-bg {
    opacity: 0.15;
    filter: grayscale(100%);
    transition: opacity 0.3s ease, filter 0.3s ease;
}

.work-box:hover + .work-bg,
.work-box:hover .work-bg {
    opacity: 0.9;
    filter: grayscale(0%);
}

/* ─── Glow button (cursor-follow radial glow on hover) ───────────────────── */

.glow-btn::before {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: radial-gradient(circle at var(--x, 50%) var(--y, 50%), rgba(255, 255, 255, 0.1), transparent 100%);
    opacity: 0;
    transition: opacity 0.3s ease;
    pointer-events: none;
}

.glow-btn:hover::before {
    opacity: 1;
}

/* ─── Shimmer text ───────────────────────────────────────────────────────── */

.shimmer-element {
    background: linear-gradient(
        90deg,
        white 10%,
        #505153 20%,
        white 30%
    );
    background-size: 200% 100%;
    background-position: 100%;
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
}

.shimmer-element.shimmer-hover {
    animation: shimmer-once 6s ease-out forwards;
}

@keyframes shimmer-inf {
    0%   { background-position: 100% 0; }
    75%  { background-position: -100% 0; }
    100% { background-position: -100% 0; }
}

@keyframes shimmer-once {
    0%   { background-position: 70% 0; }
    25%  { background-position: -100% 0; }
    100% { background-position: -100% 0; }
}

/* ─── Scrolling box ──────────────────────────────────────────────────────── */

.scrolling-box {
    transform: translateY(20px);
    will-change: transform;
    transition: transform 0.05s linear;
}

@media (prefers-reduced-motion: reduce) {
    .scrolling-box {
        transform: none !important;
    }
}

/* ─── VSL video wrapper (animated gradient bg + matching gradient glow) ───── */

/* The animated multi-radial gradient and an OUTER GLOW that is the same gradient,
   blurred and bleeding past the wrapper's edges (::before, behind everything).
   The gradient stack is held in --vsl-grad so the wrapper and the glow stay identical.
   NOTE: the glow halo extends OUTSIDE the box, so .vsl-video-wrapper (and its ancestors)
   must NOT clip overflow — put any overflow:hidden / border-radius clipping on an INNER
   element (e.g. the video itself), or the glow gets cut off. */
.vsl-video-wrapper {
    --vsl-grad:
        radial-gradient(at 0% 100%,  hsla(34,100%,79%,1) 0px, transparent 50%),
        radial-gradient(at 100% 100%, hsla(195,80%,90%,1) 0px, transparent 50%),
        radial-gradient(at 49% 100%, hsla(322,42%,84%,1) 0px, transparent 50%),
        radial-gradient(at 19% 56%,  hsla(4,86%,71%,1)  0px, transparent 50%),
        radial-gradient(at 78% 58%,  hsla(247,38%,61%,1) 0px, transparent 50%);

    position: relative;
    background: var(--vsl-grad);
    background-size: 200% 200%;
    animation: vsl-gradient 15s linear infinite;
}

.vsl-video-wrapper::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;                 /* sit behind the wrapper's content (the video) */
    border-radius: inherit;      /* match the wrapper's rounding, if any */
    background: var(--vsl-grad);
    background-size: 200% 200%;
    animation: vsl-gradient 15s linear infinite;
    filter: blur(45px);          /* the glow — bigger blur = softer/wider halo */
    opacity: 0.75;               /* glow strength */
    pointer-events: none;
}

@keyframes vsl-gradient {
    0%   { background-position: 0% 70%; }
    50%  { background-position: 60% 50%; }
    100% { background-position: 0% 70%; }
}

/* Honour reduced-motion: keep the gradient + glow, drop the looping animation. */
@media (prefers-reduced-motion: reduce) {
    .vsl-video-wrapper,
    .vsl-video-wrapper::before {
        animation: none;
    }
}

/* ─── scrub-blur-in.js initial states ────────────────────────────────────── */

.scrub-blur-chars {
    visibility: hidden;
}
.scrub-blur-chars.ready {
    visibility: visible;
}
.scrub-blur-chars .char {
    display: inline-block; /* required for per-char transform and filter to work */
}
.scrub-blur-in,
.scrub-scale-in {
    opacity: 0;
}

/* ─── scrub-fade-in.js / scrub-fade-out.js (opacity-only scrub) ───────────── */

/* fade-in starts hidden so it doesn't flash fully visible before the scrubbed fromTo first
   renders (JS gsap.set()s the start opacity, then scrubs to 1). fade-out has no initial state —
   it starts at its natural visible opacity (same as .scrub-blur-out). */
.scrub-fade-in {
    opacity: 0;
}

/* ─── scrub-darken.js (scroll-scrubbed black overlay) ────────────────────── */

/* .scrub-darken hosts a JS-injected black overlay (GSAP can't animate ::before/::after);
   position:relative anchors that overlay to the element. The overlay starts clear and GSAP
   scrubs its opacity up to data-opacity; pointer-events:none keeps content beneath clickable,
   z-index:2 lays it over the element's normal-flow content. */
.scrub-darken {
    position: relative;
}
.scrub-darken-overlay {
    position: absolute;
    inset: 0;
    background-color: var(--ra-surface-black);
    pointer-events: none;
    z-index: 2;
}

/* ─── rise-up-text.js initial states ─────────────────────────────────────── */

.rise-up-text {
    visibility: hidden;   /* hidden until JS splits + .ready reveals (no flash) */
}
.rise-up-text.ready {
    visibility: visible;
}
/* The overflow mask itself needs no CSS — SplitText's mask:"lines" wraps each line
   in an element with overflow:clip applied inline. Lines are block-level (SplitText
   sets their display), so no inline-block helper is needed. */

/* ─── reveal-divider.js initial state ────────────────────────────────────── */

/* Collapsed before the JS reveals it (scaleX 0→1); JS sets transform-origin per data-origin. */
.reveal-divider .line,
.reveal-divider .elementor-divider-separator {
    transform: scaleX(0);
}

/* ─── scrub-border.js initial state ──────────────────────────────────────── */

/* Hidden until JS sets the compressed start state and adds .ready — without this the box
   (at the top of the page) flashes at full size before the scrubbed fromTo first renders.
   background-color = the frosted INITIAL state GSAP darkens from; no !important (that would beat
   GSAP's inline write and freeze the darken). Don't set a background on this element in Bricks —
   let this rule own it. blur(16px) is a constant frosted-glass backdrop (not animated). */
.scrub-border-main {
    visibility: hidden;
    background-color: var(--ra-border-bg-start);
    -webkit-backdrop-filter: blur(var(--ra-border-blur));
    backdrop-filter: blur(var(--ra-border-blur));
}
.scrub-border-main.ready {
    visibility: visible;
}

/* ─── scrub-header.js initial state ──────────────────────────────────────── */

/* Hidden until JS sets the start state (narrower + lowered) and adds .ready — without this
   the header flashes at its final width/position before the scrubbed fromTo first renders. */
.scrub-header {
    visibility: hidden;
}
.scrub-header.ready {
    visibility: visible;
}

/* ─── Image gallery z-depth (depth-gallery.js) ───────────────────────────── */

.depth-img-1 { filter: blur(2px); }
.depth-img-2 { filter: blur(1px); }
.depth-img-4 { filter: blur(3px); }

.gsap-overlay {
    position: absolute;
    inset: 0;
    background-color: var(--ra-surface-overlay);
    pointer-events: none;
    z-index: 1;
}

/* ─── Offerings scroll cycler (offerings.js) ─────────────────────────────── */

.offering-img-wrapper {
    display: grid;
}
.offering-img-wrapper > * {
    grid-area: 1 / 1;   /* stack all image widgets in the same cell */
}
.offering-img {
    opacity: 0;          /* JS fades in the active slide */
}

/* ─── Drag scroller (drag-scroller.js) ───────────────────────────────────── */

/* The scroller is the masked viewport. Its `overflow` is set in BRICKS, not here —
   use `hidden`, or `clip` if the cards have glows/shadows that need to bleed past
   the edge (clip pairs with overflow-clip-margin). Do NOT use `overflow-x: hidden`
   alone: that forces overflow-y to auto and you get a stray vertical scrollbar.
   user-select:none stops text being selected mid-drag. */
.drag-scroller {
    user-select: none;
    -webkit-user-select: none;
}

/* The track must be free to extend past the scroller — NO overflow here. GSAP owns
   its translateX, so no CSS transition / Bricks Interaction on transform. Draggable
   swaps `grab` → `grabbing` automatically during a drag. */
.drag-track {
    will-change: transform;
    cursor: grab;
}

/* data-drag="off" is a pure auto-scroll marquee — nothing to grab, so don't
   advertise a grab cursor. */
.drag-scroller[data-drag="off"] .drag-track {
    cursor: default;
}

/* Items are static passengers the track carries, and they must never compress:
   a flex item that shrinks changes the measured set width / bounds and the loop
   (or the end stop) lands in the wrong place. Widths themselves stay in Bricks —
   unlike the testimonial cards, items here may be different widths. */
.drag-track > * {
    flex-shrink: 0;
}

/* THE CROP, HALF ONE OF TWO — the opt-in parallax (data-parallax on the scroller).
   Identical mechanism to .drag-canvas-content below, and the same two-halves rule:
   this standalone `scale` PROPERTY covers the window before the script runs, then
   drag-scroller.js clears it and re-applies the same number through GSAP's transform
   so the crop and the parallax x have ONE owner. Delete this and the media pops from
   uncropped to cropped on load; delete the JS clear and the two multiply.

   It is scoped inside .drag-scroller so `.drag-content` stays a scroller-local tag —
   the element it goes on may be the media itself or a wrapper around it. Rows without
   data-parallax simply never move it. */
.drag-scroller .drag-content {
    scale: var(--ra-scroller-scale);
}

/* ─── Drag canvas (drag-canvas.js) ───────────────────────────────────────── */

/* THE CROP, HALF ONE OF TWO. This is the standalone `scale` PROPERTY (not a
   transform), and it exists to cover the window before the script runs — without it
   every image on the page pops from uncropped to cropped on load. drag-canvas.js
   then clears it (`content.style.scale = 'none'`) and writes the same number into
   GSAP's transform matrix instead, so scale and the parallax x/y have ONE owner.

   BOTH HALVES ARE REQUIRED AND THEY ARE NOT INTERCHANGEABLE:
     • Delete this rule → the load pop comes back.
     • Delete the JS clear → the two scales MULTIPLY (1.25 × 1.25 = 1.5625), because
       the `scale` property is applied on top of the `transform` property, not
       instead of it. That also silently invalidates the parallax travel budget.

   It lives in animations.css (front-end only) on purpose: the Bricks EDITOR then
   shows the media uncropped while you compose. That costs nothing in accuracy —
   `scale` is visual, so the item's layout box is identical either way.

   The number is a token so this rule and the script can't drift apart. */
.drag-canvas-content {
    scale: var(--ra-canvas-scale);
}

/* The mask is the trigger, so Draggable swaps grab → grabbing on it during a drag
   (cursor / activeCursor in the script). The resting `grab` is here so the affordance
   is right before the script loads.

   `cursor: inherit` on descendants is deliberate: the items are usually links, and
   <a>'s cursor:pointer would otherwise break the affordance up into patches across a
   surface whose primary gesture is dragging. Drop this rule to get pointer back on
   links. user-select stops text being selected mid-drag. */
.drag-canvas {
    cursor: grab;
    user-select: none;
    -webkit-user-select: none;
}
.drag-canvas * {
    cursor: inherit;
}

/* data-parallax="0" turns the effect off but the crop stays — the media is still
   scaled, so the bleed is simply unused. Nothing to declare here; noted so the
   token's role isn't mistaken for parallax-only. */

/* ─── Work showcase (work-showcase.js) ───────────────────────────────────── */

/* The card's STATIC geometry — the crop box, the stage and the thumbnail sizing — is
   in style.css, so the Bricks editor renders a card correctly while you compose it.
   What's here is only what must NOT reach the editor: the resting states of an
   interaction the editor can't run.

   These transitions are fine, and don't contradict the "GSAP owns opacity" rule: this
   component's GSAP writes go to the two stage layers, never to a thumbnail. */
.work-showcase-thumbnails-wrapper img {
    opacity: var(--ra-showcase-thumb-rest);
    transition: opacity 0.2s ease;
}
.work-showcase-thumbnails-wrapper img.is-active {
    opacity: 1;
}

/* THE REVEAL. Visible by default — that is the TOUCH case, where there is no hover and
   a hover-gated strip would simply never appear. Hover devices take the opposite
   default inside the query, so neither case needs a width breakpoint (this build's
   Bricks breakpoint is 600px while the theme's -mobile attributes switch at 768px;
   asking about hover instead sidesteps the mismatch entirely). */
@media (hover: hover) and (pointer: fine) {
    .work-showcase-thumbnails-wrapper {
        opacity: 0;
        visibility: hidden;
        transition: opacity 0.3s ease, visibility 0.3s;
    }

    /* :not(.is-dragging) is what stops the strip strobing mid-drag: the pointer stands
       still while the cards slide underneath it, so without this every card passing the
       cursor would flash its strip open and shut. drag-scroller.js holds the class for
       the whole gesture, throw included.

       `visibility` (not opacity alone) is load-bearing for a second reason — a hidden
       strip is not hit-tested, which is what keeps work-showcase.js from swapping shots
       as thumbnails slide past a parked cursor. Don't reduce this to opacity. */
    .drag-scroller:not(.is-dragging) .work-showcase-image-wrapper:hover .work-showcase-thumbnails-wrapper {
        opacity: 1;
        visibility: visible;
    }
}

@media (prefers-reduced-motion: reduce) {
    .work-showcase-thumbnails-wrapper,
    .work-showcase-thumbnails-wrapper img {
        transition: none;
    }
}

/* ─── Testimonial carousel (testimonial-scroller.js) ─────────────────────── */

/* The section is the ONLY element with overflow:hidden — it's the masked viewport
   that clips the rows extending beyond it. user-select:none stops text from being
   selected while dragging. */
.testimonial-section {
    user-select: none;
    -webkit-user-select: none;
}

/* The row must be free to extend past the section — NO overflow here. GSAP owns the
   translateX, so suppress any builder transition on it and hint the GPU. Draggable
   swaps `grab` → `grabbing` automatically during a drag. */
.testimonial-row {
    will-change: transform;
    cursor: grab;
}

/* Cards never compress (flex-shrink:0 + min-width) — they're static passengers the
   row carries. Keep all cards the same width so the cloned sets align seamlessly. */
.testimonial-card {
    width: 33vw;
    min-width: 33vw;
    flex-grow: 0;
    flex-shrink: 0;
}

/* Hover-dim (desktop only, ≥769px): hovering ONE card darkens every OTHER card with a black overlay; stop
   hovering and they all clear. By default NO card is dimmed (overlay opacity:0) — the
   dim only appears once some card is hovered. Pure CSS — :has() detects any hovered
   card, :not(:hover) spares the one under the cursor. Works on the cloned cards too
   (no listeners to break, unlike testimonial-readmore.js).

   The overlay is a ::after pseudo at z-index:20 so it sits above the card's content
   (text / image / read-more button), darkening the whole card; pointer-events:none
   keeps that button clickable. position:relative anchors it; border-radius:inherit
   matches the card's rounded corners. The overlay's opacity is CSS-owned; the card's
   GSAP-owned drag-press scale (testimonial-scroller.js) is a TRANSFORM, so the two
   never compete. During auto-scroll the "hovered" card hands off as cards slide under a
   stationary cursor — by design. */
.testimonial-card {
    position: relative;
}
.testimonial-card::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 20;                   /* above the card's inner content, so it dims everything */
    pointer-events: none;          /* keep the read-more button clickable through it */
    border-radius: inherit;        /* match the card's rounded corners */
    background: var(--ra-scrim-card);
    opacity: 0;                    /* no dim until a sibling card is hovered */
    transition: opacity 0.25s ease;
}
/* Desktop only (≥769px): on touch a tap leaves a sticky :hover that would dim the OTHER
   cards until you tap elsewhere — unwanted on mobile. Gating activation to ≥769px (the same
   breakpoint as the gallery overlays) means the overlay never lights up on mobile; the
   ::after still exists (opacity:0, non-interactive) but does nothing. */
@media (min-width: 769px) {
    .testimonial-section:has(.testimonial-card:hover) .testimonial-card:not(:hover)::after {
        opacity: 1;
    }
}
@media (prefers-reduced-motion: reduce) {
    .testimonial-card::after {
        transition: none;   /* overlay snaps in/out instead of fading */
    }
}


/* Read more / less (testimonial-readmore.js). Front-end only (animations.css), so the Bricks
   editor shows the full, editable text. The paragraph is clamped to 4 lines with an automatic
   "…"; .expanded (toggled by the JS on click) removes the clamp. Change the 4 to adjust. */
.testimonial-para {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 4;
    line-clamp: 4;
    overflow: hidden;
}
.testimonial-card.expanded .testimonial-para {
    -webkit-line-clamp: unset;
    line-clamp: unset;
    overflow: visible;
}
.testimonial-readmore-btn {
    cursor: pointer;
}

/* ─── Testimonial gallery overlays (.testimonial-gallery-wrapper / -image) ────
   Front-end only, pure CSS, no GSAP — so these CSS filters/transitions fight
   nothing (none of these classes are GSAP-animated; don't also tag them with a
   GSAP filter effect like .depth-img-N or the two will clash). The whole block is
   gated to ≥769px, so NOTHING shows ≤768px (the theme's mobile breakpoint).

   The image is a Bricks Image widget = a raw <img>, which can't host an overlay
   child OR ::before/::after (replaced element). So the "0.5 black overlay that
   clears on hover" is done with filter: brightness(): compositing black at alpha
   0.5 over a pixel yields pixel × (1 − 0.5) = pixel × 0.5 — exactly brightness(0.5).
   Identical result, no overlay element, works on the bare <img>. Hover → brightness(1).

   The wrapper IS a container, so its static bottom gradient (unchanged on hover)
   uses a ::before; pointer-events:none keeps hover reaching the <img> beneath.
   Needs position:relative — set here; harmless unless the wrapper was positioned
   absolute/fixed in Bricks (a plain "wrapper" is normally static/relative). */
@media (min-width: 769px) {
    .testimonial-gallery-image {
        filter: brightness(0.5);            /* ≡ an rgba(0,0,0,0.5) black overlay */
        transition: filter 0.3s ease;
    }
    .testimonial-gallery-image:hover {
        filter: brightness(1);              /* overlay "goes away" on hover */
    }

    .testimonial-gallery-wrapper {
        position: relative;
    }
    .testimonial-gallery-wrapper::before {
        content: "";
        position: absolute;
        inset: 0;
        z-index: 10;
        pointer-events: none;
        /* Alpha-0 stops use the surface RGB (not the `transparent` keyword) so the fade
           never drifts toward grey mid-gradient in browsers that interpolate un-premultiplied. */
        background: linear-gradient(
            to right,
            rgba(var(--ra-surface-rgb), 0) 0%,
            rgba(var(--ra-surface-rgb), 0) 65%,
            rgba(var(--ra-surface-rgb), 1) 100%
        );
    }
}

/* ─── Button hover (gradient blur-in, hover-animations.js) ───────────────── */

.btn-gd {
    opacity: 0;   /* hidden until .btn-primary hover; hover-animations.js blurs + fades it in */
}

/* ─── Expand-out (scrub-expand-out.js) ───────────────────────────────────── */

/* ─── scrub-slide-in.js initial state ────────────────────────────────────── */

/* Hidden until JS sets the offset start state and adds .ready — without this the element would
   flash at its natural position (and clarity/opacity) before the scrubbed fromTo first renders. */
.scrub-slide-in {
    visibility: hidden;
}
.scrub-slide-in.ready {
    visibility: visible;
}

/* ─── Hero entrance (load-hero.js runner + load-hero-<variant>.js) ───────── */

/* Hidden on load so they don't flash before the JS sequence reveals them. load-hero-vault.js
   splits each line into masked words and rises them up (transform only — the rise-up-text.js
   principle), all lines at once. (.hero-tail is no longer part of the intro, so it's not hidden
   here.) The runner reveals these instantly on reduced-motion, and on any page that carries no
   hero-variant class at all — so an untagged page is never left stuck behind these rules.

   ADDING A VARIANT THAT USES DIFFERENT MARKUP? Scope its initial-state rules to its own class
   (e.g. `.load-hero-2 .hero-headline { visibility: hidden }`), never bare selectors. An
   unscoped rule ships to every page, including ones where that variant never runs to undo it.
   These four stay unscoped because they're the theme's shared hero-line vocabulary and the
   runner's reveal path covers them. */
.hero-1,
.hero-2,
.hero-3,
.hero-4 {
    visibility: hidden;
}
.hero-word {
    display: inline-block;        /* required for the per-word rise transform */
}

/* .scrub-header-wrapper fades in as the load-hero RUNNER's chrome beat (runner-owned, not
   per-variant: this rule hides the header on EVERY page, so something must always reveal it) —
   start it hidden so it doesn't flash before the fade. On reduced-motion, or on a page with no
   hero-variant class, the runner sets it visible instantly. */
.scrub-header-wrapper {
    opacity: 0;
}

/* Mobile-only header scroll background. scrub-header.js toggles .is-scrolled on the WRAPPER
   (full-width) once scrolled past data-bg-distance px (default 140). Scoped to ≤768px so it's
   mobile-only. The transition is narrowed to the two colour props ON PURPOSE, so only the
   background/border fade — opacity (owned by load-hero's GSAP fade) and width/transform aren't
   listed and stay un-transitioned. Needs a sticky/fixed header for this to be visible. */
@media (max-width: 768px) {
    .scrub-header-wrapper {
        border-bottom: 1px solid transparent;
        transition: background-color 0.3s ease, border-bottom-color 0.3s ease;
    }
    .scrub-header-wrapper.is-scrolled {
        background-color: var(--ra-surface-black);
        border-bottom-color: var(--ra-hairline);
    }
}

/* .hero-video and .blur-logo-bg-wrapper fade in mid-intro as part of the runner's chrome beat
   (at the variant's EXTRAS_START) — start them hidden so they don't flash before the fade. GSAP
   owns the opacity. Front-end only, so the Bricks editor still shows them; on reduced-motion, or
   on a page with no hero-variant class, the runner reveals them instantly. */
.hero-video,
.blur-logo-bg-wrapper {
    opacity: 0;
}

/* ─── load-hero-daven.js — preloader + hero (variant-scoped) ─────────────── */

/* SCOPED WITH :has() ON PURPOSE. The preloader is a full-page curtain, so its section is a
   SIBLING of the hero, not a descendant — `.load-hero-daven .preloader-number` would never
   match. `body:has(.load-hero-daven)` reads as "this page runs the daven intro", which is
   exactly the condition these initial states belong to.

   The counter starts hidden so the placeholder text Bricks renders can't flash before the JS
   parks the digits below their masks — which can be up to 3s, since the runner waits on
   document.fonts.ready first. load-hero-daven.js reveals it as it builds. */
body:has(.load-hero-daven) .preloader-number {
    opacity: 0;
}

/* The columns below are JS-INJECTED by load-hero-daven.js — Bricks carries only the single
   `.preloader-number` element. Styling them here (rather than in Bricks) keeps the structure
   out of the builder and out of reach of Bricks' ID-level CSS. Left unscoped because these
   classes exist ONLY when that script has injected them, so the rules match nothing anywhere
   else — same as `.hero-word` and `.mobile-menu-item-mask`. */
.preloader-number-row {
    display: flex;
    justify-content: center;
    font-variant-numeric: tabular-nums;   /* equal-width digits, so the columns can't jitter as
                                             the number changes. If your font has no tabular
                                             figures, give .num-wrap a fixed width instead. */
}
.num-wrap {
    display: grid;        /* stacks both digits in ONE cell, so they overlap exactly */
    overflow: clip;       /* ← the mask. clip over hidden: it creates no scroll container and
                             doesn't interfere with sticky ancestors. */
    text-align: center;

    /* Paint room on the SIDES ONLY, so the mask doesn't shave the digits.
       A digit's box is its ADVANCE width, and ink needn't fit inside it — negative
       `letter-spacing` narrows the advance directly (.big-text sets -4%), and condensed faces
       have tight side bearings. Clipping at the box edge then trims the glyph and individual
       numbers look subtly cut. The padding widens the clip region; the equal negative margin
       gives the space back, so the columns sit exactly where they did.

       Deliberately NOT `overflow-clip-margin` (which the character windows use): that expands
       the clip on EVERY side, and the vertical edge here IS the mask — the digit hides exactly
       one box-height below it, so any vertical paint room un-hides the top of the parked digit.
       Horizontal-only is the whole point. For the same reason, never add vertical padding here. */
    padding: 0 0.06em;
    margin: 0 -0.06em;
}
.num-digit {
    grid-area: 1 / 1;

    /* This is what makes the mask actually mask. The digit travels exactly one box-height
       (yPercent 100) to hide behind .num-wrap's clip edge, so the box must be TALLER than the
       glyph's ink — otherwise the digit lands flush with the clip edge and sub-pixel rounding
       leaves a hairline of it showing. Digits have no descenders, so 1.2 clears comfortably.

       ONLY EFFECTIVE ALONGSIDE the inline `text-box: normal` that load-hero-daven.js sets on
       each digit: a cap-height trim collapses the box to the ink and neutralises line-height
       entirely, so this declaration would do nothing on its own. The two belong together —
       don't remove one without the other.

       Fixing the mask here rather than by over-travelling (yPercent 120) is deliberate: the two
       digits have to move in lockstep to read as one strip, and any travel beyond 100% opens a
       gap between the outgoing and incoming digit that flickers through the window mid-roll. */
    line-height: 1.2;
}

/* NO RULE FOR .ra-twin-char — deliberately. The twin-swap window's position/display/overflow
   are set INLINE by core/utils.js → RAnim.twinSwap, and must stay there.

   These elements are created by that script, so there is no pre-JS window for a stylesheet to
   cover — and a stylesheet is the weaker place to own them anyway. A project-level reset like
   `*:not(#some-id) { … }` carries ID specificity (1,0,0) and silently outranks any class rule
   here; an inline style doesn't lose that fight. The class name is kept purely as a hook for
   inspecting the DOM. See that function for why the window clips on BOTH axes. */

/* .hero-bg (the hero background video) blurs + fades in as the last beat of the daven intro.
   This variant owns BOTH properties (it is not part of the runner's chrome), so the from-state
   here MIRRORS the tween's — keep the blur equal to BG_BLUR in load-hero-daven.js. Scoped to
   the variant class: a .hero-bg on any other page is untouched. */
.load-hero-daven .hero-bg {
    opacity: 0;
    filter: blur(24px);
}

/* ─── mobile-menu.js initial states ──────────────────────────────────────── */

/* The fixed wrapper starts hidden + non-interactive so it can't capture taps over its
   area while closed; mobile-menu.js reveals it (autoAlpha) only while the menu is open.
   The wrapper being hidden also hides the panel, so there's no flash before JS parks it —
   we deliberately do NOT set transform here: GSAP owns it entirely (mobile-menu.js parks
   .mobile-menu-main at yPercent:100). A CSS translateY(100%) would STACK with GSAP's
   yPercent (GSAP parses the CSS px translate into its own y), leaving the panel 200% down
   and off-screen even when open. Front-end only — the Bricks editor keeps showing the menu
   editable. */
.mobile-menu-wrapper {
    visibility: hidden;
    opacity: 0;
    background-color: rgba(0, 0, 0, 0);            /* dim overlay — transparent at rest */
    /* Fade ONLY the overlay colour. opacity + visibility stay GSAP-owned (autoAlpha),
       so they're deliberately NOT in this transition list — same narrowing trick as
       .scrub-header-wrapper's mobile scroll background. Matches the 0.5s panel slide. */
    transition: background-color 0.5s ease;
}
.mobile-menu-wrapper.is-open {
    background-color: var(--ra-scrim-menu);          /* mobile-menu.js toggles .is-open */
}
.mobile-menu-main {
    /* Frosted-glass blur of whatever sits behind the panel. Only visible if the panel's
       own background is (semi-)transparent — an opaque bg paints over the blur. */
    -webkit-backdrop-filter: blur(8px);
    backdrop-filter: blur(8px);
}

/* Per-item rise mask. mobile-menu.js wraps each .mobile-menu-nav-item / -bottom-item in a
   <div class="mobile-menu-item-mask"> at init; overflow:clip hides the parked (yPercent:100)
   item behind its own edge so it slides up into view — the block-level equivalent of
   rise-up-text.js's SplitText mask:"lines". Injected by JS + front-end only, so the Bricks
   editor markup is untouched. */
.mobile-menu-item-mask {
    overflow: clip;
}

@media (prefers-reduced-motion: reduce) {
    .mobile-menu-wrapper {
        transition: none;  /* overlay snaps in, matching the instant (0s) panel slide */
    }
}

/* ─── hover-icon-btn.js ──────────────────────────────────────────────────── */

/* Icon-button hover. The ::before/::after glow overlays FADE IN on hover (pseudo-elements
   can't be GSAP-animated, so this stays pure CSS); hover-icon-btn.js GSAP-scales the icon
   wrapper. .hover-btn-outer needs position+overflow for the overlays: the -20px top inset is
   clipped away so the top glow band doesn't show (same trick as .icon-box-big). */
.hover-btn-outer {
    position: relative;
    overflow: hidden;
}

.hover-btn-outer::before {
    content: "";
    position: absolute;
    inset: 0;
    pointer-events: none;
    border-radius: 8px;
    box-shadow: inset 0 0 8px var(--ra-glow-hover);
    z-index: 10;
    opacity: 0;
    transition: opacity 0.3s ease;   /* fade in on hover (matches the default scale duration) */
}

.hover-btn-outer:hover::before {
    opacity: 1;
}

@media (prefers-reduced-motion: reduce) {
    /* Overlays snap (no fade); the GSAP scale is skipped entirely in JS. */
    .hover-btn-outer::before,
    .hover-btn-outer::after {
        transition: none;
    }
}

/* ─── guide-index.js ─────────────────────────────────────────────────────── */

/* Hover-triggered module index overlay. Show/hide + collapse are INSTANT (no
   transitions, by request). Front-end only, so the Bricks editor shows the card
   and every sub-module expanded and editable. guide-index.js toggles .is-active
   on the section and .is-open on the single open sub-module. */

/* Inactive: the fixed full-screen section must not block the page behind it, so
   it's click-through — except .index-btn, which stays interactive so it can be
   hovered/tapped to open. The window-wrapper (card + its overline links) is hidden
   until active; the bare .guide-index-window is hidden too as a fallback for markup
   without the wrapper. Scoping the hide to :not(.is-active) means the active
   wrapper/card keeps whatever display its own CSS/Bricks gives it — none imposed here. */
.guide-index-section:not(.is-active) {
    pointer-events: none;
}
.guide-index-section:not(.is-active) .guide-index-window-wrapper,
.guide-index-section:not(.is-active) .guide-index-window {
    display: none;
}
.index-btn {
    pointer-events: auto;   /* re-enable the trigger while the section is click-through */
}

/* Active: 80% black backdrop over the page + the whole section becomes interactive
   (card scrolls/clicks; tapping the backdrop dismisses on touch). Set NO background
   on the section in Bricks so this owns the dim — otherwise a Bricks bg would show
   even when inactive. */
.guide-index-section.is-active {
    pointer-events: auto;
    background-color: var(--ra-scrim-index);
}

/* Sub-modules collapsed by default; guide-index.js adds .is-open to exactly one at
   a time. Scoping to :not(.is-open) leaves the open one's display to its own CSS.
   (Icon state is exposed as aria-expanded="true" on .guide-index-title-dropdown-icon-wrapper
   if you want to style an open-indicator — kept static here, no animation.) */
.guide-index-sub-module-wrapper:not(.is-open) {
    display: none;
}

/* ─── logos-outer.js initial state ───────────────────────────────────────── */

/* Hidden until the script has injected an <img> into every .logo-single and added
   .ready — without this the grid paints as a row of empty padded boxes for one frame.
   .ready is added even when the ACF pool comes back empty (the script logs why), so a
   mis-configured field reads as a visibly empty showcase rather than a section that
   isn't there at all. */
.logos-outer {
    visibility: hidden;
}
.logos-outer.ready {
    visibility: visible;
}
