/*
  LUXVISION — Storm splash + persistent brand mark.

  Two separable pieces that happen to share a file because the splash is
  really just a staged entrance for the second one:

    .lv-brand  - the permanent LUXVISION mark, fixed in the top-left of the
                 nav band. This is NOT a splash element. It exists on every
                 page, holds position through the whole scroll, and sits at
                 the same height as the pill bar because both read the same
                 --lv-nav-band-* tokens.

    .lv-splash / .lv-splash-veil - the arrival sequence. Storm cloud builds,
                 the logo lights up from behind it and burns through, then
                 sweeps to its resting place and the storm clears to reveal
                 the carousel. Removed from the DOM when it finishes; nothing
                 about the resting layout depends on it.

  The logo does not move between two different elements during the swipe.
  There is exactly one .lv-brand, always laid out in its final position, and
  the splash simply transforms it out to centre-screen and then releases it
  back. Animating a transform to zero is the only version of this that
  cannot land a pixel off, because the destination is the layout itself
  rather than a second set of coordinates that has to be kept in sync.
*/

/* ---------- Persistent brand mark ---------- */
.lv-brand {
  position: fixed;
  top: var(--lv-nav-band-top);
  left: var(--lv-space-gutter);
  height: var(--lv-nav-band-h);
  display: flex;
  align-items: center;
  /* Below the mobile nav sheet (49) so a full-screen menu covers it, and
     below the pill nav (50). Raised above the storm only while splashing. */
  z-index: 48;
  transform-origin: center center;
  will-change: transform, filter, opacity;
}

.lv-brand img {
  display: block;
  width: clamp(116px, 11vw, 152px);
  height: auto;
}

/* 🔴 THE MARK NOW SITS ON TOP OF THE SMOKE, NOT INSIDE IT.

   Joel, 14 Sept 2026: "can we have the logo just fade in over the smoke layer?"

   It used to sit at 210, BETWEEN the back plate (1) and the front veil (220),
   so it genuinely surfaced out of the cloud rather than being pasted onto it.
   That was the right idea and it was the source of the hang he spotted an hour
   earlier: anything the mark did while the veil was still thick, nobody saw.
   Clearing the veil in time fixed the symptom. Putting the mark above it
   removes the mechanism.

   ⚠️ 230 IS ABOVE THE VEIL'S 220, AND THAT CREATES A REAL HAZARD. .lv-brand is
   an <a href="index.html">. The veil was also what swallowed clicks during the
   intro — click anywhere to skip — so a mark raised above it becomes a live
   link sitting in the middle of the screen, and a skip-click that happens to
   land on the logo reloads the page instead. Hence the second rule: it is not
   tidiness, it is the bug that change would otherwise introduce. */
body.is-splashing .lv-brand {
  z-index: 230;
  pointer-events: none;
}

/* ---------- Splash stage ---------- */
.lv-splash,
.lv-splash-veil {
  position: fixed;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
}

/* Deliberately NOT opaque any more. The smoke plate underneath has to read
   through it from the very first frame, so this is only a vignette that
   sinks the corners and keeps the centre of the frame reserved for the
   backlight and the mark. */
.lv-splash {
  z-index: 200;
  background: radial-gradient(ellipse 85% 70% at 50% 46%, rgba(11, 11, 16, 0) 0%, rgba(11, 11, 16, 0.72) 72%, rgba(11, 11, 16, 0.92) 100%);
}

/* The half of the storm that passes IN FRONT of the logo.

   It also has to SWALLOW pointer events, not pass them through. Clicking
   anywhere is how you skip the intro, and the hero carousel underneath
   treats a click as "select the centred product" and navigates. Without
   this, skipping the splash would silently fire off a page load. */
.lv-splash-veil {
  z-index: 220;
  pointer-events: auto;
}

/*
  The smoke plate.

  This replaces the procedural SVG turbulence that used to stand in for
  weather here. The turbulence was a reasonable no-asset answer, but it
  could only ever read as abstract churn - it has no volume, no depth
  falloff, and no light behaviour, because there is no light in it. Real
  footage of smoke has all three, which is what makes the logo look like
  it is genuinely BEHIND something rather than fading up over a texture.

  ONE plate serves the whole hero, in two roles:

    .lv-atmos       - persistent, sits under everything. Present through the
                      splash AND behind the running carousel, so the product
                      ring turns inside the same environment the logo arrived
                      through instead of cutting to an unrelated background.
    .lv-splash-veil - a second, lighter copy IN FRONT of the logo during the
                      reveal only, then cleared. This is the layer that does
                      the actual "through the smoke" work.

  Both point at the same file, so the browser fetches and caches it once.
  The front copy uses the smaller mobile encode on purpose: it is blurred
  and heavily faded, so the extra resolution would be decoded and then
  thrown away.
*/
/* The orb. Not a background and not a pool - a ball of storm sitting on the
   turning ring's centre axis, with the product panels orbiting it. The
   panels are the only opaque thing in the hero, so the orb reads through the
   gaps between them and bleeds past their feathered edges (see the mask on
   .lv-slide img in carousel.css). That is what ties the ring together into
   one object instead of nine pictures on a black field.

   Sized to sit behind the ring and reach past it. It has to be wider than
   the centre panel or it never shows in the gaps, which is the only place
   it is visible once the panels are in front of it - but not so wide that it
   becomes a backdrop wash instead of a body with a centre. Pulled back in
   when the panel dropped from 74vh to 40vh: an orb sized to clear a
   full-screen panel is far too big to read as a ball behind a smaller ring.

   All four values are tunable and do what they say. The mask's own stops are
   the softness: dense to 26%, gone by 72%. */
.lv-atmos,
.lv-splash-veil {
  --lv-smoke-x: 50%;
  --lv-smoke-y: 49%;
  --lv-smoke-w: 38%;
  --lv-smoke-h: 52%;
}

/* Slow counter-drift, so the orb is never quite the same shape twice and the
   20 second video loop is harder to catch. Transform only - it rides the
   compositor and never re-rasterises the plate. */
.lv-atmos video {
  animation: lv-orb-churn 38s ease-in-out infinite;
}

@keyframes lv-orb-churn {
  0%   { transform: scale(1.06) translate3d(0, 0, 0); }
  50%  { transform: scale(1.14) translate3d(1.5%, -1.5%, 0); }
  100% { transform: scale(1.06) translate3d(0, 0, 0); }
}

.lv-atmos {
  /* Single source of truth for the still frame, so the reduced-motion rule
     below and the <video poster> in the markup can never drift apart. */
  --lv-atmos-poster: url("https://d2ol7oe51mr4n9.cloudfront.net/user_3HBdJpvbW25zg5bHsblkwhPmcZ8/70fe7493-c938-4680-909a-91113319b4d0.jpg");
  position: fixed;
  inset: 0;
  z-index: 1;
  overflow: hidden;
  pointer-events: none;
  /* No background fill here any more. A flat fill would be masked along with
     the smoke, and the mask has to cut all the way to nothing so the page's
     own black shows through cleanly at the edges - a second dark layer with
     a soft edge on top of it just reads as a smudge. */
  transition: opacity 0.8s var(--lv-ease);
  -webkit-mask-image: radial-gradient(
    ellipse var(--lv-smoke-w) var(--lv-smoke-h) at var(--lv-smoke-x) var(--lv-smoke-y),
    #000 0%, #000 30%, transparent 76%
  );
  mask-image: radial-gradient(
    ellipse var(--lv-smoke-w) var(--lv-smoke-h) at var(--lv-smoke-x) var(--lv-smoke-y),
    #000 0%, #000 30%, transparent 76%
  );
}

/* The front copy is masked WIDER and softer than the plate behind it. If both
   used the same ellipse their edges would land on top of each other and read
   as one hard rim; offsetting them keeps the boundary ambiguous, which is the
   only way a soft-edged pool of smoke looks like depth rather than a shape. */
.lv-splash-veil {
  -webkit-mask-image: radial-gradient(
    ellipse calc(var(--lv-smoke-w) + 16%) calc(var(--lv-smoke-h) + 14%) at var(--lv-smoke-x) var(--lv-smoke-y),
    #000 0%, #000 24%, transparent 82%
  );
  mask-image: radial-gradient(
    ellipse calc(var(--lv-smoke-w) + 16%) calc(var(--lv-smoke-h) + 14%) at var(--lv-smoke-x) var(--lv-smoke-y),
    #000 0%, #000 24%, transparent 82%
  );
}

.lv-atmos video,
.lv-splash-veil video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Scaled slightly past the viewport so the plate's own frame edge can
     never appear, and so the two copies do not sit in perfect register
     with each other - the slight scale difference reads as depth. */
  transform: scale(1.06);
}

/* Once the hero is behind you the atmosphere goes with it. Driven off a
   body class rather than a sibling selector so it does not depend on the
   DOM order of the hero and this layer. */
body.lv-past-hero .lv-atmos {
  opacity: 0;
}

/* The front copy. Larger and softer than the plate behind, so the two never
   sit in register - that offset is what gives the logo somewhere to be
   between them. Starts hidden in CSS rather than waiting for JS to hide it,
   so it cannot flash at full strength over the mark in the frames before
   the timeline starts. */
.lv-splash-veil video {
  transform: scale(1.22);
  filter: blur(2px);
  opacity: 0;
}


/* Held back until the sequence finishes so the hero cannot flash into
   view behind a clearing cloud before the logo has landed. */
body.is-splashing {
  overflow: hidden;
}

@media (max-width: 860px) {
  .lv-brand img { width: clamp(104px, 26vw, 132px); }
}

/*
  Reduced motion: no sweep, no reveal, and no moving smoke. The logo is
  simply present in its resting position from the first frame and the
  carousel starts immediately.

  The plate becomes its own poster frame - a still photograph of smoke,
  which keeps the whole visual identity intact while removing the one thing
  the preference is actually about. A continuously looping full-viewport
  background is exactly the sort of ambient motion this query exists to
  suppress, so hiding the video element and letting the poster show through
  is the honest reading of it, not a degraded fallback. splash.js checks the
  same query and never calls play(), so the file is not even decoded.
*/
@media (prefers-reduced-motion: reduce) {
  .lv-atmos video { display: none; }
  .lv-atmos {
    background-image: var(--lv-atmos-poster);
    background-size: cover;
    background-position: center;
  }
}

/* ── THE MARK RESOLVING: REGISTRATION ─────────────────────────────────────
   14 September 2026. Joel: "I want a more polished load in, I want to keep
   the smoke idea."

   The smoke is untouched. What changed is HOW the mark arrives.

   It used to be a blur-and-brightness fade: light through fog finding its
   edges. True to the idea, but it is the same reveal any brand could use.

   Now the mark converges into REGISTRATION. It arrives as separated red,
   green and blue and slides into alignment, the way a display resolves when
   its channels line up. **That is literally what LUXVISION sells**, and it is
   the same three emitter colours sampled off our own COB illustration for the
   bento dust — #e25c4e, #69b15c, #95abe9 — rather than pure RGB, which reads
   as a broken television instead of a display coming up.

   ⚠️ ONE element, not three tinted copies. `drop-shadow` filters composite on
   the GPU; three stacked <img> clones would be three decodes, three layers and
   three things to keep in sync.

   ⚠️ `@property` is REQUIRED. A custom property is not animatable until it is
   registered, and an unregistered one snaps from start to end with no warning
   at all. That bug already cost a round on the social badges. */
@property --lv-sep  { syntax: "<length>";  inherits: false; initial-value: 0px; }
@property --lv-blur { syntax: "<length>";  inherits: false; initial-value: 0px; }
@property --lv-brt  { syntax: "<number>";  inherits: false; initial-value: 1; }
@property --lv-spec { syntax: "<number>";  inherits: false; initial-value: 0; }
/* ⚠️ The highlight's OPACITY is its own property rather than a calc() off the
   position. The first version used `calc(1 - abs(var(--lv-spec) * 2 - 1))`, and
   CSS `abs()` is recent enough that Safari 15 does not have it — the rule would
   have been dropped silently and the band would have sat at full strength the
   whole way across. Two properties, no arithmetic that can fail to parse. */
@property --lv-specA { syntax: "<number>"; inherits: false; initial-value: 0; }
/* The separation's own alpha. Driven to zero as the channels converge so the
   three drop-shadows leave the filter chain entirely rather than sitting in it
   at zero offset, still being evaluated every frame for no visible result. */
@property --lv-regA  { syntax: "<number>"; inherits: false; initial-value: 0; }

body.is-splashing .lv-brand img {
  /* ⚠️ Promote the layer. Four stacked filters re-rasterise the element every
     frame; without this the compositor repaints it on the main thread and the
     resolve judders — which is what Joel saw as "jumpy". */
  will-change: filter, opacity;
  filter:
    blur(var(--lv-blur))
    brightness(var(--lv-brt))
    drop-shadow(calc(var(--lv-sep) * -1) 0 0 rgba(226, 92, 78, var(--lv-regA)))
    drop-shadow(var(--lv-sep) 0 0 rgba(149, 171, 233, var(--lv-regA)))
    drop-shadow(0 calc(var(--lv-sep) * -0.42) 0 rgba(105, 177, 92, calc(var(--lv-regA) * 0.6)));
}

/* The specular pass: one highlight travelling across the mark as it settles,
   the way light crosses a screen you have just switched on. `--lv-spec` runs
   0 to 1 and carries the band from one side to the other. */
body.is-splashing .lv-brand { position: fixed; overflow: visible; }
body.is-splashing .lv-brand::after {
  content: "";
  position: absolute;
  inset: -12% -6%;
  pointer-events: none;
  /* ⚠️ Plain compositing, NOT mix-blend-mode. Blending an element with its
     backdrop takes it off the fast path and forces a readback every frame —
     expensive, and over a near-black stage a white gradient at low alpha looks
     the same either way. */
  opacity: var(--lv-specA);
  will-change: opacity, background;
  background: linear-gradient(104deg,
    transparent calc(var(--lv-spec) * 150% - 34%),
    rgba(255,255,255,0.55) calc(var(--lv-spec) * 150% - 16%),
    rgba(255,255,255,0.10) calc(var(--lv-spec) * 150% - 6%),
    transparent calc(var(--lv-spec) * 150% + 8%));
}

/* ⛔ Reduced motion gets none of this. The mark is simply there, in
   registration, at full brightness — which is what the skip paths land on
   too, so every route ends on the same frame. */
@media (prefers-reduced-motion: reduce) {
  body.is-splashing .lv-brand img { filter: none; }
  body.is-splashing .lv-brand::after { display: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   THE FIRST FRAME.

   .lv-atmos defaults to fully visible, on purpose: a page with scripting off
   still needs its smoke. The cost is that it paints at FULL opacity on frame
   one and splash.js then sets it to zero to roll it back in — the ending,
   flashed before the beginning.

   ⚠️ A loading cover used to hide that. Joel, 14 Sept: "Since the loading bar
   didn't help lets remove that" — and he was right, it bought 1.35s of black
   to solve a 1-frame problem. The class is set by one inline script in the
   head, so the rule applies only when the sequence that needs it is going to
   run, and a browser with no JavaScript is untouched.
   ═══════════════════════════════════════════════════════════════════════════ */
html.lv-js .lv-atmos { opacity: 0; }
