/* ==========================================================================
   Banner — homepage intro section (two-column layout, glowing video frame,
   animated background mosaic). Naming here is intentionally our own
   (banner / mosaic / shade / bloom, not "hero" / "tile" / "scrim") so the
   code doesn't read as a lift from any reference site.
   ========================================================================== */

.banner {
  position: relative;
  overflow: hidden;
  background: var(--color-bg);
}

/* --- Background mosaic: rows drift horizontally, all in the same
   direction, each at its own (slow) speed. Each row's tile sequence is
   duplicated so the loop point is invisible (0% -> -50% lands exactly on
   the seam), sized generously (48 tiles/row) so a row never visibly runs
   out before that seam. --- */

.banner__mosaic {
  position: absolute;
  /* Extra vertical overshoot (-35%) + vertical centering so the rotated rows
     always cover the full banner height with no empty band at the bottom,
     regardless of how many rows or the viewport-scaled tile size. */
  inset: -45% -30%;
  transform: rotateZ(-10deg) scale(1.15);
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 10px;
  z-index: 0;
  opacity: 0.6;
}

.banner__mosaic-row {
  display: flex;
  width: max-content;
  animation: bannerMosaicDrift var(--row-duration, 90s) linear infinite;
  transform: translateX(var(--row-offset, 0px));
  /* Promote the moving row to its own GPU layer so tile edges render crisply at
     fractional scroll positions instead of shimmering / bleeding a hairline. */
  will-change: transform;
  backface-visibility: hidden;
}

@keyframes bannerMosaicDrift {
  0% {
    transform: translateX(var(--row-offset, 0px));
  }
  100% {
    /* -50% would be off by half a gap-width if spacing came from `gap`
       (n-1 gaps total, not n) — spacing lives on the tile's own margin
       instead so each tile "owns" its trailing space and duplicating the
       set exactly doubles the width, making -50% land pixel-exact on the
       duplicate's start with no seam. */
    transform: translateX(calc(var(--row-offset, 0px) - 50%));
  }
}

.banner__mosaic-tile {
  /* Tile width scales with the viewport like the reference hero (~14.2vw:
     ~190px on a laptop, ~227px at 1600), clamped so it never gets tiny or
     huge. Height derives from the 16:9 ratio. */
  flex: 0 0 clamp(160px, 14.2vw, 260px);
  margin-right: 10px;
  aspect-ratio: 16 / 9;
  border-radius: 8px;
  /* Dark base + padding-box clip: any 1px sub-pixel edge shows this dark tone
     (invisible on the dark hero) instead of a bright bleed, and the image never
     paints under the border/rounded corner. A thin dark outline cleanly frames
     every tile — light images included — killing the edge "biting". */
  background-color: #101015;
  border: 1px solid rgba(0, 0, 0, 0.35);
  background-clip: padding-box;
  background-size: cover;
  background-position: center;
}

.banner__shade {
  position: absolute;
  inset: 0;
  z-index: 1;
  background-image: linear-gradient(90deg, var(--color-bg) 0%, rgba(5, 5, 6, 0.96) 34%, rgba(5, 5, 6, 0.4) 68%, rgba(5, 5, 6, 0.4) 100%),
    linear-gradient(rgba(5, 5, 6, 0.05) 0%, rgba(5, 5, 6, 0.88) 100%);
}

/* Brand-colored bloom behind the text, echoing the logo's gradient
   (purple > magenta > orange). Screen blend makes it glow through the
   dark shade instead of just sitting flat on top of it. */
.banner__bloom {
  position: absolute;
  inset: 0;
  z-index: 1;
  mix-blend-mode: screen;
  opacity: 0.7;
  background-image: radial-gradient(circle at 12% 18%, rgba(91, 42, 134, 0.32), transparent 30%),
    radial-gradient(circle at 30% 26%, rgba(192, 41, 139, 0.14), transparent 28%),
    radial-gradient(circle at 16% 52%, rgba(255, 122, 26, 0.12), transparent 34%);
}

/* --- Layout --- */

.banner__inner {
  position: relative;
  z-index: 2;
  max-width: var(--container-max);
  margin-inline: auto;
  padding: 96px var(--container-pad) 88px;
  min-height: 820px;
  display: grid;
  grid-template-columns: minmax(500px, 0.92fr) minmax(440px, 1fr);
  align-items: center;
  gap: 48px;
}

/* --- Content column --- */

.banner__title {
  font-family: var(--font-heading);
  font-size: 52px;
  font-weight: 800;
  line-height: 1.08;
  letter-spacing: -0.01em;
  color: var(--color-text);
}

.banner__lede {
  margin-top: var(--space-6);
  font-size: 20px;
  line-height: 1.56;
  color: var(--color-text-muted);
  max-width: 560px;
}

.banner__cta-wrap {
  margin-top: var(--space-10);
}

.btn--lg {
  height: 56px;
  padding: 0 var(--space-6);
  font-size: 18px;
  border-radius: var(--radius-sm);
}

/* --- Media column: glowing video frame --- */

.banner__media {
  position: relative;
}

/* Soft ambient bloom behind the frame: a second copy of the same rotating
   conic-gradient, blurred and enlarged, sitting behind the crisp border
   ring (same technique, same animation — just blurred+dimmed). */
.banner__media-bloom {
  position: absolute;
  inset: -40px;
  z-index: 0;
  overflow: hidden;
  border-radius: var(--radius-md);
  filter: blur(42px);
  opacity: 0.4;
}

.banner__media-bloom::before {
  content: "";
  position: absolute;
  inset: -100%;
  background: conic-gradient(var(--color-purple), var(--color-magenta), var(--color-orange), var(--color-magenta), var(--color-purple));
  animation: bannerFrameSpin 16s linear infinite;
}

.banner__media-frame {
  position: relative;
  z-index: 1;
  overflow: hidden;
  padding: 2px;
  border-radius: var(--radius-md);
  background: #07070b;
  box-shadow: 0 30px 96px rgba(0, 0, 0, 0.64), 0 0 30px rgba(91, 42, 134, 0.34),
    0 0 34px rgba(192, 41, 139, 0.26), 0 0 58px rgba(255, 122, 26, 0.16);
}

/* Animated gradient border: an oversized conic-gradient square spins behind
   the video; overflow:hidden + the 2px padding clip it down to a thin
   rotating ring. (Uses the `rotate` property, not `transform`, so it can
   spin independently of anything else that might transform the frame.) */
.banner__media-frame::before {
  content: "";
  position: absolute;
  inset: -100%;
  z-index: 0;
  background: conic-gradient(var(--color-purple), var(--color-magenta), var(--color-orange), var(--color-magenta), var(--color-purple));
  animation: bannerFrameSpin 16s linear infinite;
}

@keyframes bannerFrameSpin {
  to {
    rotate: 360deg;
  }
}

.banner__media-inner {
  position: relative;
  z-index: 1;
  aspect-ratio: 16 / 9;
  border-radius: calc(var(--radius-md) - 2px);
  overflow: hidden;
  background: #07070b;
}

.banner__media-video {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  background: #07070b;
}

/* --- Responsive --- */
@media (max-width: 1024px) {
  .banner__inner {
    grid-template-columns: 1fr;
    gap: var(--space-10);
    min-height: 0;
    padding-block: var(--space-16);
    align-items: start;
  }
  .banner__title {
    font-size: 40px;
  }
  .banner__lede {
    font-size: 18px;
  }

  /* Full-width, centered CTA — the base .cta-liquid recipe (base.css) is
     inline-block/shrink-to-content everywhere else it's reused (roadmap,
     about), so this overrides just this one instance. .btn's own
     justify-content:center (header.css) centers the label once the box
     is stretched full-width, no extra text-align needed. */
  .banner__cta-wrap {
    display: block;
    width: 100%;
  }

  .banner__cta {
    width: 100%;
  }

  /* .is-stuck (js/banner-cta-sticky.js) locks the button just below the
     header once scrolling would otherwise carry it up underneath —
     plain CSS position:sticky can't do this here: its containing block
     is .banner__content, which ends right at the button itself (the
     last child), leaving sticky almost no room to travel before its own
     bound would release it again. Fixed positioning has no such limit,
     since nothing between here and the viewport uses transform/filter.
     left/right stay at container-pad — same size/shape as the in-flow
     button, just pinned instead of scrolling. margin:0 is required too —
     .banner__cta-wrap's own margin-top (this rule's non-.is-stuck
     sibling, and the 767px one below) still applies even under
     position:fixed (fixed doesn't cancel margin), so without this
     override the bar sat ~40px below the header instead of flush
     against it. .banner__cta-placeholder holds the button's vacated spot
     open (JS sets its height to match) so nothing above or below it
     jumps when the button leaves the flow.
     position can't itself transition (static -> fixed is an instant
     swap), so the "smooth" attach is a short pop-in animation on
     transform/opacity instead — plays once whenever .is-stuck is added,
     not on removal (unsticking stays instant, matching how it leaves on
     scroll-up). */
  .banner__cta-wrap.is-stuck {
    position: fixed;
    top: var(--header-height);
    left: var(--container-pad);
    right: var(--container-pad);
    /* Cancels the width:100% from the plain (non-.is-stuck) rule above.
       Fixed positioning with left AND right set already sizes the box to
       exactly fill that span on its own — leaving width:100% in place is
       an over-constrained combination (width + left + right all set at
       once), and the spec's tie-break for that case keeps width+left and
       drops right, so the box came out 100vw wide starting at `left`,
       overflowing past the right edge instead of stopping there. */
    width: auto;
    margin: 0;
    z-index: 40;
  }

  .banner__cta-placeholder {
    display: none;
  }
}

@media (max-width: 767px) {
  .banner__title {
    font-size: 32px;
  }
  .banner__lede {
    margin-top: var(--space-4);
    font-size: 16px;
  }
  .banner__cta-wrap {
    margin-top: var(--space-6);
  }
  .btn--lg {
    height: 52px;
    font-size: 16px;
  }
}

/* Multiple lede paragraphs (added via Enter in the banner block): tighter gap
   between paragraphs than the big gap under the title. */
.banner__lede + .banner__lede {
  margin-top: var(--space-3);
}
