/* ============================================================
   CINEMATIC HERO — scroll-scrubbed laptop journey
   ============================================================ */

.journey {
  position: relative;
  background: var(--bg);
}

.journey-pin {
  position: relative;
  width: 100%;
  height: 100svh;
  height: 100dvh;
  overflow: hidden;
}

.journey-stage {
  position: absolute;
  inset: 0;
  overflow: hidden;
}

.journey-media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}

.journey-media video,
.journey-media img.poster {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* The artifact-suppression layers below darken the whole hero take as a
   side effect, which made the laptop itself harder to read — a small
   brightness lift on the source compensates for that specifically. */
#heroJourney .journey-media video {
  filter: brightness(1.2) contrast(0.85);
}

.journey-media img.poster {
  transition: opacity var(--dur-med) var(--ease-out);
  z-index: 2;
}
.journey-media img.poster.is-hidden { opacity: 0; pointer-events: none; }

.journey-vignette {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  background:
    radial-gradient(120% 90% at 50% 100%, rgba(0,0,0,0.55), transparent 60%),
    radial-gradient(120% 70% at 50% 0%, rgba(0,0,0,0.5), transparent 55%),
    linear-gradient(180deg, rgba(4,4,8,0.35) 0%, rgba(4,4,8,0) 18%, rgba(4,4,8,0) 78%, rgba(4,4,8,0.5) 100%);
}

/* The hero master take carries a bright out-of-focus window/blind in the
   background. During the static open/close beats it sits left of the
   laptop; during the orbit it swings across to the right and briefly fills
   most of that half of the frame. Masking it here is far cheaper than
   re-generating the whole continuous take, and the region is otherwise
   near-black already so the extra falloff is invisible. */
#heroJourney .journey-vignette {
  background:
    radial-gradient(52% 74% at 15% 35%, rgba(3,3,6,0.97) 0%, rgba(3,3,6,0.88) 50%, rgba(3,3,6,0.4) 80%, transparent 100%),
    radial-gradient(48% 42% at 88% 15%, rgba(3,3,6,0.96) 0%, rgba(3,3,6,0.85) 45%, rgba(3,3,6,0.3) 80%, transparent 100%),
    radial-gradient(120% 90% at 50% 100%, rgba(0,0,0,0.55), transparent 60%),
    radial-gradient(120% 70% at 50% 0%, rgba(0,0,0,0.5), transparent 55%),
    linear-gradient(180deg, rgba(4,4,8,0.35) 0%, rgba(4,4,8,0) 18%, rgba(4,4,8,0) 78%, rgba(4,4,8,0.5) 100%);
}

/* Blanket highlight suppressor: the window swings through positions the
   positional mask above can't chase (it briefly fills most of the right
   half during the orbit beat). mix-blend-mode: multiply darkens bright
   pixels hard while leaving the already near-black scene practically
   untouched, so it catches the artifact wherever it drifts without
   flattening the rest of the shot. */
#heroJourney .journey-stage::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  mix-blend-mode: multiply;
  background: rgba(15, 15, 19, 0.4);
}

/* The outro master carries faint smudge/fingerprint-like texture baked into
   the glossy screen surface during the reveal beat (confirmed present in the
   uncompressed raw master too, so it's from the generation itself, not
   compression). It's low-contrast, not a bright spot, so the multiply trick
   above won't touch it — multiply preserves the ratio between a dark smudge
   and its already-dark surroundings, and blur alone still let a faint trace
   through. So during the window it's actually visible (roughly progress
   0.03-0.58, the raking-angle reveal — lifted again before the crisp
   settled frontal shot at the end) two things stack: a heavier blur, plus a
   soft near-opaque wash on top that overwrites the texture outright rather
   than just softening it. */
#outroJourney .journey-media video {
  filter: contrast(0.93) brightness(0.94);
  transition: filter 0.4s ease-out;
}
#outroJourney.is-texture-masked .journey-media video {
  filter: blur(14px) contrast(0.85) brightness(0.95);
}
#outroJourney .journey-stage::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.4s ease-out;
  background: radial-gradient(70% 65% at 50% 35%, rgba(8,8,11,0.9) 0%, rgba(8,8,11,0.75) 60%, rgba(8,8,11,0.4) 100%);
}
#outroJourney.is-texture-masked .journey-stage::before {
  opacity: 1;
}

/* Unlike the hero (which hands off into a normal, non-pinned section with
   its own top fade), the outro is the LAST pinned take before the page
   ends the journey entirely — its final frame (bright reflective table +
   glowing laptop edge) was still visibly lit right up to the exact pixel
   where .journey-pin's overflow:hidden clips it, because the generic
   vignette's bottom radial only ever reaches 0.55 alpha, never solid
   black. That partial fade let real video brightness sit directly against
   the much darker Contact section below with no transition, reading as a
   hard line. This override pushes the same bottom radial all the way to
   opaque black at the edge, so the frame is fully faded out before the
   clip instead of being cut off mid-brightness. */
#outroJourney .journey-vignette {
  background:
    radial-gradient(130% 22% at 50% 100%, rgba(0,0,0,1) 0%, rgba(0,0,0,0.75) 45%, transparent 100%),
    radial-gradient(120% 70% at 50% 0%, rgba(0,0,0,0.5), transparent 55%),
    linear-gradient(180deg, rgba(4,4,8,0.35) 0%, rgba(4,4,8,0) 18%, rgba(4,4,8,0) 78%, rgba(4,4,8,0.5) 100%);
}

.journey-grain {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  opacity: 0.05;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='90' height='90'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
}

/* Copy layer, pinned centered, cross-faded per beat */
.journey-copy {
  position: absolute;
  inset: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  padding: 0 var(--container-pad) calc(var(--sp-24) + var(--safe-bottom));
  pointer-events: none;
}

.journey-beat {
  position: absolute;
  left: var(--container-pad);
  right: var(--container-pad);
  bottom: calc(var(--sp-24) + var(--safe-bottom));
  max-width: 720px;
  opacity: 0;
  transform: translateX(-36px);
  /* This transition only ever plays for the centered title/CTA beats now
     (see below) — it's also the pre-JS resting look for the four numbered
     "01"–"04" beats (prevents a flash before the page's JS runs), but
     journey.js overrides it with transition:none the moment it takes over,
     since their per-frame position comes from gsap.set() driven directly
     by scroll progress; a CSS transition running at the same time would
     fight it frame by frame. Fade-OUT for the centered beats is forced
     instant in JS (journey.js setProgress) rather than via a CSS timing
     trick, since which side's transition-duration "wins" when the rule
     itself changes isn't reliably consistent across browsers. */
  transition: opacity 0.75s var(--ease-out), transform 0.75s var(--ease-out);
}
.journey-beat.is-active { opacity: 1; transform: translateX(0); pointer-events: auto; }

/* The four numbered beats ("01"–"04") are animated continuously by
   journey.js (gsap.set per scroll frame), not by this class — but they
   still benefit from a compositing hint since they update every frame. */
.journey-beat:not(.journey-beat--center) {
  will-change: transform, opacity;
}

.journey-beat--center {
  left: 50%;
  right: auto;
  bottom: auto;
  top: 52%;
  transform: translate(-50%, -40%);
  max-width: 900px;
  text-align: center;
}
.journey-beat--center.is-active { transform: translate(-50%, -50%); }

.journey-beat h1,
.journey-beat h2 {
  font-size: var(--fs-hero);
  line-height: var(--lh-tight);
  max-width: 14ch;
}
.journey-beat--center h1 { max-width: none; margin-inline: auto; }

/* The 4 scrolling side beats ("Built to turn visitors into customers." etc.)
   sit smaller than the centered hero/CTA titles above. */
.journey-beat:not(.journey-beat--center) h2 {
  font-size: clamp(1.8rem, 3vw + 0.6rem, 3rem);
}

.journey-beat p {
  margin-top: var(--sp-4);
  color: var(--ink-dim);
  max-width: 46ch;
  font-size: 1.05rem;
}
.journey-beat--center p { margin-inline: auto; }

.journey-beat .journey-actions {
  margin-top: var(--sp-8);
  display: flex;
  gap: var(--sp-4);
  flex-wrap: wrap;
}
.journey-beat--center .journey-actions { justify-content: center; }

/* "Start your project" — scoped back to the original accent blue, same
   as the nav dot and the contact form's submit button (see the matching
   comment in contact.css/layout.css): --accent itself was changed
   site-wide, but this button specifically should stay as it was. */
.journey-actions .btn-primary {
  background: #5b6cff;
}
.journey-actions .btn-primary:hover {
  background: #6d7cff;
}

/* Floating service labels beside the laptop (spatial typography) */
.service-float {
  position: absolute;
  z-index: 5;
  opacity: 0;
  transform: translateX(28px);
  transition: opacity 0.65s var(--ease-out), transform 0.65s var(--ease-out);
  pointer-events: none;
}
.service-float.is-active { opacity: 1; transform: translateX(0); }
.service-float .tag-num {
  display: block;
  font-family: var(--font-body);
  font-size: var(--fs-small);
  color: var(--accent-2);
  margin-bottom: var(--sp-1);
  letter-spacing: 0.08em;
}
.service-float .tag-label {
  font-family: var(--font-display);
  font-size: clamp(1.1rem, 1.6vw, 1.6rem);
  white-space: nowrap;
}

.scroll-cue {
  position: absolute;
  left: 50%;
  bottom: calc(var(--sp-6) + var(--safe-bottom));
  transform: translateX(-50%);
  z-index: 6;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--sp-2);
  color: var(--ink-faint);
  font-size: var(--fs-small);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  opacity: 1;
  transition: opacity var(--dur-med) var(--ease-out);
}
.scroll-cue.is-hidden { opacity: 0; }
.scroll-cue .line {
  width: 1px; height: 28px;
  background: linear-gradient(var(--ink-faint), transparent);
  animation: scrollcue 1.8s var(--ease-in-out) infinite;
}
@keyframes scrollcue {
  0% { transform: scaleY(0); transform-origin: top; opacity: 0; }
  40% { transform: scaleY(1); transform-origin: top; opacity: 1; }
  60% { transform: scaleY(1); transform-origin: bottom; opacity: 1; }
  100% { transform: scaleY(0); transform-origin: bottom; opacity: 0; }
}

@media (max-width: 860px) {
  .journey-beat { max-width: 92%; bottom: calc(var(--sp-16) + var(--safe-bottom)); }
  .journey-beat h1, .journey-beat h2 { font-size: clamp(2rem, 8vw, 3rem); max-width: none; }
  .service-float { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  .journey-pin { height: auto; }
  .journey-beat, .service-float { opacity: 1 !important; transform: none !important; }
}
