/* ==========================================================================
   SPLIT BANNER, the one reusable full-bleed photo-and-type banner.
   Phase 0 shared foundation, DESIGN-BIBLE.md Note 1 (the split-hero house
   style). Every lane page builds its hero, showcase, and section banners
   from THIS component. Do not hand-roll a second version.

   WHY: one CSS class, one markup shape, grep-verifiable (bible acceptance
   test 1). Faces are never covered by text (Canon Keeper law) because the
   caption band always sits in the image's top OR bottom third, never
   mid-frame, chosen from real per-photo staging data, never guessed.

   ── MARKUP, the ONLY shape a lane crew needs ──────────────────────────────
   <div class="split-banner"
        data-split-banner
        data-variant="single | slideshow | collage"     (default: single)
        data-images="img/web/a.jpg|img/web/b.jpg"        (pipe-separated,
                                                            1 image for single,
                                                            2 to 4 for
                                                            slideshow, 1 to 4
                                                            tiles for collage,
                                                            more than 4 for
                                                            collage feeds a
                                                            rotation pool)
        data-kicker="Eyebrow label"                      (optional)
        data-heading="Big headline, wraps to two lines"  (required)
        data-body="One or two sentences of support copy" (optional)
        data-cta-href="intake.html"                      (optional, pairs
                                                            with data-cta-label)
        data-cta-label="Start a project"
        data-cta-href-2="tel:5712451075"                 (optional 2nd CTA)
        data-cta-label-2="Call 571-245-1075"
        data-align="left | center | right"               (default: left)
        data-band="auto | top | bottom"                  (default: auto,
                                                            reads capTop/objPos
                                                            hints from
                                                            ja-images.js when
                                                            the src matches a
                                                            gallery entry)
        data-theme="dark | light"                        (default: dark;
                                                            light = dark text
                                                            on a pale scrim,
                                                            for a bright photo)
        data-interval="9000"                             (ms per frame,
                                                            slideshow/collage
                                                            only, floor 8000)
        data-eager="true"                                (opts the FIRST
                                                            frame out of lazy
                                                            loading; use only
                                                            for a true above
                                                            the fold hero)
   ></div>
   split-banner.js finds every [data-split-banner] on the page on
   DOMContentLoaded and hydrates it. Nothing else to wire up.

   IMAGE IDS: data-images accepts real paths (img/web/xxx.jpg). When a path
   matches a window.JA_IMAGES entry's src, split-banner.js inherits that
   entry's objPos, capTop, title and alt text automatically so the band
   placement is never a guess. Per-instance data-heading/data-body/etc
   always win over inherited gallery copy.

   ── VARIANTS ───────────────────────────────────────────────────────────
   single    one photo, static. The default, the safest choice.
   slideshow 2 to 4 photos, slow crossfade (hold >= 8s per frame, the fade
             itself resolves in under 300ms, opacity only, never a Ken Burns
             zoom). Pauses the instant the banner scrolls out of view.
             Freezes on frame one under prefers-reduced-motion.
   collage   up to 4 tiles in a mosaic grid (for lanes with real photographic
             depth, per Note 4, e.g. Headshots, Lifestyle). Pass more images
             than tiles (data-images accepts any count) and split-banner.js
             distributes the extras round-robin so each tile gets its own
             honest rotation pool and crossfades independently on a
             staggered clock, never all four at once. Pass 4 or fewer images
             and the mosaic stays still, an honest static collage rather
             than a manufactured loop, per the bible's own "thin supply
             gets a static banner, not a stretched one" rule. Same
             hold/fade/pause/reduced-motion rules as slideshow. The caption
             band sits on a dedicated, full-width scrim strip so it never
             has to dodge a face in any one tile.

   ── MOTION LAW, enforced in split-banner.js, not just by convention ──────
   No banner ever animates unattended forever. Every timer is cancelled the
   instant IntersectionObserver reports the banner off screen, and every
   timer is skipped entirely when prefers-reduced-motion matches. There is
   no scroll-jacking anywhere in this file, native scroll is never touched.

   ── TYPE SYSTEM ───────────────────────────────────────────────────────
   Matches the shipped site exactly (Inter for display type, Space Mono for
   the eyebrow/CTA label, the locked navy/gold palette). The bible's prose
   names Playfair Display; that face is not loaded anywhere in site-v4 and
   is not being added here, since a new webfont means a new external
   request, and this component ships with zero external requests. Inter at
   the same weight and tracking the "Behind the Lens" reference section
   already uses on index.html is the honest, already-licensed choice.
   ========================================================================== */

/* CLS root-cause fix, 2026-08-11: split-banner.js only adds the
   ".split-banner" class once it hydrates the host div (see
   split-banner.js hydrateOne, "host.className = base + 'split-banner'"),
   but the raw, pre-hydration markup on every lane page already carries
   [data-split-banner] from the first byte of HTML, whether or not the
   author also wrote class="split-banner" (most did not, including
   faq.html and lifestyle-brand-photography.html). Every rule below that
   sets min-height was written against the CLASS only, so the host div
   had no promised height until JS ran. On a page where the browser's
   first paint lands before the deferred hydration script executes, that
   paints a collapsed, near-zero-height box, and the min-height jumping
   in afterward is a real, measured layout shift: reproduced live on
   es/faq.html and es/lifestyle-brand-photography.html at mobile width
   (CLS 0.52 and 0.15), gone once these selectors also match the
   attribute that is present from t=0. Same values either way, so
   nothing about the hydrated, final layout moves; this only removes the
   empty-box starting state the shift measured from. Same structural
   lesson as the round 2 min-height fix below: a rule that can silently
   fail to apply must be structurally impossible to miss, not
   conventionally correct. */
.split-banner, [data-split-banner] {
  --sb-navy: #172a36;
  --sb-navy2: #1a3040;
  --sb-navy3: #0f1e28;
  --sb-gold: #c09f89;
  --sb-pale: #eaf2f6;
  --sb-fade-ms: 260ms;
  /* height of the caption band's protected zone, top or bottom third
     (never mid-frame). Single source of truth for the scrim height AND,
     on the collage variant below, for how much of the frame the photo
     grid is allowed to occupy. Overridden per-breakpoint at the bottom
     of this file, mobile only. */
  --sb-band-h: 62%;
  /* ── ROOT-CAUSE FIX, elevation siege ROUND 2 (2026-08-10): the safety
     floor must beat every size variant STRUCTURALLY, not by out-ranking
     it on specificity ─────────────────────────────────────────────────
     Round 1 fixed the collage-grid-vs-band overlap. This is a second,
     separate trap in the same file: .split-banner[data-sb-size="compact"]
     (an attribute selector, specificity 0-2-0) was setting min-height
     directly, so it always beat the tablet and mobile safety floors
     below, which were plain .split-banner rules (specificity 0-1-0),
     REGARDLESS of source order, on every viewport those floors exist for.
     Live result: all six compact lane banners on services.html rendered
     at the bare 300px compact floor on phones instead of the mobile
     floor, and the caption band overflowed 18 to 76px into photo
     territory, covering faces on the Headshots and Portraits lanes. Lane
     D dodged this once already on faq.html by removing the attribute,
     which only relocates the trap, it does not defuse it, and this is
     the second time it has fired.
     The fix: no variant rule sets min-height directly anymore. Every
     variant below sets ONLY this custom property, --sb-min-h. Every
     breakpoint's floor, base/tablet/mobile alike, sets min-height
     exactly once, on the bare .split-banner selector, as max(the
     variant's own floor, that breakpoint's safety floor). One min-height
     declaration per breakpoint means there is no second, more-specific
     min-height rule left anywhere in this file for a variant, present or
     future, to accidentally out-specify. The floor is not competing on
     specificity anymore, it is baked into the same declaration, so it
     cannot lose. */
  --sb-min-h: clamp(340px, 62vw, 100vh);

  position: relative;
  width: 100%;
  min-height: var(--sb-min-h);
  overflow: hidden;
  isolation: isolate;
  background: var(--sb-navy3);
  color: var(--sb-pale);
}

.split-banner[data-sb-size="tall"], [data-split-banner][data-sb-size="tall"] { --sb-min-h: 100vh; }
.split-banner[data-sb-size="compact"], [data-split-banner][data-sb-size="compact"] { --sb-min-h: clamp(300px, 42vw, 560px); }

.split-banner[data-sb-theme="light"] { color: var(--sb-navy); }

/* ── photo layer ────────────────────────────────────────────────────── */
.split-banner__frame {
  position: absolute;
  inset: 0;
  z-index: 1;
}
.split-banner__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity var(--sb-fade-ms) ease;
  will-change: opacity;
}
.split-banner__img[data-sb-active="true"] { opacity: 1; z-index: 1; }
/* single-image variant: one frame, always shown, no fade needed */
.split-banner[data-variant="single"] .split-banner__img { transition: none; opacity: 1; }

/* ── collage mosaic ─────────────────────────────────────────────────── */
.split-banner__collage {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  grid-template-rows: repeat(2, 1fr);
  gap: 2px;
  background: var(--sb-navy3);
}
.split-banner__collage-cell {
  position: relative;
  overflow: hidden;
  background: var(--sb-navy2);
}
.split-banner__collage-cell .split-banner__img { transition: opacity calc(var(--sb-fade-ms) * 1.4) ease; }
/* 3-image collage: first cell spans both rows for a considered, asymmetric
   mosaic instead of an awkward empty fourth quadrant. Desktop/tablet cancel
   this below (single row there), mobile keeps it. */
.split-banner__collage[data-sb-count="3"] .split-banner__collage-cell:first-child {
  grid-row: 1 / 3;
}

/* ── ROOT-CAUSE FIX, emergency rebuild (2026-08-10, Jorge live 22:35): the
   2x2 collage grid divides its own zone, (100% - --sb-band-h), across TWO
   rows. On a full-height desktop hero that zone is ~38% of the viewport,
   so each row lands around 200px tall while each cell is roughly half the
   viewport wide, an extreme ~4.7:1 letterbox. object-fit:cover on a shape
   that wide crops away most of a source photo's height (cover binds on
   the cell's short dimension), which is why a leadership team photo on
   corporate-headshots-washington-dc.html and lifestyle-brand-photography.html
   rendered as a ceiling light fixture, and why headshot-photography.html's
   4-tile collage read as foreheads-only, effectively looking like only one
   tile "worked." The photo pick was never the bug, the row count was:
   giving every cell the FULL collage-zone height instead of half of it
   makes cover bind on width instead for these typically 3:2-ish source
   photos, which crops the sides, not the subject's head. Scoped to
   viewports where this was actually reported (>=641px); mobile's 2x2 grid
   above is untouched; a single row of up to 4 cells at phone widths would
   be unusably thin slivers, and mobile was never the complaint. */
@media (min-width: 641px) {
  .split-banner__collage {
    grid-template-rows: 1fr;
  }
  .split-banner__collage[data-sb-count="1"] { grid-template-columns: 1fr; }
  .split-banner__collage[data-sb-count="2"] { grid-template-columns: repeat(2, 1fr); }
  .split-banner__collage[data-sb-count="3"] { grid-template-columns: repeat(3, 1fr); }
  .split-banner__collage[data-sb-count="4"] { grid-template-columns: repeat(4, 1fr); }
  .split-banner__collage[data-sb-count="3"] .split-banner__collage-cell:first-child {
    grid-row: auto;
  }
}

/* ── ROOT-CAUSE FIX, elevation siege round 1 (2026-08-10): the caption band
   never gets a dedicated area of its own ──────────────────────────────────
   Found live: headshot-photography.html, corporate-headshots-washington-dc.html,
   and lifestyle-brand-photography.html all shipped a collage hero where the
   headline sat across 2 to 5 faces. Root cause was structural, not a bad
   photo pick: the 2x2 collage grid always filled the FULL banner (inset:0),
   and the band's own scrim was only a semi-transparent gradient painted on
   top of it, keyed to a single first-image staging hint. Any photo whose
   subject (in ANY of the 4 cells) landed in the band's real footprint,
   which is most of the frame at 62% tall, showed straight through the
   gradient. Retuning individual photos could not fix this: the next photo
   swap could reintroduce it, and three lanes already had, independently.

   The fix reserves the band's own footprint outright: the photo grid is
   sized to occupy ONLY the zone the band does not, using the exact same
   --sb-band-h the scrim uses, so the two are always complementary and can
   never both claim the same pixels. No collage cell can ever render behind
   the band, for any photo, on any lane, permanently, structurally. */
.split-banner[data-variant="collage"][data-band="bottom"] .split-banner__collage {
  top: 0;
  bottom: auto;
  height: calc(100% - var(--sb-band-h));
}
.split-banner[data-variant="collage"][data-band="top"] .split-banner__collage {
  top: auto;
  bottom: 0;
  height: calc(100% - var(--sb-band-h));
}

/* ── scrim, keyed to the caption band so it never bleeds past the
   third of the frame it is protecting ─────────────────────────────── */
.split-banner__scrim {
  position: absolute;
  left: 0; right: 0;
  z-index: 2;
  pointer-events: none;
}
.split-banner[data-band="bottom"] .split-banner__scrim {
  bottom: 0;
  height: var(--sb-band-h);
  background: linear-gradient(to top, rgba(15, 27, 34, .92) 0%, rgba(15, 27, 34, .62) 42%, rgba(15, 27, 34, 0) 100%);
}
.split-banner[data-band="top"] .split-banner__scrim {
  top: 0;
  height: var(--sb-band-h);
  background: linear-gradient(to bottom, rgba(15, 27, 34, .92) 0%, rgba(15, 27, 34, .62) 42%, rgba(15, 27, 34, 0) 100%);
}
.split-banner[data-sb-theme="light"][data-band="bottom"] .split-banner__scrim {
  background: linear-gradient(to top, rgba(234, 242, 246, .94) 0%, rgba(234, 242, 246, .66) 42%, rgba(234, 242, 246, 0) 100%);
}
.split-banner[data-sb-theme="light"][data-band="top"] .split-banner__scrim {
  background: linear-gradient(to bottom, rgba(234, 242, 246, .94) 0%, rgba(234, 242, 246, .66) 42%, rgba(234, 242, 246, 0) 100%);
}

/* ── caption band, top or bottom third, never mid-frame ───────────── */
.split-banner__band {
  position: absolute;
  left: 0; right: 0;
  z-index: 3;
  display: flex;
  flex-direction: column;
  padding: clamp(24px, 4.2vw, 56px) clamp(20px, 4vw, 52px);
  max-width: 720px;
}
.split-banner[data-band="bottom"] .split-banner__band { bottom: 0; }
.split-banner[data-band="top"] .split-banner__band { top: 0; }

.split-banner[data-align="left"] .split-banner__band { left: 0; align-items: flex-start; text-align: left; }
.split-banner[data-align="center"] .split-banner__band { left: 50%; transform: translateX(-50%); align-items: center; text-align: center; }
.split-banner[data-align="right"] .split-banner__band { right: 0; align-items: flex-end; text-align: right; }

.split-banner__kicker {
  font-family: 'Space Mono', monospace;
  font-size: 10px;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--sb-gold);
  margin-bottom: clamp(10px, 1.6vw, 16px);
}

.split-banner__heading {
  font-family: 'Inter', sans-serif;
  font-weight: 600;
  font-size: clamp(26px, 4.4vw, 52px);
  letter-spacing: -.02em;
  line-height: 1.1;
  margin: 0 0 clamp(10px, 1.6vw, 16px);
  max-width: 18ch;
  /* two-line cap per the bible's spec, wraps naturally at the max-width
     above. This is a hard px-free backstop (max-height in em, so it scales
     with the clamp() font-size automatically) for the rare heading that
     still runs long. Deliberately NOT -webkit-line-clamp: Chromium renders
     an automatic ellipsis glyph for line-clamp truncation regardless of
     text-overflow, confirmed live while building this component, and this
     site's hard law is no ellipsis anywhere, inherited copy included.
     split-banner.js trims to whole words before this ever has to bite, so
     in practice this max-height rarely clips anything, it only guarantees
     that if it ever does, it cuts clean instead of showing a banned
     glyph. */
  max-height: 2.35em;
  overflow: hidden;
}
.split-banner__heading em {
  font-style: normal;
  color: var(--sb-gold);
}

.split-banner__body {
  font-size: 15px;
  line-height: 1.72;
  color: rgba(234, 242, 246, .72);
  max-width: 46ch;
  margin: 0 0 clamp(14px, 2vw, 22px);
  /* bounded for the same reason the heading is bounded: an unbounded body
     paragraph can grow the band tall enough to threaten the image's own
     safe third, on any viewport, not just mobile (found live at 768 wide
     during Phase 0 verification with a longer support line). Same em-based
     max-height backstop as the heading, same reason it is not
     -webkit-line-clamp: no automatic ellipsis glyph, ever. */
  max-height: 5.2em;
  overflow: hidden;
}
.split-banner[data-sb-theme="light"] .split-banner__body { color: rgba(23, 42, 54, .72); }

.split-banner__ctas {
  display: flex;
  gap: 12px;
  flex-wrap: wrap;
}
.split-banner__cta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: 'Space Mono', monospace;
  font-size: 11px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  text-decoration: none;
  padding: 13px 24px;
  border-radius: 6px;
  transition: transform .18s ease, box-shadow .18s ease;
}
.split-banner__cta[data-sb-cta="primary"] {
  color: var(--sb-navy);
  background: var(--sb-gold);
  box-shadow: 0 8px 22px -8px rgba(192, 159, 137, .55);
}
.split-banner__cta[data-sb-cta="secondary"] {
  color: var(--sb-pale);
  background: rgba(234, 242, 246, .08);
  border: 1px solid rgba(234, 242, 246, .3);
}
.split-banner[data-sb-theme="light"] .split-banner__cta[data-sb-cta="secondary"] {
  color: var(--sb-navy);
  border-color: rgba(23, 42, 54, .3);
  background: rgba(23, 42, 54, .05);
}
.split-banner__cta:hover { transform: translateY(-1px); }

/* ── tablet range: the base clamp(340px, 62vw, 100vh) formula produces a
   banner shorter, relative to real caption content, than at any other
   width in the 641 to 1024px band (font size, padding, and margins in the
   band all track their OWN, faster vw coefficients than the host's 62vw,
   so they close the gap at both narrower and wider widths but not here).
   Found live during Phase 0 verification at 768 wide, a real caption band
   measured 313px tall inside a 476px banner, crossing this component's own
   safe-zone promise. A dedicated floor for this range only, matching the
   same reasoning as the mobile override below. ─────────────────────────── */
@media (min-width: 641px) and (max-width: 1024px) {
  /* same max(variant-floor, breakpoint-floor) pattern as the mobile fix
     below, so the identical compact-defeats-the-floor trap cannot
     resurface in the tablet range either. Same bug, same shape, elevation
     siege round 2 (2026-08-10), see the root-cause comment at the top of
     this file. */
  .split-banner, [data-split-banner] { min-height: max(var(--sb-min-h), clamp(460px, 78vw, 720px)); }
}

/* ── mobile: band drops to a fixed, full-width strip with a shorter line
   count, per the bible. It stays on whichever edge (top or bottom) the
   per-photo data-band already resolved to, deliberately NOT forced to the
   top unconditionally: most of Jorge's real derivatives anchor the subject
   toward the top of the frame (objPos "center top" is the gallery's own
   default), so an unconditional top-anchored band would routinely cross a
   face on the exact viewport where the caption band is largest relative to
   the photo. Faces never covered by text is the harder law here and wins
   over the literal "top-anchored" phrasing. Position (top:0 vs bottom:0)
   is inherited from the [data-band] rules above; only geometry, type size,
   and column count change for the narrow viewport. ─────────────────────── */
@media (max-width: 640px) {
  /* taller floor than the desktop clamp's own low end would give at these
     widths, on purpose: the band's real text content (kicker, a two line
     heading, a two line body, one CTA row, its own padding) measures
     roughly 210 to 230px even at the shortest mobile widths, and the safe
     zone this component promises (Note 1 acceptance test 2, zero caption
     bands overlapping a face) only holds if the banner is tall enough that
     the band stays a MINORITY of the frame, never most of it. */
  .split-banner, [data-split-banner] {
    /* ROUND 2 fix (2026-08-10): max() against --sb-min-h, see the
       root-cause comment at the top of this file. This is the exact line
       the compact attribute selector used to defeat by specificity; it
       cannot anymore, because compact's own floor is only ever read as
       an input to this max(), never set as a competing min-height
       declaration of its own. Also matches [data-split-banner] directly
       (CLS root-cause fix, 2026-08-11, see the top of this file), so this
       floor is live before split-banner.js has hydrated the host, not
       only after. */
    min-height: max(var(--sb-min-h), clamp(420px, 118vw, 620px));
    /* narrower band footprint than desktop's 62 percent: at these widths
       62 percent would leave the collage variant's photo grid too thin a
       sliver to read as a photo at all. Still tall enough, per the safe
       zone reasoning above, that the band stays a minority of the frame,
       and the collage reservation rule above tracks this value exactly,
       so the same zero-overlap guarantee holds at every width. */
    --sb-band-h: 52%;
  }
  /* Fix crew 2026-08-11, real-device walk finding 4: on the "tall" (100vh)
     collage variant, the collage box is pinned to the top (calc(100% -
     --sb-band-h) tall) and the band used to be pinned to bottom:0, sized
     only to its own content, content-height-driven from the container's
     TRUE bottom edge. Those are two independent anchors, so whenever the
     band's real content (kicker + heading + body + CTA row) came in
     shorter than the full --sb-band-h zone it reserves, the leftover
     space between them showed as a dead navy gap. Measured before this
     fix on corporate-headshots-washington-dc.html at 402x874: gap 171px.
     First attempt tried narrowing --sb-band-h itself (52% to 42%), which
     looked safe at 402 wide (28px margin on the tightest page) but broke
     at 375: this component's heading/body wrap to an extra line at
     certain widths, and band content on some pages jumped past 380px
     there, overlapping the collage by up to 36px, the exact face-behind-
     text bug the round 1 fix above exists to prevent. Reverted that.
     This is the safe version: leave --sb-band-h and the collage's own
     calc(100% - band-h) box completely untouched (zero change to the
     round 1 safety math, still structurally impossible for a collage
     cell to render behind the band, for any photo, any content length,
     any width), and instead anchor the band's TOP to the exact same line
     the collage's bottom already sits on, letting it grow downward by
     its own content instead of counting backward from the container's
     full-height bottom edge. The gap is now always exactly zero by
     construction, not by a tuned number, so it cannot regress at a width
     this crew did not happen to test. Checked 320 to 640 wide on all six
     collage lane pages (en and es): gap 0 everywhere, band never
     overlaps the collage (top-anchored, only grows away from it). */
  .split-banner[data-variant="collage"][data-band="bottom"] .split-banner__band,
  [data-split-banner][data-variant="collage"][data-band="bottom"] .split-banner__band {
    top: calc(100% - var(--sb-band-h));
    bottom: auto;
  }
  /* symmetric case: no live collage lane currently resolves data-band to
     "top" (all six measured "bottom", band.js picks this per-photo from
     ja-images.js staging hints), but the same gap would occur in mirror
     image if one ever does, so this is here defensively, same technique,
     same zero blast radius to anything currently shipping. */
  .split-banner[data-variant="collage"][data-band="top"] .split-banner__band,
  [data-split-banner][data-variant="collage"][data-band="top"] .split-banner__band {
    bottom: calc(100% - var(--sb-band-h));
    top: auto;
  }
  .split-banner__band {
    left: 0 !important;
    right: 0 !important;
    transform: none !important;
    align-items: flex-start !important;
    text-align: left !important;
    max-width: none;
    padding: 20px 16px 22px;
  }
  .split-banner__scrim {
    left: 0 !important;
    right: 0 !important;
  }
  .split-banner__heading {
    font-size: clamp(21px, 6.4vw, 28px);
    max-width: none;
    /* two lines max, matching the desktop spec exactly, not stretched to
       three just because the viewport is narrow. max-height is already set
       in em on the base rule above, so it tracks this smaller font-size
       automatically, no redeclaration needed. */
  }
  .split-banner__body {
    font-size: 13px;
    max-width: none;
    /* shorter line count on mobile, per the bible, one line tighter than
       desktop's backstop */
    max-height: 3.44em;
  }
  .split-banner__collage { grid-template-columns: repeat(2, 1fr); }
}

/* ── reduced motion: freeze on frame one, no timers, no fallback fade ─── */
@media (prefers-reduced-motion: reduce) {
  .split-banner__img { transition: none !important; }
}

/* ── loading state: quiet placeholder so layout never jumps before the
   first frame paints ──────────────────────────────────────────────── */
.split-banner__img:not([src]) { background: var(--sb-navy2); }

/* SEO static h1, 2026-08-12. Pre-rendered inside each banner host so crawlers
   without JavaScript see the page topic; hydrateAll() removes it and builds
   the styled heading in its place. This rule only ever paints for no-JS
   visitors and during the instant before hydration. */
.sb-static-h1{margin:0;padding:28px clamp(20px,4vw,44px) 0;font-weight:300;
  font-size:clamp(26px,3.4vw,44px);line-height:1.15;color:var(--ink,#eaf2f6)}
