/* =================================================================
   Defying Gravity — animated header

   Two independent systems:

   1. .sky   – background plate. Covers the hero at any shape, free
               to drift. Nothing depends on its position.
   2. .mark  – the logo. One registered 2000 x 1625 canvas; every
               layer is inset:0 / width:100% / height:100% with NO
               per-layer transform, so the layers can never drift
               out of alignment with each other. Motion is added as
               separate overlay elements, never by moving artwork.

   Tune the two knobs below; everything else follows.
   ================================================================= */

*, *::before, *::after { box-sizing: border-box; }

:root {
  --hero-h:    100vh;    /* fallback for browsers without dynamic viewport units */
  --mark-fill: 1.40;    /* mark height as a multiple of hero height  */
  --mark-cap:  1.34;    /* how far the mark may overflow the hero width */
  --sky-lift:  1;       /* 0 = flat, 1 = full nebula                 */
}

@supports (height: 100dvh) {
  :root { --hero-h: 100dvh; }
}

/* The old build animated @property <angle> custom properties inside
   conic-gradients to sweep the glow. That forces a full-surface repaint every
   frame on masked + blurred layers and overruns Chrome's compositor tile
   budget. The sweep is now a plain transform: rotate() on a pre-painted layer,
   so those @property declarations are no longer needed. */

@keyframes rotate-cw { to { transform: rotate(360deg); } }

html, body { margin: 0; }

body {
  background: #120820;
  color: #e9e2f5;
  font: 16px/1.6 system-ui, -apple-system, "Segoe UI", sans-serif;
}

/* ---------- hero shell ---------- */

.hero {
  position: relative;
  width: 100%;
  height: var(--hero-h);
  overflow: hidden;
  background: #120820;
  container-type: size;   /* gives .sky / .mark real cqw + cqh to work with */
  isolation: isolate;     /* keeps every blend mode inside the header */
}

/* ---------- 1. sky ---------- */

.sky {
  position: absolute;
  inset: -3%;                       /* headroom so the drift never shows an edge */
  background: url("sky-seamless.png") center / cover no-repeat;
  opacity: var(--sky-lift);
  /* Lift the nebula so its clouds stay visible when `cover` crops the plate to
     its darker centre band on wide / ultrawide screens (otherwise the middle
     reads as flat purple). */
  filter: saturate(1.22) brightness(1.28) contrast(1.05);
  /* sky-bloom fades the plate up from black on load (the opening); sky-drift
     runs forever. Different properties (opacity vs transform), so they coexist. */
  animation: sky-drift 58s ease-in-out infinite, sky-bloom 2.4s ease-out both;
  will-change: transform, opacity;
}

/* Centre glow — keeps the cloud lit around the logo and breathes gently, the
   way the video's mist stays illuminated near the mark. */
.sky-glow {
  position: absolute;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  background:
    radial-gradient(38% 42% at 50% 49%, rgba(120, 90, 200, .35) 0, rgba(80, 40, 150, .12) 45%, transparent 72%),
    radial-gradient(30% 30% at 61% 40%, rgba(224, 96, 220, .22) 0, transparent 70%),
    radial-gradient(30% 30% at 40% 60%, rgba(72, 214, 224, .18) 0, transparent 70%);
  opacity: 0;
  animation: sky-glow-in 3s ease-out 1s both, sky-glow-breathe 12s ease-in-out 4s infinite;
}

.sky-vignette {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(150% 108% at 50% 48%, transparent 55%, rgba(10, 4, 20, .40) 100%),
    linear-gradient(to bottom, rgba(10, 4, 20, .26), transparent 20% 80%, rgba(10, 4, 20, .32));
  pointer-events: none;
}

/* ---------- drifting star field ---------- */

.sizzle {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  mix-blend-mode: screen;
  z-index: 1;
  opacity: 0;                 /* blooms in with .particles-on */
}

.particles-on .sizzle { opacity: 1; transition: opacity 1.4s ease; }

.spark-field {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  mix-blend-mode: screen;
  z-index: 1;
  /* Particles hold off until the logo is in place; JS adds .particles-on. */
  opacity: 0;
}

.particles-on .spark-field { opacity: 1; transition: opacity .8s ease; }

.spark-field i {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: var(--s);
  height: var(--s);
  border-radius: 50%;
  background: rgb(var(--c));
  box-shadow:
    0 0 4px 1px rgba(var(--c), .95),
    0 0 12px 3px rgba(var(--c), .62),
    0 0 26px 7px rgba(var(--c), .22);
  opacity: 0;
  animation: spark-fly var(--d) ease-in-out infinite;
  animation-delay: var(--delay);
  will-change: transform, opacity;
}

/* A few brighter particles open into a tiny four-point flare. */
.spark-field i:nth-child(7n)::before,
.spark-field i:nth-child(7n)::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 700%;
  height: 1px;
  background: linear-gradient(90deg, transparent, rgba(var(--c), .85), transparent);
  transform: translate(-50%, -50%);
}

.spark-field i:nth-child(7n)::after { transform: translate(-50%, -50%) rotate(90deg); }

/* Reduce the static star-field population by one third without changing the
   timing, speed, size or trajectory of any remaining particle. */
.spark-field i:nth-child(3n) { display: none; }

/* ---------- 2. mark ---------- */

.mark {
  position: absolute;
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  aspect-ratio: 2000 / 1625;
  /* height-driven so the song ring is never cropped; width caps it on
     narrow screens. 1.230769 = 2000/1625. */
  width: min(calc(100cqh * var(--mark-fill) * 1.230769), calc(100cqw * var(--mark-cap)));
  /* The logo fades up in place as the two cloud banks clear, with just a
     whisper of push-in — a restrained, premium reveal rather than a big grow.
     `translate` handles centring, so `transform: scale()` is free. */
  transform: scale(.96);
  opacity: 0;
  animation: mark-grow 3.6s ease-out 1.4s both;
}

@keyframes mark-grow {
  0%   { opacity: 0; transform: scale(.96); }
  100% { opacity: 1; transform: scale(1); }
}

.layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
}

/* --- circle glow. Two separate registered plates: a FIXED colour bed and a
       slowly-rotating ring. Keeping colour and geometry apart means the palette
       stays anchored (cyan lower-left, magenta upper-right) while the ring and
       its smoke turn. Both screen-blended; rotation is transform-only. --- */

/* FIXED light gradient: bright at top-right, fading to shadow at bottom-left.
   Applied to the flare directly (it doesn't move) and to the ring via its
   stationary .ring-veil wrapper, so the lighting stays put while the ring
   turns through it. */
:root { --light-grad: radial-gradient(115% 115% at 70% 26%, #000 22%, rgba(0,0,0,.85) 46%, rgba(0,0,0,.06) 82%); }

/* The finished ring artwork. circle.webp is the neon spectral ring with its
   swirling light-trails and colour grading already rendered — the reference
   look. Screen-blend so its near-black field drops out over the sky and only
   the glow shows. The whole plate rotates slowly, so the colour and the swirl
   trails travel round together — exactly like the reference video (the palette
   moves; it is not fixed). Only this layer turns; the song titles and logo
   stay put. Rotation is a compositor-only transform, so no per-frame repaint. */
.circle {
  /* Transparent version of circle.webp: its dark field is converted to real
     alpha (luminance → opacity) so the ring composites correctly over the sky
     at ANY width. The opaque .webp painted a solid dark rectangle over the
     nebula because the mark forms an isolated stacking context (transform +
     will-change for the grow-in), which stops screen-blend from dropping the
     black against the sky — that flattened the centre on wide screens. */
  background: url("circle-glow.png") center / 100% 100% no-repeat;
  mix-blend-mode: screen;
  transform-origin: 50% 50%;
  /* Slow, majestic spin + a gentle opacity breath (separate property, so the
     two coexist without fighting over `transform`). */
  animation: circle-rotate 54s linear infinite,
             circle-pulse 9s ease-in-out infinite;
  will-change: transform, opacity;
}

@keyframes circle-rotate { to { transform: rotate(360deg); } }

@keyframes circle-pulse {
  0%, 100% { opacity: .9; }
  50%      { opacity: 1;  }
}

/* --- song ring. songs.png drives a MASK, so light always follows the arc
       of the lettering: no clip-path boxes, no words chopped mid-letter. --- */

/* Wrapper: one-shot spin into position as the clouds clear, then holds still.
   Screen blend is here so it composites once over the scene. */
.ring-base-spin {
  mix-blend-mode: screen;
  transform-origin: 50% 50%;
  animation: songs-spin-in 4.4s cubic-bezier(.16, .72, .2, 1) 1.4s both;
}

/* Inner image: the ongoing breathe (scale + soft opacity fade). */
.ring-base {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: .40;
  transform-origin: 50% 50%;
  animation: songs-breathe 9.5s ease-in-out infinite -3s;
  will-change: transform, opacity;
}

@keyframes songs-spin-in {
  from { transform: rotate(-42deg); }
  to   { transform: rotate(0deg); }
}

/* Gentle life: ~1% grow/shrink + a soft opacity breath. */
@keyframes songs-breathe {
  0%, 100% { opacity: .35; transform: scale(.99); }
  50%      { opacity: .45; transform: scale(1.01); }
}

.ring-tint,
.ring-sweep {
  -webkit-mask: url("songs.png") center / 100% 100% no-repeat;
          mask: url("songs.png") center / 100% 100% no-repeat;
  mix-blend-mode: screen;
}

/* tints each title with the ring colour behind it — teal at lower-left,
   magenta at upper-right — the way the printed artwork does */
.ring-tint {
  background: url("glow.png") center / 100% 100% no-repeat;
  filter: blur(1px) saturate(1.15) brightness(1.5);
  opacity: .12;
  animation: ring-pulse 14s ease-in-out infinite;
}

/* A highlight that travels round the ring, lighting each title in turn.
   The songs.png mask stays on this stationary parent; the child .rot spins a
   pre-painted conic gradient with a compositor-only transform. This single
   moving layer replaces the twelve masked song wedges the old build stacked
   here — those were the main surface-count/tile-budget offender. */
.ring-sweep {
  opacity: .9;
}

.ring-sweep .rot {
  position: absolute;
  inset: 0;
  background: conic-gradient(from 0deg,
    transparent          0deg 26deg,
    rgba(150, 240, 255, .30) 52deg,
    rgba(255, 255, 255, .92) 76deg,
    rgba(255, 175, 255, .34) 100deg,
    transparent          126deg 360deg);
  animation: rotate-cw 12s linear infinite;
  will-change: transform;
}

/* --- title --- */

.title-backdrop {
  background: radial-gradient(ellipse 25% 26% at 50% 49%,
    rgba(9, 3, 18, .34) 0 42%,
    rgba(12, 5, 24, .2) 64%,
    rgba(12, 5, 24, .07) 82%,
    transparent 100%);
  pointer-events: none;
}

.title-group {
  transform-origin: 50% 48.1%;
  scale: 1.035;   /* title reduced 10% (was 1.15) */
}

.title-group--defying {
  animation: defying-float 7.4s ease-in-out infinite -1.1s;
}

.title-group--gravity {
  animation: gravity-float 8.6s ease-in-out infinite -3.4s;
}

.title-letter-sparkles {
  mix-blend-mode: screen;
  opacity: 0;
}

/* The supplied flare plate is still on the exact registered 2000×1625
   canvas. The split falls cleanly between its upper and lower flare pairs. */
.title-letter-sparkles--defying {
  clip-path: inset(0 0 61.2% 0);
  transform-origin: 50% 35%;
  animation: letter-flare-a 5.4s ease-in-out infinite .1s;
}

.title-letter-sparkles--gravity {
  clip-path: inset(38.8% 0 0 0);
  transform-origin: 50% 44%;
  animation: letter-flare-b 5.4s ease-in-out infinite 1.55s;
}

/* The swoosh is built like the ring-sweep so it renders identically in every
   browser: the letter mask stays FIXED on this parent (via --combined) and the
   parent screen-blends against the glass behind it. The coloured band lives in
   an inner `.sweep-band` that moves by `transform` only — no background-position
   animation, no sub-pixel blur, nothing the compositor rasterizes per frame or
   rounds differently between Chrome and Safari. */
.title-diagonal-flare {
  mix-blend-mode: screen;
  opacity: 1;
  scale: 1.035;   /* match the reduced title so the swoosh mask stays aligned */
  transform-origin: 50% 48.1%;
}

.title-diagonal-flare .sweep-band {
  position: absolute;
  /* Keep the composited surface close to the title's real alpha bounds instead
     of translating another full 2000 x 1625 plate. The narrow element is
     tilted as part of the same transform that moves it. */
  left: 28%;
  top: 32%;
  width: 16%;
  height: 33%;
  background: linear-gradient(90deg,
    transparent 0%,
    rgba(255, 255, 255, .035) 28%,
    rgba(255, 255, 255, .14) 43%,
    rgba(255, 255, 255, .252) 50%,
    rgba(255, 250, 244, .105) 58%,
    rgba(255, 255, 255, .0252) 72%,
    transparent 100%);
  opacity: 0;
  transform: translate3d(-150%, 0, 0) skewX(-14deg);
  transform-origin: 50% 50%;
}

/* JS toggles `.is-sweeping`; the narrow band moves left-to-right at a constant
   rate while its own opacity feathers only the beginning and end of the pass. */
.title-diagonal-flare.is-sweeping .sweep-band {
  animation: title-band-travel 4.8s linear both,
             title-band-fade   4.8s linear both;
  will-change: transform, opacity;
}

.title-diagonal-flare--combined {
  -webkit-mask: url("title.png") center / 100% 100% no-repeat;
          mask: url("title.png") center / 100% 100% no-repeat;
}

.tag {
  transform-origin: 50% 57%;
  scale: 0.9775;   /* tagline reduced 15% (was 1.15) so it sits inside the circle */
  translate: 0 1.65%;
  animation: tag-float 10.8s ease-in-out infinite -2.6s;
}

/* Keep the mark's incidental layers off the permanent will-change promotion
   list so Chrome spends its compositor memory on the surfaces that actually
   move. (The twelve song wedges that used to need special handling here are
   gone — replaced by the single rotating ring-sweep highlight.) */
.is-chromium .title-group,
.is-chromium .title-letter-sparkles,
.is-chromium .title-diagonal-flare,
.is-chromium .tag {
  will-change: auto;
}

/* The Chrome recording shows compositor tiles failing exactly when the
   clipped sparkle plates animate filter + transform. Opacity alone preserves
   their registered positions without allocating another moving texture. */
.is-chromium .title-letter-sparkles {
  filter: none !important;
  transform: none !important;
}

/* --- sparkles: the real artwork glints, not CSS dots --- */

.sparkle {
  mix-blend-mode: screen;
  transform-origin: 49.33% 44.49%;   /* measured centroid of sparkle.png */
  opacity: 0;
}

.sparkle--a { animation: sparkle-bloom 10s ease-in-out infinite; animation-delay: .5s; }
.sparkle--b { animation: sparkle-bloom 10s ease-in-out infinite; animation-delay: 5s;
              filter: hue-rotate(-30deg); }

/* ---------- opening reveal ---------- */

/* Black veil over the whole hero that lifts as the clouds bloom, then is set
   to display:none so it never costs anything after the intro. */
.intro-veil {
  position: absolute;
  inset: 0;
  z-index: 3;                 /* below the cloud layer (z-index 4); it only
                                masks the very first frames before the cloud
                                video starts painting, so the logo never shows
                                un-clouded */
  background: #05010c;
  pointer-events: none;
  animation: veil-lift .5s ease-out both, veil-gone 0s linear .6s forwards;
}

/* Two cloud banks (see markup note). Each isolated wrapper duotones the smoke
   violet and screen-blends it. Together they fill the screen; both hold, then
   fade — the logo grows in behind them. */
.cloud {
  position: absolute;
  inset: 0;                   /* each bank is full-screen; they overlap to fill */
  z-index: 4;
  pointer-events: none;
  isolation: isolate;         /* keep the multiply tint contained to this bank */
  mix-blend-mode: screen;
  animation: cloud-fade 4.4s ease-in both;
  will-change: opacity;
}

/* Bottom bank: normal — dense smoke sits along the bottom edge.
   Top bank: flipped BOTH ways — vertical so its dense base hangs from the top
   edge, and horizontal so it doesn't read as a mirror image of the bottom.
   Screen-blended together they fill the whole frame. */
.cloud--top .cloud-video { transform: scale(-1, -1); }

.cloud-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* lift + moderate contrast so the two banks fill the whole frame solidly
     while keeping some billowing structure */
  filter: brightness(.9) contrast(1.35);
}

/* Deep violet-magenta gradient multiplied onto the smoke — sets the cloud
   colour precisely (unlike hue-rotate on grey). */
.cloud-tint {
  position: absolute;
  inset: 0;
  mix-blend-mode: multiply;
  background: linear-gradient(125deg,
    rgb(74, 46, 138) 0%,
    rgb(104, 54, 166) 44%,
    rgb(150, 64, 164) 76%,
    rgb(96, 58, 162) 100%);
}

/* Hold the full cloud, then fade — the reveal window (slowed). */
@keyframes cloud-fade {
  0%   { opacity: 1; }
  40%  { opacity: 1; }
  100% { opacity: 0; }
}

@keyframes veil-lift  { from { opacity: 1; } to { opacity: 0; } }
@keyframes veil-gone  { to { display: none; } }

@keyframes sky-bloom {
  from { opacity: 0; }
  to   { opacity: var(--sky-lift); }
}

@keyframes sky-glow-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

@keyframes sky-glow-breathe {
  0%, 100% { opacity: .85; }
  50%      { opacity: 1; }
}

/* ---------- keyframes ---------- */

@keyframes sky-drift {
  0%, 100% { transform: scale(1.02) translate3d(-.6%,  .4%, 0); }
  50%      { transform: scale(1.06) translate3d( .6%, -.5%, 0); }
}

/* Very wide viewports make the overscanned sky a particularly large GPU
   texture. WebKit and Chromium can expose compositor-tile boundaries while
   that texture is continuously transformed. Keep only the one-shot intro fade
   at ultrawide ratios; all foreground animation remains unchanged. */
@media (min-aspect-ratio: 2 / 1) {
  .sky {
    animation: sky-bloom 2.4s ease-out both;
    transform: none;
    will-change: opacity;
  }
}

@keyframes ring-pulse {
  0%, 100% { opacity: .08; }
  46%      { opacity: .18; }
}

@keyframes spark-fly {
  0%, 100% {
    opacity: 0;
    transform: translate3d(0, 4vh, 0) scale(.35);
  }
  16% { opacity: .2; }
  38% {
    opacity: .95;
    transform: translate3d(calc(var(--dx) * .48), calc(var(--dy) * .48), 0) scale(1);
  }
  55% { opacity: .45; }
  76% {
    opacity: 0;
    transform: translate3d(var(--dx), var(--dy), 0) scale(.55);
  }
}

@keyframes defying-float {
  0%, 100% { transform: translate3d(-.06%, -.17%, 0) rotate(-.12deg) scale(.999); }
  38%      { transform: translate3d( .09%,  .04%, 0) rotate( .08deg) scale(1.0015); }
  70%      { transform: translate3d(-.015%, .11%, 0) rotate(-.03deg) scale(1); }
}

@keyframes gravity-float {
  0%, 100% { transform: translate3d( .07%,  .14%, 0) rotate( .1deg) scale(1.001); }
  42%      { transform: translate3d(-.08%, -.05%, 0) rotate(-.09deg) scale(.999); }
  76%      { transform: translate3d( .01%, -.12%, 0) rotate( .025deg) scale(1.0005); }
}

@keyframes tag-float {
  0%, 100% { transform: translate3d(-.035%, .07%, 0) rotate(-.025deg) scale(.9995); }
  48%      { transform: translate3d( .045%,-.06%, 0) rotate( .025deg) scale(1.0005); }
}

@keyframes letter-flare-a {
  0%, 8%, 43%, 100% { opacity: 0; filter: brightness(.85) blur(.25px); transform: rotate(-.18deg) scale(.997); }
  15%                { opacity: .35; filter: brightness(1.15) blur(.08px); }
  22%                { opacity: 1; filter: brightness(1.9) blur(0); transform: rotate(.18deg) scale(1.004); }
  32%                { opacity: .42; filter: brightness(1.2) blur(.08px); }
}

@keyframes letter-flare-b {
  0%, 8%, 43%, 100% { opacity: 0; filter: brightness(.85) blur(.25px); transform: rotate(.16deg) scale(.997); }
  15%                { opacity: .3; filter: brightness(1.12) blur(.08px); }
  22%                { opacity: 1; filter: brightness(1.9) blur(0); transform: rotate(-.18deg) scale(1.004); }
  32%                { opacity: .38; filter: brightness(1.18) blur(.08px); }
}

@keyframes title-band-travel {
  from { transform: translate3d(-150%, 0, 0) skewX(-14deg); }
  to   { transform: translate3d(260%, 0, 0) skewX(-14deg); }
}

@keyframes title-band-fade {
  0%       { opacity: 0; }
  12%      { opacity: 1; }
  88%      { opacity: 1; }
  100%     { opacity: 0; }
}

@keyframes sparkle-bloom {
  0%, 4%    { opacity: 0;   scale: .84; }
  13%       { opacity: 1;   scale: 1.02; }
  24%       { opacity: .45; scale: 1.05; }
  36%, 100% { opacity: 0;   scale: 1.09; }
}

/* ---------- reduced motion: park every layer explicitly ----------
   (the old file left .title-swoosh frozen at opacity 1, which put a
   permanent white stripe across the title) */

@media (prefers-reduced-motion: reduce) {
  .sky, .spark-field i, .ring-spin,
  .ring-base, .ring-base-spin, .ring-tint, .ring-sweep, .ring-sweep .rot,
  .title-group, .title-letter-sparkles, .title-diagonal-flare, .sparkle, .tag,
  .cloud, .intro-veil, .sky-glow, .mark { animation: none; }
  .sky          { transform: none; }
  .cloud        { display: none; }
  .mark         { opacity: 1; transform: none; }
  .intro-veil   { display: none; }
  .sky-glow     { opacity: .9; }
  .spark-field   { opacity: 1; }   /* particles-on is never set here */
  .spark-field i { opacity: .28; transform: none; }
  .sizzle       { display: none; }
  .ring-sweep   { opacity: 0; }
  .title-group  { transform: none; }
  .title-letter-sparkles { opacity: 0; transform: none; }
  .title-diagonal-flare { opacity: 0; }
  .tag          { transform: none; }
  .flare        { opacity: .95; }
  .ring-spin    { transform: none; }
  .ring-tint    { opacity: .12; }
  .ring-sweep   { opacity: 0; }
  .sparkle--a   { opacity: .9; scale: 1; }
  .sparkle--b   { opacity: 0; }
}

/* ---------- narrow screens: let the mark fill more of the frame ---------- */

@media (max-width: 700px) {
  :root { --mark-cap: 1.62; --mark-fill: 1.30; }

  .sky {
    inset: -2.5rem;
    background-position: 47% 50%;
  }

  .spark-field i:nth-child(n + 17) { display: none; }
}

/* ---------- placeholder page ---------- */

.page { max-width: 62ch; margin: 0 auto; padding: 4rem 1.5rem; opacity: .6; }
