/* ═══════════════════════════════════════════════════════════════════
   BAC · SHARED OVERLAY — index-v8 components, ported wholesale
   ═══════════════════════════════════════════════════════════════════
   Loaded on every page (index.html, origin/index.html,
   growth-systems/index.html) via one <link>, instead of copy-pasted
   per file — this repo is multi-page now, and duplicating this block
   three times means fixing it three times.

   Everything here is a COMPLETE REPLACEMENT of Framer's own attempt
   at the same feature, not a re-skin of it: the buttons, the nav
   dark/light swap, and the glyph-wall hero/band backgrounds are lifted
   from site/index-v8.html directly, per the operator's instruction —
   "complete replacements from index-v8, not just fitting it."

   ⚠ EVERYTHING BELOW IS !important, DELIBERATELY. Framer's own SSR
   <style> block is emitted INLINE, per page, AFTER this stylesheet
   link in <head> — a same-or-lower-specificity rule here loses the
   cascade on source order alone. This is the same lesson learned
   repeatedly porting into the previous single-page export: buttons,
   nav swap, responsive breakpoints all needed it, for the same reason.
   ⚠ PATHS ARE ABSOLUTE (leading /), not relative — this file is
   shared across pages at different directory depths (/, /origin/,
   /growth-systems/), so "assets/..." would resolve wrong one level
   down. Every url()/asset reference below uses /assets/... or
   /motion/... from the site root. */

:root{
  --ink:#042839;--sky:#86E1FA;--paper:#F9F8F5;
  --f-text:"Helvetica Neue",Helvetica,Arial,sans-serif;
  --p-dur:800ms;--p-ease:cubic-bezier(.83,0,.17,1);
}

/* ═══ THE BUTTONS · index-v8's .p component ═══════════════════════
   site/index-v8.html:735-883. The box-model reset for each button's
   Framer wrapper is applied INLINE by JS (bac-overlay.js), not here —
   see that file's RESET_STYLE for why (a same-specificity stylesheet
   rule loses to Framer's own two-class wrapper rule regardless of
   !important arms races; inline always wins). This block only carries
   custom properties and the new child elements' (.p-label/.p-tile)
   own rules, which don't compete with any Framer rule since those
   class names don't exist anywhere in Framer's own CSS. */
.p{
  --tile:40px;--gutter:8px;--pad-y:12px;--pad-x:32px;
  --dur:var(--p-dur);--ease-swap:var(--p-ease);
  --fg:var(--paper);--ground:var(--ink);--edge:rgba(249,248,245,.42)}
.p::after{content:none!important}
.p.sm{--tile:31px;--gutter:6px;--pad-y:8px;--pad-x:20px}
.p:focus{outline:none}
.p:focus-visible{outline:2px solid var(--sky);outline-offset:3px}
.p.light{--fg:var(--ink);--ground:var(--paper);--edge:rgba(4,40,57,.32)}
.p.no-caret .p-caret{display:none}

.p-label{
  position:relative;overflow:hidden;
  display:inline-flex;align-items:center;justify-content:center;
  padding:var(--pad-y) var(--pad-x);
  border:1px solid var(--edge);background:transparent;color:var(--fg);
  /* 14px site-wide, .sm included — was 12px/11px (.sm), bumped to
     match the nav's own body text ("Origin"/"Growth Systems", 14px)
     per the operator's instruction to apply the nav-cta fix (originally
     scoped just to that one button) everywhere .p is used. */
  font-size:14px;line-height:normal;letter-spacing:normal;
  white-space:nowrap;
  translate:calc(-1 * (var(--tile) + var(--gutter))) 0;
  transition:translate var(--dur) var(--ease-swap)}
.p-label::before{content:"";position:absolute;inset:-1px;z-index:1;
  background:var(--fg);clip-path:inset(0 0 0 0);
  transition:clip-path var(--dur) var(--ease-swap)}
.p-label::after{content:attr(data-label);position:absolute;inset:0;z-index:2;
  display:flex;align-items:center;justify-content:center;color:var(--ground);
  clip-path:inset(0 0 0 0);
  transition:clip-path var(--dur) var(--ease-swap)}
.p-caret{position:absolute;top:0;bottom:0;left:0;width:2px;z-index:3;
  margin-left:-2px;background:var(--fg);opacity:0;pointer-events:none;
  transition:left var(--dur) var(--ease-swap)}

.p-tile{position:relative;z-index:0;display:flex;align-items:center;
  justify-content:center;flex:0 0 var(--tile);width:var(--tile);
  background:var(--fg);color:var(--ground);
  font-size:16px;line-height:1;overflow:hidden;
  transition:rotate var(--dur) var(--ease-swap),scale var(--dur) var(--ease-swap)}
.p.sm .p-tile{font-size:12px}
.p-tile.l{margin-right:var(--gutter);transform-origin:left center;
  rotate:-45deg;scale:0}
.p-tile.r{position:absolute;right:0;top:0;bottom:0;z-index:2;
  transform-origin:right center;rotate:0deg;scale:0}
.p-tile.r{rotate:0deg;scale:1}

.p:hover .p-label,.p:focus-visible .p-label{translate:0 0}
.p:hover .p-label::before,.p:focus-visible .p-label::before,
.p:hover .p-label::after,.p:focus-visible .p-label::after{clip-path:inset(0 0 0 100%)}
.p:hover .p-tile.l,.p:focus-visible .p-tile.l{rotate:0deg;scale:1}
.p:hover .p-tile.r,.p:focus-visible .p-tile.r{rotate:-45deg;scale:0}
.p:hover .p-caret,.p:focus-visible .p-caret{
  left:100%;animation:p-caret var(--p-dur) steps(1,end) forwards}
@keyframes p-caret{0%,99%{opacity:1}100%{opacity:0}}

.p:active .p-label{translate:0 0;transition:none}
.p:active .p-label::before,.p:active .p-label::after{
  clip-path:inset(0 0 0 0);transition:none}
.p:active .p-tile.l{rotate:0deg;scale:1;transition:none}
.p:active .p-tile.r{rotate:-45deg;scale:0;transition:none}
.p:active .p-caret{opacity:0}

/* ═══ THE NAV · light bar, no dark-mode swap ══════════════════════
   index-v8 itself swaps the bar to a dark/blurred variant over dark
   hero/band content, and an earlier pass here reimplemented that
   (JS toggling .on-dark on <html> off computed background luma,
   bac-overlay.js). Operator instruction: drop the swap, light-only
   everywhere — removed rather than left disabled, since dead
   detection code (the luma sampling, the elementFromPoint walk, the
   scroll/resize listeners driving it) is exactly the kind of thing
   that rots silently once nothing exercises it. The logo's native
   fill (rgb(4,40,57), i.e. --ink navy — confirmed directly off the
   <symbol>'s own path attribute) already reads correctly against
   this light bar with no filter needed. */
/* position:fixed — operator instruction, superseding the sticky rule
   this replaced. The bar now sits out of document flow permanently,
   overlapping whatever scrolls underneath it (the hero, deliberately —
   see the hero top-padding removal below); z-index is unchanged (still
   5, still the highest value anything in this file uses) so it keeps
   sitting above the page's own content the whole time. */
.bac-nav-bar{
  position:fixed!important;
  top:0!important;
  left:0!important;
  width:100%!important;
  /* Framer's own inline style still carries transform:translateX(-50%),
     paired originally with a left:50% this rule no longer sets — left
     alone from a fixed left:0 anchor, that -50%-of-its-own-width shift
     threw the bar off-screen. Cancel it out. */
  transform:none!important;
  background-color:rgba(250,250,248,.92)!important;
  backdrop-filter:blur(5px)!important;
  -webkit-backdrop-filter:blur(5px)!important}
.bac-nav-bar .text{color:var(--ink)!important}

/* the nav CTA ("Book the Hour") is a .p button too */
.p.nav-cta{
  --fg:var(--ink);--ground:var(--paper);--edge:rgba(4,40,57,.32)}
/* label size no longer needs a nav-cta-specific override — .p-label's
   own rule is 14px site-wide now, which already matches the nav's
   plain text ("Origin"/"Growth Systems") that this button sits beside.
   See .p-label's own comment for why. */

/* nav-bar hover pairing (logo + "Origin" + "Growth Systems") —
   bac-overlay.js toggles this class on whichever ISN'T being hovered.
   ⚠ ASYMMETRIC, on purpose — the logo can DIM the other two, but is
   never dimmed itself, even while hovering Origin/Growth Systems. See
   wireNavHover()'s per-item target lists for how that's built. */
.bac-nav-item-dim{opacity:.6;transition:opacity 200ms ease}

/* nav's "Origin"/"Growth Systems" — operator instruction: drop
   Framer's own fixed min-width (confirmed via its compiled CSS:
   min-width:100px on "Origin", ~108px on "Growth Systems" — real
   glyph width is nowhere near that, so each sat centred in a mostly-
   empty box) and let the box fit its own content instead, with real
   padding standing in for the breathing room the min-width used to
   provide. Scoped to .bac-nav-pill-link (wireNavLinks adds it only
   inside .bac-nav-bar) rather than every .bac-wired-link, since the
   footer reuses the exact same two labels and isn't part of this ask. */
.bac-nav-pill-link{min-width:0!important;padding:4px 10px!important}

/* nav-row-5/-6 are the ROW each link sits in, one level up from
   .bac-nav-pill-link above — a second, separate min-width:140px (each
   page's own compiled CSS) on the wrapping box itself, not the link.
   Dropping the pill's min-width alone left this one standing: "Origin"
   (the narrower label) still sat inside a 140px row with room to spare
   on one side, same box-bigger-than-content issue at the next layer up.
   Operator instruction, in step with the rule above: fit the row to
   its own content too.

   nav-row-6 also carried place-content:center flex-start — align-items
   is the cross axis (vertical, already centred) and justify-content
   flex-start is the main axis, which only reads as "left-aligned
   inside the box" while the box is wider than its content; once the
   box IS its content there is nothing left to left-align against, but
   centring it explicitly keeps the two rows on the same rule rather
   than one centred by omission and the other by accident. */
.nav-row-5,
.nav-row-6{
  min-width:0!important;
  justify-content:center!important;
  padding-left:0!important;
  padding-right:0!important}

/* nav-row-3 (link pair + "Book the Hour") and nav-row-4 (the link pair
   itself, Growth Systems/Origin) both set to a 16px gap — operator
   instruction. nav-row-3 was tried at 0 first and reopened to 16px now
   that -5/-6's left/right padding is gone too, so the 16px here is the
   ONLY thing separating the two links from each other and from the
   CTA, rather than stacking on top of padding that did the same job. */
.nav-row-3,
.nav-row-4{gap:16px!important}

/* ═══ UNDERLINE-FILL HOVER · footer links + nav's Origin/Growth
   Systems ═══════════════════════════════════════════════════════════
   One growing underline, left to right, not the browser default or a
   simple fade. `background-size` animates instead of `width` so the
   line can sit UNDER descenders (background, not a border/pseudo box)
   without a separate ::after element per link — a linear-gradient
   with a hard stop is functionally a two-colour fill, and animating
   its size is exactly "fills in". Not applied to the nav logo — it's
   an icon, not a text link, and it already gets the opacity-dim
   treatment (see above) as its own hover language.

   ⚠ THE FILL PAINTS ON A TIGHT INLINE SPAN AROUND JUST THE LABEL'S OWN
   TEXT (bac-overlay.js wraps it), NOT ON `.bac-wired-link` ITSELF —
   that wrapper (and Framer's own text element inside it) is stretched
   to fill whatever width its own layout gives it, which is often wider
   than the visible letters (the nav's "Origin", the footer's "Privacy
   Policy"/"Terms of Service" column). background-size:100% painted
   there filled the WHOLE box on hover, not just the text. The hover
   TRIGGER stays on the wrapper (see the selectors below) so the full
   click area still activates it — only where the fill paints changed. */
.bac-underline{
  background-image:linear-gradient(currentColor, currentColor);
  background-repeat:no-repeat;
  background-position:0 100%;
  background-size:0% 1px;
  transition:background-size 280ms cubic-bezier(.22,1,.36,1)}
.bac-wired-link:hover .bac-underline,
.bac-wired-link:focus-visible .bac-underline,
.bac-underline:hover,.bac-underline:focus-visible{background-size:100% 1px}

/* "How it works" in the origin/growth-systems hero — operator
   instruction: same underline hover as every other wired link (the
   .bac-underline rules above already cover that once wireHowItWorks
   wraps its text the same way wireNavLinks does), plus drop it from
   Framer's own Medium cut to Roman/normal weight — the same swap the
   site's own body copy uses (confirmed: "Haas Grot Text Web 55 Roman"
   is what everything at font-weight:400 already loads here), not just
   a numeric font-weight override against the Medium face, which would
   either do nothing (separate static font files per weight, not a
   variable-weight one) or fake-bold the wrong direction. */
.bac-how-it-works-link,
.bac-how-it-works-link .text{
  --font-family:"Haas Grot Text Web 55 Roman","Haas Grot Text Web 55 Roman Placeholder",sans-serif!important;
  font-family:"Haas Grot Text Web 55 Roman","Haas Grot Text Web 55 Roman Placeholder",sans-serif!important;
  --font-weight:400!important;
  font-weight:400!important}

/* ═══ GLYPH-WALL HOSTS · positioning only, mount logic in the JS ══
   One class now, not a hero/quote split — bac-overlay.js auto-detects
   every full-bleed dark section on the page (see "findDarkSections")
   rather than targeting specific hardcoded sections, so it doesn't
   know in advance how many hosts a given page will end up with. */
.bac-glyph-wall{position:absolute;inset:0;z-index:0;pointer-events:auto}

/* every dark hero/band section (tagged bac-dark-section in JS,
   ensureGlyphWalls) is built from Framer's own layout frames stacked
   over the glyph wall — heading and copy each sit in their own
   full-width wrapper div, and that wrapper's LAYOUT BOX (not just the
   glyph pixels of the text) is what receives pointer events by
   default. Since those wrappers are large, mostly-whitespace boxes,
   the practical result is that clicking/hovering over the hero works
   or doesn't depending on which invisible box happens to be on top of
   that particular pixel — "random", from the outside. Default every
   descendant in these sections to pointer-events:none and open it
   back up only where something is actually meant to be interactive:
   the glyph wall itself (the sonar hover), real buttons/links, and
   anything this file has wired up as one. */
.bac-dark-section *{pointer-events:none}
.bac-dark-section .bac-glyph-wall,
.bac-dark-section .p,
.bac-dark-section a,
.bac-dark-section [role="button"],
.bac-dark-section [role="link"],
.bac-dark-section .bac-wired-action,
.bac-dark-section .bac-wired-link{pointer-events:auto!important}

/* ═══ THE THREE-CARD CYCLE · "own what wins" ══════════════════════
   REPLACED, not skinned — see bac-overlay.js's wireCardCycle() for
   the full reasoning. Earlier passes tried to reuse Framer's own
   problem-col-4 (Home) Active/Inactive component (real in the design, frozen
   on export) and drive it from outside. That failed in increasingly
   serious ways: its own two "which card is active" signals (a variant
   class and an inline background-color) could drift from EACH OTHER
   under real interaction; its live component re-rendered its own copy
   INTO a slot this module had already populated, producing doubled
   text; and finally, self-healing against both of those crashed
   React outright (`NotFoundError: Failed to execute 'removeChild' —
   the node to be removed is not a child of this node`), because
   mutating a subtree React still tracks as its own is fundamentally
   unsafe, not just fiddly. Per the operator: stop building on top of
   Framer's component for this section — replace it. This is that
   replacement: an entirely new, self-contained structure
   (.bac-cards-row) built fresh in JS, holding zero shared DOM with
   Framer's original. That original is hidden (.bac-cards-row-hidden,
   below) rather than removed — React keeps whatever it wants to do
   with a display:none subtree nobody can see or reach with a pointer,
   and this module never has to touch it again after that one class
   is added. No self-healing observers needed anymore: there is
   nothing left for Framer to fight over. */
.bac-cards-row-hidden{display:none!important}

/* the whole "Financial services firms build growth..." band — tagged
   in JS (wireGrowthBand) since its own DOM has no stable class of its
   own to hang a rule on. Same divider convention as everything else
   in this file (rgba(4,40,57,.25)); the row's own border-top, just
   below, is a lighter .12 — this is the section's outer edge, not
   another internal divider. */
.bac-growth-band{border-top:1px solid rgba(4,40,57,.25)}

/* ═══ HEADER CARET-TYPING · ported from the handover package ═══════
   Same class names and timings as bac-v8-launch-2026-08-23's
   motion/verbs.js + its inline <style> (the .hl swash-highlight rules
   that package also carries are left out — nothing here uses them):
   a per-character reveal (.rd-ch, hidden via color:transparent until
   .on lands) and a single blinking terminal caret (.caret-v, 1060ms
   step blink) that only blinks on the ONE header currently marked
   .in-view by the JS (whichever finished header sits closest to the
   viewport's top) — never more than one blinking cursor on screen at
   once, same as the original. See bac-overlay.js's wireHeaderTyping
   for the reveal timing and the in-view tracking. */
.rd-tx{display:inline}
.rd-ch{white-space:pre-wrap}
[data-motion="type"].rd-typing .rd-ch:not(.on){color:transparent}
.caret-v{display:inline-block;width:max(1.5px,.022em);height:.72em;
  background:currentColor;vertical-align:baseline;margin-left:.07em;opacity:0}
[data-motion="type"].rd-typing .caret-v,
[data-motion="type"].rd-done.in-view .caret-v{animation:caret-blink 1060ms steps(1,end) infinite}
@keyframes caret-blink{0%,50%{opacity:1}50.01%,100%{opacity:0}}
@media (prefers-reduced-motion:reduce){.caret-v{display:none}}

/* the trailing-period char inside a typed header — same --sky/#197281
   split as colorHeaderPeriods, just resolved through a custom
   property (set inline per-header in JS) instead of a direct color,
   so it can still be hidden by the .rd-ch:not(.on) rule above while
   its own char hasn't been "typed" yet. */
.rd-ch.bac-header-period.on{color:var(--hp-color)}

.bac-cards-row{display:flex;border-top:1px solid rgba(4,40,57,.12);
  border-bottom:1px solid rgba(4,40,57,.12)}
.bac-card-new{flex:1 1 0;position:relative;overflow:hidden;
  padding:32px 24px;border-right:1px solid rgba(4,40,57,.25);
  background-color:var(--paper);transition:background-color 300ms ease}
/* outer edges of the row — previously only the top/bottom (on
   .bac-cards-row) and the between-card dividers existed, leaving the
   row's own left/right ends open. Same convention, same colour, just
   closing the frame. */
.bac-card-new:first-child{border-left:1px solid rgba(4,40,57,.25)}
.bac-card-new:last-child{border-right:1px solid rgba(4,40,57,.25)}
.bac-card-new.bac-active{background-color:#FFFFFF}

.bac-card-icon{width:20px;height:20px;margin-bottom:24px;color:var(--ink)}
.bac-card-icon svg{width:100%;height:100%;display:block}

.bac-card-headline{
  font-family:"Tiempos Text Regular","Tiempos Text Regular Placeholder",serif;
  font-size:16px;letter-spacing:-0.02em;line-height:140%;
  color:var(--ink);margin:0 0 12px}

.bac-card-body{
  font-family:"Haas Grot Text Web 55 Roman","Haas Grot Text Web 55 Roman Placeholder",sans-serif;
  font-size:14px;letter-spacing:-0.02em;line-height:140%;
  color:rgba(4,40,57,.8);margin:0;
  opacity:0;transform:translateY(12px);
  transition:opacity 420ms cubic-bezier(.22,1,.36,1),
             transform 420ms cubic-bezier(.22,1,.36,1)}
.bac-card-new.bac-active .bac-card-body{opacity:1;transform:translateY(0)}

.bac-card-bar{position:absolute;top:0;left:0;right:0;height:8px}
/* Primary ink, not the bright --blue this used to be — operator
   instruction. Written as the token rather than the literal
   #042839 so the bar follows the palette if ink is ever re-struck;
   the two are the same value today. */
.bac-card-bar-fill{display:block;height:100%;width:0%;background:var(--ink)}
/* duration read from a custom property JS sets alongside the same
   constant that drives the interval (--bac-cycle-ms) — one source of
   truth, so the bar's fill can never drift out of sync with the
   actual 4s swap even if that number ever changes. */
.bac-card-bar-fill.bac-fill{width:100%;
  transition:width var(--bac-cycle-ms,4000ms) linear}

/* ═══ THE GROWTH-SYSTEMS MAP · sonar pulse on the label dots ══════
   The 8 labelled dots (Conversations/Sales/Enquiries/Services/
   Brokers/Loans/Applications/Portals) scattered over the hero-card
   graphic on the home page.

   ⚠ NOT A PSEUDO-ELEMENT ON THE DOT — first attempt was, and it was
   invisible in practice. The dot itself computes overflow:clip
   (needed so its own round icon doesn't spill past its circular
   corners), and a clipped element's OWN pseudo-elements are clipped
   by that same box — so a ::before scaling outward got cut back to
   the dot's original edge every frame, and "pulsing" read as nothing
   at all.

   ⚠ NOT A CHILD OF THE SHARED SECTION EITHER — second attempt tried
   that (a real sibling element instead of a pseudo-element), and it
   was WORSE: that section is Framer's own live component, and it can
   re-render on its own initiative — measured directly, all 8 rings
   present and animating one moment, 0 rings moments later with no
   navigation or interaction, wiped by something else's re-render.
   Same class of fragility the three-card section taught this file
   the hard way.

   FIX: the rings live in .bac-sonar-overlay, a container
   bac-overlay.js creates and appends directly to <body> — outside
   Framer's tree, so nothing it does can ever touch them. Positioned
   in PAGE coordinates (computed once at wire time, static layout, no
   resize handling needed — matches every other one-off position read
   in this codebase), so it can't move any dot or label; it's a layer
   floating above the page, not part of anything's box model. */
.bac-sonar-overlay{position:absolute;top:0;left:0;z-index:5}
.bac-sonar-ring{
  position:absolute;border-radius:50%;
  border:1px solid rgba(255,255,255,.9);
  opacity:0;pointer-events:none;
  transform:translate(-50%,-50%) scale(1);
  /* return-to-rest fade/grow — only live when .bac-sonar-ping is NOT
     applied (see below), so ending a pulse hands off into a transition
     instead of an instant style snap */
  transition:opacity 500ms ease, transform 500ms ease;
}
.bac-sonar-ring.bac-sonar-ping{
  /* forwards is load-bearing: without it, the animation's default
     fill-mode (none) reverts the ring to its resting style the INSTANT
     the keyframe's own duration elapses — before the JS setTimeout
     even removes .bac-sonar-ping. forwards holds the pulse's end state
     until JS actually acts, which is what the transition above needs
     to fade/grow FROM. */
  animation:bac-sonar-ping 1400ms cubic-bezier(.22,.61,.36,1) 1 forwards;
  transition:none; /* the keyframe owns opacity/transform while pinging */
}
@keyframes bac-sonar-ping{
  0%{opacity:.55;transform:translate(-50%,-50%) scale(1)}
  100%{opacity:0;transform:translate(-50%,-50%) scale(1.6)}
}

/* ═══ ORIGIN · "Agents do the work / Catch customers / System
   continuity" three-column row — top-align, not bottom-align ═══════
   .page-origin .problem-row-2 is the flex row wrapping the three column
   boxes (was problem-row-2 (Origin) before the rebuild renamed it).
   Confirmed directly (getBoundingClientRect on all three): the column
   boxes are genuinely different heights, and this row's own
   align-items:flex-end bottom-anchors each one — so the shorter two
   columns' headlines start LOWER than the tallest column's, which is
   the "looks uneven" the operator flagged. flex-start instead anchors
   every column to the row's own top regardless of height, so all
   three headlines start at the same Y. Single node on this page
   (checked: only one match), so this doesn't reach past what it's
   meant to touch. */
.page-origin .problem-row-2{align-items:flex-start!important}

/* ═══ GROWTH SYSTEMS · ONE card holding the HubSpot icon AND the
   "1 Unlock…4 Fuel" row, matching "YOUR SYSTEMS" / "NEW DESTINATIONS"
   ═══════════════════════════════════════════════════════════════════
   Recipe lifted directly off those two columns' own real container
   (.offer-col-24 on the home page, measured; was hubspot-col-11 (Growth Systems) before the
   rebuild renamed it): border-radius 19px, background
   rgba(250,250,248,.15), padding 19.2px, 1px solid rgb(250,250,249)
   border (theirs is drawn via Framer's [data-border]::after
   convention; this is a new element this file creates, so a plain
   border property does the same job with no pseudo-element
   indirection needed). flex column, same as that container's own
   layout — but justify-content:space-between instead of a fixed gap:
   bac-overlay.js sets this card's height explicitly to match the
   real reference card's, and this card's own content (icon + row)
   needs a DIFFERENT internal gap than 3 stacked pills do to fill that
   same height. space-between distributes whatever's left between the
   two children automatically, for whatever height gets set, instead
   of this needing to compute the right gap by hand too. */
.bac-gs-card{
  display:flex;flex-direction:column;align-items:center;
  justify-content:space-between;
  box-sizing:border-box;
  border-radius:19px;background:rgba(250,250,248,.15);
  padding:19.2px;
  /* opacity lowered (was solid rgb(250,250,249)), per the operator —
     the .bac-rim fill-in animation draws in almost this exact colour
     (rgba(250,250,249,.9)), so a fully-opaque static border of nearly
     the same colour left the animated fill barely distinguishable
     from the border it was filling in on top of. Fainter static
     border, unchanged rim (that animation's own values are untouched,
     per instruction) — the fill now reads as visibly stronger by
     contrast, not because it changed. */
  border:1px solid rgba(250,250,249,.35)}

/* ═══ GROWTH SYSTEMS · the three "YOUR SYSTEMS" feeds, converging ══
   into the shared card, with a travelling band indicating data flow.
   Technique and CONFIG (cycle timing, dasharray/dashoffset mechanism,
   the keyframe's own easing shape) are lifted directly from
   site/index-v8.html's "01-unlock" figure (search that file for
   ".trace"/".tr"/"@keyframes feed") — the operator named it explicitly
   as the reference. Not copied verbatim: index-v8's version drives a
   full hand-drawn SVG plate at a fixed 640×420 viewBox; this is a
   small absolutely-positioned overlay sized to whatever these real
   DOM elements measure at, appended to <body> like the sonar-pulse
   overlay elsewhere in this file (same reasoning: these three pills
   and the card are inside Framer's live component tree, which can
   re-render independent of anything here — an SVG living outside
   that tree can't be touched by it).

   .bac-flow-track is the static state — ALWAYS drawn, resolved
   without JS motion, satisfying the same static-first rule every
   other animation on this site follows. .bac-flow-band is the
   travelling segment: dasharray is [band, huge] so exactly one band
   exists on the path, and animating dashoffset carries it start to
   end — identical mechanism to index-v8's .tr, band width scaled per
   path (see bac-overlay.js) rather than index-v8's fixed 44px, since
   these paths are much shorter than that figure's.

   ⚠ IT LOOPS — flagged, not hidden, same as index-v8's own comment on
   this exact mechanism ("the doctrine's first rung... kept out of the
   one-shot reveal system on purpose"). An ambient "data is flowing"
   indicator is inherently ongoing, not a one-shot reveal; the
   operator asked for index-v8's own line-progression technique
   specifically, which is this same accepted, named exception —
   not a fresh violation invented here. */
.bac-flow-track{fill:none;stroke:rgba(255,255,255,.16);stroke-width:1.25;
  stroke-linecap:round}
/* two bands, one colour language: white for the "YOUR SYSTEMS" feed
   into the shared card (existing data, arriving); Clear Blue for the
   card's own feed out to "NEW DESTINATIONS" (new reach, the accent —
   consistent with --sky being used accent-only sitewide, never as a
   base colour). .bac-flow-band-white is applied in JS only to the
   incoming set (drawFlowLines's cardIsSource:false call); the
   outgoing set keeps this base rule's default. */
.bac-flow-band{fill:none;stroke:#86E1FA;stroke-width:1.25;
  stroke-linecap:round;
  stroke-dasharray:var(--band) calc(var(--len) + var(--band) * 2);
  stroke-dashoffset:var(--band);opacity:1}
.bac-flow-band.bac-flow-band-white{stroke:#FFFFFF}
.bac-flow-band.bac-flow-on{
  animation:bac-flow var(--cycle,4400ms) linear infinite}
@keyframes bac-flow{
  0%   {stroke-dashoffset:var(--band);
        animation-timing-function:cubic-bezier(.62,0,.28,1)}
  30%  {stroke-dashoffset:calc(var(--band) - var(--len))}
  44%  {stroke-dashoffset:calc(-1 * var(--len))}
  100% {stroke-dashoffset:calc(-1 * var(--len))}
}
/* reduced motion: the track alone is already the resolved diagram —
   freeze the band rather than hide it, so the argument (three systems,
   one connection) still reads without implying nothing flows there. */
@media (prefers-reduced-motion:reduce){
  .bac-flow-band.bac-flow-on{animation:none;opacity:0}
}

/* ═══ GROWTH SYSTEMS · the shared card's own border-fill ("rim") ══
   index-v8's "01-unlock" figure doesn't just feed the "ONE CONNECTION"
   card — the card's own border FILLS IN as the connection arrives:
   two paths (top half, bottom half), each starting at the card's
   left-middle (the arrival point, same coordinate the feeds land on)
   and running around to its right-middle (the departure point), drawn
   by walking stroke-dashoffset from --len to 0 rather than travelling
   a band — "not a travelling band, a border accumulates" (index-v8's
   own comment). Same mechanism family as .bac-flow-band, different
   keyframe shape: this one FILLS AND HOLDS rather than repeating a
   pass. Timed off the same --cycle/--len discipline, so it can be
   handed the identical 4400ms cycle and stay in step with the feed
   that triggers it (bac-overlay.js wires this to fire alongside the
   incoming set's arrival, same as index-v8 does).

   ⚠ RESOLVED STATE IS "COMPLETE", NOT "EMPTY" — the base rule (no
   .bac-rim-on) is fully drawn and visible: dashoffset 0, opacity 1.
   That's what a reader with JS off, or prefers-reduced-motion, or a
   PDF capture sees — the connection already made, which is the
   argument, not "nothing has arrived yet". Only .bac-rim-on (added
   only when motion is actually permitted) overrides to the animated
   start state and plays the fill-in. */
.bac-rim{fill:none;stroke:rgba(250,250,249,.9);stroke-width:1.25;
  stroke-linecap:round;
  stroke-dasharray:var(--len);stroke-dashoffset:0;opacity:1}
.bac-rim.bac-rim-on{
  stroke-dashoffset:var(--len);opacity:0;
  animation:bac-rim var(--cycle,4400ms) linear infinite}
/* identical percentages to index-v8's own @keyframes rim: hold empty
   until the feed lands (30%, matching bac-flow's own arrival point),
   fill to complete by 60%, hold complete, then fade WHILE STILL
   COMPLETE and reset behind the fade — resetting a visible border
   would snap rather than dissolve. */
@keyframes bac-rim{
  0%   {stroke-dashoffset:var(--len);opacity:1}
  30%  {stroke-dashoffset:var(--len);opacity:1;
        animation-timing-function:cubic-bezier(.32,0,.24,1)}
  60%  {stroke-dashoffset:0;opacity:1}
  86%  {stroke-dashoffset:0;opacity:1}
  96%  {stroke-dashoffset:0;opacity:0}
  100% {stroke-dashoffset:var(--len);opacity:0}
}

/* ═══ GROWTH SYSTEMS · cross-linked hover between the diagram's
   "1 Unlock…4 Fuel" pills and the detail cards below them ══════════
   Three things happen together, whichever side (diagram pill or
   detail card) is actually hovered/focused — bac-overlay.js decides
   WHICH item's group activates; this file only owns what "active"
   looks like:
     · the diagram pill's own translucent background opacity lifts
       (.15 → .4, same colour, just stronger)
     · the matching detail card's number badge border turns #00AEFF
       and gains a glow
     · the shared HubSpot icon gains a stronger shadow (regardless of
       WHICH of the four items triggered it — one shared indicator,
       not per-item) */
.bac-gs-item{transition:background-color .4s ease}
.bac-gs-item.bac-gs-item-hot{background-color:rgba(250,250,248,.4)}

/* the badge's border is drawn via Framer's [data-border]::after
   convention (confirmed: base border rgba(4,40,57,.1) lives there,
   same pattern used throughout this file for Framer-drawn borders) —
   overriding color needs the same pseudo-element, a plain border
   property on the element itself would do nothing. */
.bac-gs-badge::after{transition:border-color .4s ease}
.bac-gs-badge.bac-gs-badge-hot::after{border-color:#00AEFF!important}
.bac-gs-badge{transition:box-shadow .4s ease}
.bac-gs-badge.bac-gs-badge-hot{
  box-shadow:0 0 0 3px rgba(0,174,255,.22),0 2px 10px rgba(0,174,255,.35)}

.bac-gs-icon{transition:box-shadow .4s ease}
.bac-gs-icon.bac-gs-icon-hot{
  box-shadow:0 0 28px 6px rgba(0,174,255,.4)}

/* ═══ HOME HERO · pinned scrub reveal ══════════════════════════════
   "Own what wins." (dark card, glyph wall) / "Three things decide…"
   (copy column) — bac-overlay.js wires the structure (spacer + sticky
   pin, inserted at runtime) and drives a single --hero-scrub value
   (0→1, 1:1 with scroll position, fully reversible); this file only
   owns what that value DOES.

   ⚠ FOUR ATTEMPTS BEFORE THIS ONE, EACH WRONG DIFFERENTLY.
   (1) flex-grow on both — Chrome didn't resolve it the way the spec
   suggests here (measured: card's native grow:1 to 0 while copy kept
   its native 0.4 capped copy at ~489px of a 1152px row, not the full
   remainder a lone nonzero grower should get).
   (2) card on flex-grow:1, copy centred with auto margins — flex-grow
   resolves BEFORE auto margins see what's left, so the card's grow:1
   claimed all the leftover space and the margins got nothing (measured
   both at 0px).
   (3) both on explicit widths, relying on the row's OWN
   justify-content:center to centre the pair as a unit — closer, but
   "the pair" means the CARD moves too: as it shrinks, centring the pair
   shifts the card's left edge rightward off the row's own edge, which
   reads as the card drifting diagonally (translateY up, PLUS this
   sideways creep) instead of moving straight up, and drags the bleed
   fix's own anchor (the card's left edge) away from the true viewport
   edge with it.
   (4) card stayed a shrinking flex item (flex-start, not the row's
   center, so it at least stopped drifting) paired with a MEASURED bleed
   distance on the glyph-wall's own fill layer — better, but a measured
   distance is still a guess that goes stale the moment the real margin
   at the live breakpoint differs from whatever it was when measured;
   confirmed a gap could still reappear at a width this wasn't checked
   against.

   FIX, per the operator: stop shrinking the card and stop measuring a
   bleed distance at all. The card becomes a position:absolute layer
   sized 100vw/100vh outright (see .bac-hero-scrub-left below) — there
   is no gap to close because there is nothing left to measure. Its
   real content (headline + the customer-logo row) moves into a new
   inner wrapper, .bac-hero-content-wrap, kept aligned with the row's
   own established margins (bac-overlay.js measures and re-measures
   those on resize — but that only ever positions TEXT now, never the
   bleed, so it going briefly stale during a resize costs nothing worse
   than the text drifting a few px for a moment). The copy column stays
   pulled OUT of flex flow (position:absolute) and centred with its own
   calc()'d left/width, genuinely independent of whatever the card is
   doing — z-index keeps it above the now full-bleed card. */
.bac-hero-scrub-spacer{position:relative;width:100%}
.bac-hero-scrub-pin{position:sticky!important;top:0!important}
/* the row's own background is an opaque off-white (rgb(250,250,248)) —
   harmless originally (nothing painted behind it worth seeing), but the
   card now lives BEFORE it in the section's DOM order (so the card's
   position:absolute doesn't inherit the row's own inset as its
   positioning context — see bac-overlay.js), and same-stacking-level
   elements paint in document order: the row's opaque background was
   covering the now full-bleed card with a blank rectangle sized to the
   row's own (inset) bounds. Confirmed directly (rowBg computed to
   rgb(250,250,248), matching the exact shape of the gap). */
.bac-hero-scrub-row{justify-content:flex-start!important; background:transparent!important}
/* the clipping wrapper — a ROW child (position:relative, confirmed
   already on the native element), anchored via right:0/width:100vw
   rather than left:0/width:100vw. right:0 lands exactly on the row's
   own boundary (0 in the row's own coordinate space — no measurement),
   and because 100vw is, on any real viewport, wider than the distance
   back from the row's right edge to the true left edge, the resulting
   left edge is guaranteed to land at or past x:0 regardless of
   breakpoint — again no measurement, no resize handler. overflow:hidden
   then clips the card (which just fills this box edge-to-edge) to
   that same shape: full bleed on the left, a hard stop at the row's
   real right edge on the right — see wireHeroBand's own comment in
   bac-overlay.js for the fuller account of why the card's own left:0
   version bled past that edge too. */
.bac-hero-left-outer{
  position:absolute!important;
  right:0!important; top:0!important;
  width:100vw!important; height:100%!important;
  overflow:hidden!important;
  z-index:0!important}
.bac-hero-scrub-left{
  /* explicit width/height, not inset:0 — the card's own NATIVE
     flex-basis:0 (a leftover from when it was a real flex item in the
     row) kept winning over an implicit inset-derived width even after
     position:absolute took it out of flex flow (measured directly:
     left/right/top/bottom all correctly 0, width still computed to
     1px). flex-basis has no defined effect on an out-of-flow element
     per spec, but explicit width/height sidesteps whatever's actually
     happening here rather than relying on that being honoured. */
  position:absolute!important;
  left:0!important; top:0!important;
  width:100%!important; height:100%!important;
  transform:translateY(calc(var(--hero-scrub, 0) * -100%))}
/* the headline + logo row, pulled out of the now-100vw card so they
   stay aligned with the rest of the page's content grid instead of
   also bleeding to the edge with the background.
   ⚠ display:flex + these exact properties (read off the card's own
   computed style before moving anything: row/center/center/nowrap/10px
   gap) are load-bearing, not cosmetic — the headline's own wrapper is
   a flex ITEM sized by flex-grow/basis, which only resolves against a
   flex CONTAINER. Moving it into a plain block div with none of this
   collapsed it to a ~64px sliver and wrapped "Own what wins." one
   letter per line (confirmed directly). This wrapper has to reproduce
   the card's own flex context, not just its box, for that sizing to
   still resolve the same way. */
.bac-hero-content-wrap{
  position:absolute!important; top:0!important; height:100%!important;
  display:flex!important; flex-direction:row!important;
  justify-content:center!important; align-items:center!important;
  flex-wrap:nowrap!important; gap:10px!important}
/* sits between the card (z-index 0) and the copy column (bumped to 2
   below) — see wireHeroBand's own comment on why this exists: the card
   only ever translates, it never shrinks, so its dark background is
   still behind the FULL viewport width at any progress short of 1,
   including the gaps either side of the copy column (centred at up to
   60%, not 100%). Doesn't move; fades in with --hero-scrub so the
   space around the copy column resolves to light in step with the
   card's own exit instead of showing dark through the gaps.

   ⚠ RAMPS TO FULL OPACITY BY ~25% PROGRESS, NOT LINEARLY ACROSS THE
   WHOLE SCRUB. The outer copy box (.bac-hero-scrub-right) only ever
   paints its OWN opaque white over its OWN (still-growing) area — this
   layer is the only thing covering everything OUTSIDE that box, right
   up to the true viewport edge. A straight 1:1 opacity tie to
   --hero-scrub left that outside area a visibly translucent grey-blue
   blend for most of the scroll (confirmed: the operator's own
   screenshot caught it mid-fade), which read as unresolved rather than
   "becoming white". Still a genuine ramp, not a toggle — a hard snap
   at progress>0 would reintroduce the instant-jump the operator
   explicitly ruled out earlier — just a fast one, so the visible blend
   is a brief beat at the very start of the scroll instead of stretching
   across most of it. */
.bac-hero-fake-bg{
  position:absolute!important;
  left:0!important; top:0!important;
  width:100vw!important; height:100%!important;
  z-index:1!important;
  background-color:rgb(250, 250, 248)!important;
  opacity:min(1, calc(var(--hero-scrub, 0) * 4))!important;
  pointer-events:none!important}
.bac-hero-scrub-right{
  /* out of flex flow entirely — its position no longer depends on
     whatever width the card currently has, only on the row itself
     (position:relative, confirmed already on the native element).
     z-index above both the card (0) and the fake bg (1), so it isn't
     hidden underneath either. Its own background was always
     transparent — fine when the card was narrow and the page's own
     off-white ground (rgb(250,250,248), matched here) showed through
     behind it, wrong now that the card is a full-viewport layer
     sitting behind EVERYTHING: without this, the dark navy card paints
     straight through and the copy's own dark navy text reads as
     barely visible dark-on-dark (confirmed directly). */
  position:absolute!important;
  top:0!important; height:100%!important;
  z-index:2!important;
  background-color:rgb(250, 250, 248)!important;
  /* matches the site's own border convention — read directly off the
     row's native [data-border="true"]::after (border-width:0 1px,
     border-color:rgba(4,40,57,.25)), the same hairline used to divide
     columns/sections everywhere else on the page. A plain border here
     rather than replicating the ::after pattern, since this box is
     already something bac-overlay.js built at runtime, not a native
     Framer element with that pseudo-element wired up. Right edge only —
     this box's OWN left edge isn't a section boundary the rest of the
     page marks this way. */
  border-right:1px solid rgba(4, 40, 57, .25)!important;
  /* fills out to 100% of the row at the end state, per the operator —
     the READABLE TEXT is capped separately (.bac-hero-copy-inner,
     below); this box is the growing background/boundary, not the text
     column. */
  width:calc(
    (var(--hero-right-start, 0.3155) +
     var(--hero-scrub, 0) * (1 - var(--hero-right-start, 0.3155))) * 100%
  )!important;
  /* interpolates from its NATIVE left edge (flush against the card —
     the card's own native width fraction, since there's no gap between
     them) to 0 — flush with the row's own left edge, which is where a
     box AT 100% width has to sit regardless of any centring maths. */
  left:calc(
    var(--hero-left-start, 0.6845) * (1 - var(--hero-scrub, 0)) * 100%
  )!important;
  /* the column's own native side padding is a fixed 24px — fine at its
     narrow resting width, but once it's grown it needs the same ~64px
     inset the rest of the page's full-width text uses (confirmed
     against "Origin, Our Product."'s own headline: 64px in from its
     row) or the copy reads flush against its own box edge while every
     other section's text sits inset from it. Interpolated from the
     native 24px rather than snapping straight to 64px, so the column's
     padding doesn't visibly jump the instant the scrub starts. */
  padding-left:calc(24px + var(--hero-scrub, 0) * 40px)!important;
  padding-right:calc(24px + var(--hero-scrub, 0) * 40px)!important}
/* the text's own reading column, capped independently of the outer box
   (.bac-hero-scrub-right, above) so the actual line length never grows
   to the full 100vw the outer eventually reaches. Interpolates from
   100% at rest (unchanged from the original, un-scrubbed layout — a
   flat 60% here wrapped the resting three-line copy into five, per the
   operator) down to 60% at the end state. Centred within the outer via
   margin:auto (its parent's align-items:flex-start, confirmed on the
   native element, doesn't stretch children, so this needs its own
   explicit centring rather than relying on that). */
.bac-hero-copy-inner{
  width:calc((1 - var(--hero-scrub, 0) * .4) * 100%)!important;
  margin:0 auto!important}
.bac-hero-scrub-copy{
  text-align:left!important;
  font-size:calc(
    var(--hero-copy-size-start, 16px) +
    var(--hero-scrub, 0) * (30px - var(--hero-copy-size-start, 16px))
  )!important}

/* The detail card's COPY recedes when its item is not the active one — the badge glow
   alone was carrying the state, which reads on the diagram pill but not down here where
   the reader actually is. 60% is enough to say "not this one" without making the copy
   unreadable for anyone scanning all four. Same 400ms as the rest of the group.

   The number and its container stay at full strength, per the operator: the badges are
   the row's index and should read evenly across all four, so dimming them would make
   the row look broken rather than focused. The card has exactly two children — the
   badge and a text column — so excluding the badge leaves precisely the copy. */
.bac-gs-detail > :not(.bac-gs-badge){opacity:.6;transition:opacity .4s ease}
.bac-gs-detail.bac-gs-detail-hot > :not(.bac-gs-badge){opacity:1}

/* ── GROWTH SYSTEMS · engagement timeline ──────────────────────────
   The bar's fill and its knob container are both width:33% in the page's own CSS;
   wireScopeTimeline sets them to 33/66/100 together. The width transition is what
   makes the bar read as progress rather than a jump cut. */
.bac-scope-fill{transition:width .6s cubic-bezier(.22,1,.36,1)}

/* The stage that the bar has reached reads at full strength, the other two recede —
   same 60% and same 400ms as the Unlock/Unify/Activate/Fuel row above, so the two
   groups on this page behave identically. Here the whole stage dims, including its
   "Day 1" label: unlike that row there is no number badge holding the index, so
   nothing needs to stay lit for the row to still scan as three stages.

   !important is load-bearing here, not a shortcut: the page hard-codes this row's
   resting look as INLINE opacity on each column — 1 on Day 1, 0.6 on the other two —
   which is a stylesheet rule's only real rival. A stylesheet !important outranks a
   non-important inline style, which is exactly the case this is. */
.bac-scope-stage{opacity:.6!important;transition:opacity .4s ease}
.bac-scope-stage.bac-scope-stage-hot{opacity:1!important}

/* ── HOME HERO · the BAC mark rises as the copy panel grows ────────
   Three sky circles climb out from behind the copy block and settle in
   a row above it, staggered, driven by --hero-scrub — the same single
   0→1 progress value the panel's own width and type size run on, so
   they cannot drift out of step with it. Scroll-driven, so it is
   reversible and holds wherever the reader stops, and at scroll 0 the
   value is 0 and nothing has moved: "only start rising once the
   scrolling starts" is not a separate trigger, it is what a progress
   of 0 already means.

   ═══ GEOMETRY IS THE MARK'S OWN — MEASURED OFF THE SYMBOL ═══
   Read from the nav symbol's path data (viewBox 0 0 114.902 20.126),
   not eyeballed. Every circle has outer radius 9.165 (diameter 18.33);
   what differs is the inner radius of each ring, and the first pass at
   this got two of the three numbers wrong, so the workings are here:

     circle 1  ring, inner subpath starts y=16.802 → inner r 7.637
               → stroke 1.528 =  8.33% of the diameter
     circle 2  ring, inner subpath starts y=13.747 → inner r 4.582
               → stroke 4.583 = 25.00% of the diameter — THREE TIMES
               circle 1. This is what makes the mark read ○◎●, a ring
               thickening into a disc, rather than two identical rings
               beside a dot. Missing it is what made the middle circle
               look wrong.
     circle 3  solid; one subpath, no inner.

   SPACING IS ZERO. Centres sit at x 9.165, 27.495 and 45.825 — 18.33
   apart, which is EXACTLY one diameter, so the circles are tangent and
   their outer edges touch. (27.495 is circle 2's own centre, not the
   gap; reading it as the gap is what put half a diameter of air
   between them before.)

   ⚠ DRAWN, NOT <use>d, deliberately. The symbol's paths carry
   fill="var(--ink, …)", so re-using it and recolouring would mean
   redefining --ink on an ancestor — the page's primary ink token —
   and hoping nothing else in that subtree reads it. Three circles are
   cheaper to draw than that risk is to audit, and they need to split
   into three independently-animated elements anyway, which one <use>
   of a single symbol cannot do.

   ⚠ OUT OF FLOW, so the copy does not move. The operator's constraint:
   the text stays exactly where it is and the circles overlay above it.
   Hence position:absolute against .bac-hero-copy-inner, which gets a
   position for the purpose and nothing else. */
.bac-hero-copy-inner{position:relative}

.bac-hero-marks{
  /* one number drives the mark; everything else is a ratio of it */
  --mark-size:30px;
  /* how far each circle travels, and therefore the clip window's own
     height. A ratio, not a constant, so it keeps scaling with the
     circle: it must exceed 1 or the circle cannot sit COMPLETELY below
     the window's bottom edge at rest and a sliver stays showing before
     the scroll begins. */
  --mark-rise:calc(var(--mark-size) * 1.9);
  /* the slice of the scrub ONE circle takes to arrive. Shorter than 1
     so that the last one still lands before the panel finishes
     growing, rather than the two events ending together. */
  --mark-span:.55;
  position:absolute;left:50%;bottom:100%;
  transform:translateX(-50%);
  display:flex;align-items:flex-start;
  gap:0;                            /* the mark's own: tangent circles */
  /* decoration over live copy — never eat a pointer event meant for
     the text or the hero's glyph wall underneath it */
  pointer-events:none}

/* THE CLIP WINDOW is what sells "from behind the text": its bottom
   edge sits on the copy block's top edge (bottom:100% on the parent),
   so a circle translated a full window-height down is not merely
   invisible, it is exactly where the text begins. It emerges from
   that edge rather than fading in somewhere above it. */
.bac-hero-mark{
  display:block;overflow:hidden;
  width:var(--mark-size);height:var(--mark-rise)}

.bac-hero-mark > i{
  /* THE STAGGER. Each circle reads the same --hero-scrub but starts
     its own window later, so they arrive one after another off a
     single shared value — no second timer, nothing to fall out of
     sync with. clamp() holds it at 0 before its turn and 1 after. */
  --t:clamp(0, (var(--hero-scrub, 0) - var(--mark-delay, 0)) / var(--mark-span), 1);
  display:block;
  width:var(--mark-size);height:var(--mark-size);
  border-radius:50%;
  border:calc(var(--mark-size) * .0833) solid var(--sky);
  transform:translateY(calc((1 - var(--t)) * var(--mark-rise)))}

/* ○◎● — the ring thickens, then fills. See the geometry note above:
   these two percentages are the mark's own measured strokes, and the
   step between them is the thing the eye actually reads. */
.bac-hero-mark:nth-child(2) > i{border-width:calc(var(--mark-size) * .25)}
.bac-hero-mark:last-child > i{background:var(--sky)}

/* ── EYEBROW MARKERS · sky, not the bright blue ────────────────────
   Operator instruction, across home / origin / growth-systems. Tagged
   by bac-overlay.js from the marker's own signature rather than by
   class, because the per-page hashed names collide with opposite
   meanings — see ensureEyebrowMarks() for the case that forces it.

   The halo keeps its 20% relationship to the square rather than being
   given a flat lighter value: it is the same mark at the same
   transparency, moved to a different hue, so the pair still reads as
   one object. */
.bac-eyebrow-dot{background-color:var(--sky)!important}
.bac-eyebrow-halo{background-color:rgba(134,225,250,.2)!important}

/* ── CUSTOMER-PROOF BAND · navy, not near-black ────────────────────
   Operator instruction: match the hero. The page paints this section
   in --ink-deep (#0a0f17); the glyph wall mounted over it now runs on
   --ink, the hero's ground, so the section's own colour is corrected
   underneath to match rather than left as a darker layer nothing is
   meant to see. See PROOF_WALL in bac-overlay.js for the pairing. */
.proof{background-color:var(--ink)!important}

/* ── HERO HEADLINE · the box fits the words ────────────────────────
   Operator instruction. Each hero <h1> was laid out at the full width
   of its column — measured on home, a 1216px box around 432px of
   text — so its own box said almost nothing about where the words
   actually are.

   That is not cosmetic here. The glyph wall's entrance originates at
   an ELEMENT's centre (see HERO_WALL_HOME's entranceAt), and the
   centre of a 1216px box sits 392px to the right of the centre of the
   words inside it. The pulse was being born in empty space beside the
   headline rather than in it. Fitting the box to the content makes
   the element's geometry honest, and the origin correct as a
   consequence rather than by a second measurement kept in sync.

   Safe by measurement, not by assumption: the text is left-aligned in
   a flex column, so shrinking the box moves nothing. Verified on all
   three pages — the rendered text box is identical before and after,
   and the only element whose rect changes at all is the <h1> itself.

   There is exactly one <h1> per page and it is always the hero
   headline (the rebuild adds it; see the spec), so this needs no
   narrower selector.

   ⚠ IT DOES NOT FIT A WRAPPING HEADLINE, and that is a property of
   fit-content, not a bug here: once text wraps, fit-content resolves
   to the AVAILABLE width, not the longest line. Measured on Origin,
   whose headline runs to three lines — a 486px box around 326px of
   text, still fit-content. So this rule tidies the box; it is NOT
   what makes the pulse origin correct. That is handled where it
   belongs, by measuring a Range over the text itself — see
   HERO_WALL_HOME's entranceAt. Do not delete that on the strength of
   this rule existing. */
h1.text{width:fit-content}

/* ── ORIGIN · "The decision moved" frame fits its image ────────────
   Operator instruction. The frame (.problem-col-17) and its inner
   plate (.problem-box-2) both carried aspect-ratio:2.22171 — the OLD
   export's shape (774x348). dash.png is 2450x1550, i.e. 1.58065, so
   object-fit:contain drew it 551px wide inside a 774px box and left
   111px of slack each side. What showed through that slack was the
   OTHER gradient image sitting behind it, also blue, so it read as a
   panel not quite lining up rather than an obvious gap.

   ⚠ THE FIX IS TO SHRINK THE WIDTH, NOT TO GROW THE HEIGHT, AND THE
   OBVIOUS VERSION IS WRONG. Re-ratioing at the existing width:790px
   was tried first and it broke the section: the frame is positioned
   bottom:-105px, so its height grows UPWARD, and taking it from 364
   to 506 moved its top edge from y241 to y100 — straight over the
   three copy columns ("They run in parallel…", "Hours pass…", "You
   are not buying a replacement…") which sit at y101. At z-index 3
   with an opaque gradient it covered them outright. Measured, not
   guessed: the copy's own rect is x224-592 / 616-984 / 1008-1376
   against a frame spanning x405-1195.

   So the plate keeps its HEIGHT (348px, which holds the frame at its
   original 364 and leaves everything above it untouched) and takes
   its width from the image's ratio — 348 x 1.58065 = 550. The frame
   drops to width:auto and hugs that plus its 8px padding, landing at
   566. Centring is already handled by the frame's own left:50% with a
   -50% translate and needs nothing added.

   Written as the file's real pixel dimensions rather than a decimal,
   so the next person swapping the image can see where it comes from.

   The bottom 105px stays clipped by .problem-row-3 — that is the
   intended "peeking dashboard" and is not a side effect of this. */
.problem-box-2{
  aspect-ratio:2450 / 1550!important;
  width:auto!important;
  height:348px!important}
.problem-col-17{
  aspect-ratio:auto!important;
  width:auto!important;
  height:auto!important}

/* ── ORIGIN · the same frame, scaled 1.6x and given more room to peek ──
   Operator instruction. .problem-box-3 is the white card behind the frame —
   its native height:300px is what .problem-row-3 (height:min-content) was
   actually measuring at 300px; the frame itself is absolutely positioned and
   contributes nothing to that. Grown to 400px, which grows the row with it.

   The plate scales by 1.6x on the same height-first logic as above: 348 x
   1.6 = 556.8, width still follows from the image's own ratio (556.8 x
   1.58065 = 880). Frame stays height:auto, hugging that plus its 8px padding.

   ⚠ SWITCHED FROM bottom:-105px TO top:42px, NOT LEFT AS-IS. The frame was
   bottom-anchored, so at 1.6x its extra ~209px of height would have grown
   entirely upward — climbing further over the three copy columns above it,
   the exact failure mode the original fix was written to avoid. Measured
   the existing gap instead of guessing: frame top sat 41px below the row's
   top edge (1209 vs 1168 in the live render), i.e. the 42px this asks for
   already, just expressed as a bottom offset that only held it by
   coincidence of the old sizes. top:42px keeps that distance exact and
   constant regardless of how tall the frame or the row get from here. */
.problem-box-3{
  height:400px!important}
.problem-box-2{
  height:556.8px!important;
  border-radius:16px!important}
.problem-col-17{
  top:42px!important;
  bottom:auto!important;
  border-radius:16px!important}

/* ── ORIGIN · the "decision moved" headline column fills its row ───
   Operator instruction. .problem-col-6 is the sole child of
   .problem-col-5 and carried width:50% with nothing else in the row
   to take the other half — the headline sat in the left 282px of a
   563px content box, half the row blank.

   ⚠ SAME CLASS NAME, DIFFERENT ELEMENT ON HOME. Home's export reused
   "problem-col-6" as one of three classes on an unrelated element (a
   bordered image panel, combined with .problem-col-4 and .problem-col-5
   on the SAME node rather than nested) — scoping by the class alone
   would have widened that too. :not() on the two classes it never
   carries alongside on origin excludes it without touching home. */
.problem-col-6:not(.problem-col-4):not(.problem-col-5){
  width:100%!important}

/* ── ORIGIN + GROWTH · .partner's floor, dropped from 40vh to 10vh ─────
   Operator instruction. Native on both pages; .partner-inner itself is
   already height:auto (content-driven — see the conversation this
   followed), so this only ever set a FLOOR under short content, never a
   fixed height. Left the phone-band override in components/responsive.css
   (min-height:0 at <=767) untouched — that is a separate, already-correct
   decision to drop the floor entirely at phone width, not something this
   value change should disturb. */
.partner{
  min-height:10vh!important}

/* ── HOME · Origin showcase panels — the image runs out of frame ───
   See ensureShowcaseBleed() in bac-overlay.js for why these are
   tagged from structure rather than addressed by their hashed class
   names, and for what the plate/frame pair is.

   The plate is sized FROM THE PICTURE — the artwork's own 2450x1550
   at the frame's height — instead of being the frame's content box
   with the image cropped into it. The frame already clips, so
   everything past its edge is cut and the dashboard bleeds out to the
   right, which is the behaviour Origin's cards already had.

   Anchoring the plate left is load-bearing: the frame centres its
   child by default, and a plate twice the frame's width then overflows
   EVENLY — measured at -251px on the left and +251px on the right,
   which cuts the dashboard's left edge as well as its right and loses
   the sidebar the composition starts from.

   ⚠ IT TAKES align-items, NOT justify-content, AND THAT IS NOT
   INTERCHANGEABLE HERE. The frame is a COLUMN flex container
   (flex-flow:column), so justify-content runs the VERTICAL axis and
   the horizontal one is align-items. Setting justify-content alone
   changed nothing and the plate stayed centred. Both are set below:
   align-items is the one doing the work, justify-content is harmless
   and keeps the intent readable if the direction ever changes. */
.bac-showcase-frame{
  align-items:flex-start!important;
  justify-content:flex-start!important;
  border-radius:28px!important}
/* ⚠ THE PLATE'S WIDTH IS THE ONE NUMBER THAT MATTERS, and it is set
   as "how much of the picture should be visible", not as a size.
   --showcase-visible is the fraction of the artwork the frame shows
   before cutting it off; the plate is that fraction inverted against
   the frame's own content box, so the dial is legible and the frame
   can change width without this needing a new constant.

   Three earlier passes each missed by fixing the wrong thing. The
   plate was the frame's content box with the image cropped into it
   (dashboard inset, blue both sides); then the artwork's aspect at
   height:100%, which still derived width from the frame's HEIGHT and
   capped it at 1018; then max-content, which took the image's own
   2450 and showed only its left fifth. The operator's call is 65%.

   Height follows from the artwork's ratio, so the image is never
   distorted and never re-cropped — at 2450/1550 the plate is 768x486
   and the image fills it exactly, which is why object-fit has nothing
   left to do. */
.bac-showcase-plate{
  --showcase-visible:.65;
  flex:0 0 auto!important;
  width:calc(100% / var(--showcase-visible))!important;
  height:auto!important;
  aspect-ratio:2450 / 1550!important;
  border-radius:20px!important}

/* ⚠ THE FILL LAYER HAS TO REJOIN THE FLOW. Framer wraps the image in
   a position:absolute inset:0 layer. Left absolute it contributes no
   intrinsic size and takes its box from the plate's padding box —
   measured collapsing the plate to 0x0 when the plate was sizing to
   content. Relative with auto insets keeps the pair in step whatever
   drives the width. */
.bac-showcase-plate > *{
  position:relative!important;
  inset:auto!important;
  width:100%!important;
  height:100%!important}

.bac-showcase-img{
  width:100%!important;
  height:100%!important;
  max-width:none!important;
  border-radius:20px!important}

/* ═══ CONTACT · the real HubSpot form ═════════════════════════════
   The page shipped a STATIC MOCK of this form — three bordered divs
   holding the words "Full Name", "Business email" and "What you're
   trying to change", a div that said "Submit", and no <input>, no
   <form> and no action anywhere on the page. Measured before
   replacing it: 0 inputs, 0 forms.

   The embed is the one from the latest handover package
   (bac-v8-launch-2026-08-23/site/index-v8.html) — same portal, same
   form id, same developer embed script. What is NOT carried over is
   that package's own visual treatment: the rules below restate THIS
   page's field design (1px #04283940 border, 6px radius, 12px/17px
   padding, 42px min-height, 14px label ink at 72%) read off the mock
   it replaces, so the live form looks like the page it is in rather
   than like the other package.

   ⚠ EVERYTHING IS !important AND SELECTORS ARE DEEP. HubSpot's embed
   injects its own stylesheet into the page at runtime, after this
   file, and it ships its own padding/border/font on every control. */

.hs-form-html{width:100%}
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-Form{font-family:inherit}

/* the rows: name and email share a line, the message takes its own,
   matching .contact-row-3's 8px inner gap and .contact-col-7's 12px
   stack exactly. */
/* ⚠ THE EMBED PADS ITS OWN STEP, and it is invisible to the obvious
   check. .hsfc-Step__Content carries 40px of padding on all four sides,
   so the fields laid out inside 400px of a 480px column — 40px short on
   each side — and read as not filling. getBoundingClientRect and
   clientWidth BOTH returned 480 for that element, because clientWidth
   includes padding; the tell was the rows themselves, which measured
   196 + 8 + 196 = 400 and started 40px in from the container's left
   edge. Zeroed here on the step and its wrapper so the form fills the
   column the page gives it (padding:0, not just left/right — the 40px
   top/bottom was still adding dead space above/below the fields). */
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-Form,
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-Step,
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-FormWrapper{
  padding-left:0!important;padding-right:0!important}
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-Step__Content{
  display:flex!important;flex-flow:row wrap!important;
  gap:12px 8px!important;align-items:flex-start!important;
  padding:0!important}
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-Row{margin:0!important;flex:1 1 100%!important}
/* The live form carries five fields where the mock drew three: Full
   Name*, Work Email*, Your Role, Company Name* and the message. The
   four short ones pair off two to a line — the same two-up the mock
   used for name/email — and the message takes the full width on its
   own. min-width forces the pair to stack rather than crush below
   ~380px of form. */
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-Row:has([name$="/firstname"]),
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-Row:has([name$="/name"]),
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-Row:has([name$="/email"]),
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-Row:has([name$="/jobtitle"]),
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-Row:has([name$="/company"]){
  flex:1 1 0!important;min-width:170px!important}
/* an empty row still takes part in a flex gap, and the embed emits
   two of them (a leading spacer and its hidden consent block), which
   showed as phantom gaps in the stack. */
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-Row:not(:has(input,textarea,select)){
  display:none!important}

/* the controls themselves — the mock's own box, restated */
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-TextInput,
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-TextareaInput,
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-Dropdown{
  box-sizing:border-box!important;
  width:100%!important;min-height:42px!important;
  padding:12px 17px!important;
  border:1px solid rgba(4,40,57,.25)!important;border-radius:6px!important;
  background:transparent!important;
  font-family:inherit!important;font-size:14px!important;line-height:normal!important;
  letter-spacing:-.02em!important;color:var(--ink)!important;
  outline:none!important;box-shadow:none!important;
  /* the border is the only thing that moves on hover/focus, so it is
     the only thing that transitions — a transition on `all` here also
     animates the height as the textarea grows, which reads as lag. */
  transition:border-color 160ms ease!important}
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-TextareaInput{
  min-height:96px!important;resize:vertical!important}
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-TextInput::placeholder,
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-TextareaInput::placeholder{
  color:rgba(4,40,57,.72)!important;opacity:1!important}

/* ── HOVER / ACTIVE · the border goes to full-strength primary ──
   Operator instruction. The resting border is the ink token at 25%;
   hovered or focused it is the same colour at 100%, so the state
   change is a weight change in one colour rather than a new one.
   :focus-within is included because a control wrapped by HubSpot in
   its own div can take focus on a child. */
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-TextInput:hover,
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-TextareaInput:hover,
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-Dropdown:hover,
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-TextInput:focus,
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-TextareaInput:focus,
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-Dropdown:focus,
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-Row:focus-within .hsfc-TextInput,
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-Row:focus-within .hsfc-TextareaInput{
  border-color:var(--ink)!important}

/* labels become placeholders (see the JS), so they are only hidden
   once that has actually happened — never on markup that still needs
   them. */
body.hsf-ph .hs-form-html [data-hsfc-id="Renderer"] .hsfc-FieldLabel{
  position:absolute!important;width:1px;height:1px;overflow:hidden;
  clip-path:inset(50%);white-space:nowrap}

.hs-form-html [data-hsfc-id="Renderer"] [aria-invalid="true"]{
  border-color:#b3261e!important}
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-ErrorAlert,
.hs-form-html [data-hsfc-id="Renderer"] [class*="ErrorMessage"]{
  font-family:inherit!important;font-size:12px!important;color:#b3261e!important;
  margin-top:6px!important}

/* HubSpot's own consent text and submit are both replaced by the
   page's: the consent copy sits below in the page's own markup, and
   the submit is .contact-row-4 proxying clicks to this one. Hidden,
   not removed — the embed re-renders and a removed node comes back. */
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-DataPrivacyField,
.hs-form-html [data-hsfc-id="Renderer"] .hsfc-RichText,
body.hsf-proxy .hs-form-html [data-hsfc-id="Renderer"] .hsfc-NavigationRow{
  display:none!important}
/* …except when the navigation row is carrying a form-level error,
   which is the one thing in it the reader still needs. */
body.hsf-proxy .hs-form-html [data-hsfc-id="Renderer"]
  .hsfc-NavigationRow:has(.hsfc-NavigationRow__Alerts:not(:empty)){display:block!important}

/* the proxy submit reads as a control */
[data-hs-submit]{cursor:pointer;user-select:none}
[data-hs-submit]:focus-visible{outline:2px solid var(--sky);outline-offset:3px}

/* ── the submitted state ──────────────────────────────────────────
   Replaces the form in place rather than appearing beside it. */
.bac-form-done{
  display:flex;flex-flow:column;align-items:flex-start;gap:8px;
  width:100%;padding:4px 0 0;
  opacity:0;translate:0 8px;
  transition:opacity 520ms cubic-bezier(.22,1,.36,1),
             translate 520ms cubic-bezier(.22,1,.36,1)}
.bac-form-done[hidden]{display:none}
.is-form-done .bac-form-done{opacity:1;translate:0 0}
.bac-form-done-i{display:block;width:28px;height:28px;fill:var(--ink)}
.bac-form-done h3{
  margin:0;font-family:inherit;font-size:20px;font-weight:500;
  letter-spacing:-.02em;color:var(--ink)}
.bac-form-done p{
  margin:0;max-width:52ch;font-size:14px;line-height:140%;
  letter-spacing:-.02em;color:rgba(4,40,57,.72)}
/* on success the form and its submit go, the consent line with them —
   it describes a submission that has already happened. */
.is-form-done .hs-form-html,
.is-form-done [data-hs-submit],
.is-form-done .contact-col-15{display:none!important}
@media (prefers-reduced-motion: reduce){
  .bac-form-done{transition-duration:1ms!important}
}

/* ── GROWTH SYSTEMS · hero logo carousel — shifted right, not centred ────────
   Operator report. .hero-inner is the logo rail's own small, absolutely-
   positioned wrapper on this page (a child of .hero, containing .hero-box-2
   and the rail and nothing else) — the SAME hashed class name home uses for
   its own, entirely different, position:relative hero-content wrapper (an
   800px-tall box the rail sits deep inside, several levels down). The two
   can't be told apart by DOM shape — both sit directly inside .hero — so
   scoped by page instead, the one place the two pages themselves differ:
   <html> carries page-growth here and page-home there.

   Growth's own compiled CSS sets .hero-inner width:80% but left:171px — a
   raw pixel offset from an old fixed-width export, not a proportional one,
   so it only ever centred at the one viewport width it was measured at.
   Confirmed live at 1004px: left was 171px, right 29.8px, nowhere near
   symmetric. width:80% already means the two sides should split the
   remaining 20% evenly — 10% each — so left:10% replaces the fixed px with
   the value consistent with its own width, centring it at any viewport
   width instead of one. */
.page-growth .hero-inner{
  left:10%!important}

/* ── GROWTH SYSTEMS · the six system/destination icons — square, uniform ─────
   Operator instruction, following the icon swap in growth-systems/index.html
   (the generic curved-arrow placeholder replaced with one specific SVG per
   item: Loan Origination, Practice Management, Portfolio Management,
   Applications, Warehousing, Reporting). Their shared wrapper class list
   (.hubspot-svg/-2/-3/-5/-6/-7 — one combined native rule, confirmed
   identical: width:15px;height:13px) was sized for the OLD icon's own
   non-square proportions. The new source files are all 24x24 already, so
   nothing here needs cropping — object-fit:contain on the <img> (set inline
   in the markup) just needs an actually-square box to contain within. 16px
   rather than the native 15x13, close to the original without introducing a
   new arbitrary number. */
.hubspot-svg,
.hubspot-svg-2,
.hubspot-svg-3,
.hubspot-svg-5,
.hubspot-svg-6,
.hubspot-svg-7{
  width:16px!important;
  height:16px!important}

/* ── FOOTER · height fit-to-content, hard rule ──────────────────────
   Operator instruction. .footer-col, .footer-row-2 and .footer-row-4
   already carry height:min-content natively (all three pages, same
   compiled rule) — checked live, computed height already tracks
   content (e.g. .footer-row-2 at 181px on home, changing with its
   own padding, not pinned to a fixed number). Set explicitly here
   anyway rather than left to depend on the compiled CSS continuing to
   say so: a re-export is exactly the kind of change that could swap
   min-content for a measured px value without anyone touching this
   file, and there would be nothing here to catch it. */
.footer-col,
.footer-row-2,
.footer-row-4{
  height:min-content!important}
