/* =================================================================
   INTRO — everything specific to the entry sequence:
   the dropping flare, the circle-draw, the framed video, the
   calibration panel, the cinematic ambient projection, and the
   persistent logo overlay that survives the intro fade.

   Shared tokens (palette, frame-w, circle dims, easings, reset)
   live in tokens.css and are assumed loaded BEFORE this file.

   DO NOT EDIT during homepage development — the intro is frozen.
   ================================================================= */

/* Intro-state scroll lock. site.css releases this via
   html.scroll-ready when the homepage takes over. */
html, body {
  overflow: hidden;
  height: 100vh;
  width: 100vw;
}

body::before {
  content: '';
  position: fixed; inset: 0;
  background:
    radial-gradient(ellipse 70vw 50vh at 50% 50%,
                    rgba(212, 163, 115, 0.08) 0%,
                    transparent 65%);
  pointer-events: none;
  z-index: 0;
}

/* INTRO LAYER */
.intro {
  position: fixed;
  inset: 0;
  z-index: 999;
  transition: opacity 0.9s cubic-bezier(0.4, 0, 0.2, 1);
}
.intro.gone { opacity: 0; pointer-events: none; }

/* PARTICLES */
.particles {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  z-index: 1;
}
.particles span {
  position: absolute;
  width: 2px;
  height: 2px;
  background: rgba(212, 163, 115, 1);
  border-radius: 50%;
  box-shadow: 0 0 4px rgba(212, 163, 115, 0.5);
  opacity: 0;
  animation: drift var(--dur, 24s) infinite ease-in-out;
}
@keyframes drift {
  0%   { transform: translate(0, 0);                                opacity: 0; }
  15%  { opacity: var(--maxOp, 0.35); }
  85%  { opacity: var(--maxOp, 0.35); }
  100% { transform: translate(var(--dx, 30px), -120vh);             opacity: 0; }
}

/* PRE-STATE — fills the intro layer; button + hint are absolutely
   positioned inside so the button lands at the exact viewport
   centre (where the entry flare drops to). */
.pre-state {
  position: absolute;
  inset: 0;
  z-index: 5;
  transform-origin: 50% 50%;
  transition: opacity 0.5s cubic-bezier(0.4, 0, 1, 1),
              transform 0.6s cubic-bezier(0.4, 0, 1, 1);
}
.pre-state.gone {
  opacity: 0;
  transform: scale(0.94);
  pointer-events: none;
}

.start-btn {
  position: absolute;
  left: 50%; top: 50%;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  opacity: 0;
  transform: translate(-50%, -50%) scale(0.7);
  /* Unclickable until the circle-draw finishes (intro.js adds .ready
     on the circle path's drawCircle animationend, NOT btnReveal — so
     the button responds to taps the moment the circle is fully spun,
     while its own reveal animation continues for a final 0.6s of
     polish on top). */
  pointer-events: none;
  animation: btnReveal 0.6s cubic-bezier(0.2, 0.85, 0.2, 1) 2.8s forwards;
}
.start-btn.ready { pointer-events: auto; }
@keyframes btnReveal {
  from { opacity: 0; transform: translate(-50%, -50%) scale(0.7); }
  to   { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
@keyframes btnFadeIn { to { opacity: 1; } }

.start-btn__circle {
  width: 104px;
  height: 104px;
  border-radius: 50%;
  background: rgba(7, 6, 10, 0.55);
  border: 1px solid rgba(212, 163, 115, 0.55);
  -webkit-backdrop-filter: blur(20px) saturate(160%);
          backdrop-filter: blur(20px) saturate(160%);
  display: flex;
  align-items: center;
  justify-content: center;
  color: rgba(212, 163, 115, 0.95);
  box-shadow:
    0 4px 30px rgba(0, 0, 0, 0.5),
    0 0 70px rgba(212, 163, 115, 0.18);
  transition: all 0.45s cubic-bezier(0.16, 1, 0.3, 1);
  position: relative;
}
.start-btn__circle::after {
  content: '';
  position: absolute;
  inset: -12px;
  border-radius: 50%;
  border: 1px solid rgba(212, 163, 115, 0.4);
  animation: btnRing 2.6s ease-out infinite;
  pointer-events: none;
}
@keyframes btnRing {
  0%   { transform: scale(1);    opacity: 0.75; }
  100% { transform: scale(1.75); opacity: 0;    }
}
.start-btn__icon {
  width: 32px;
  height: 32px;
  filter: drop-shadow(0 0 6px rgba(212, 163, 115, 0.55));
  animation: sparkleSpin 18s linear infinite;
}
@keyframes sparkleSpin { to { transform: rotate(360deg); } }
.start-btn:hover .start-btn__circle {
  background: rgba(212, 163, 115, 0.96);
  color: #07060a;
  border-color: rgba(212, 163, 115, 1);
  transform: scale(1.06);
  box-shadow:
    0 6px 40px rgba(0, 0, 0, 0.6),
    0 0 110px rgba(212, 163, 115, 0.45);
}
.start-btn:active .start-btn__circle { transform: scale(0.97); }

.hint {
  position: absolute;
  left: 50%;
  top: calc(50% + 88px);     /* button radius (52) + 32px gap + 4px baseline */
  transform: translateX(-50%);
  font-family: 'Geist Mono', monospace;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: rgba(212, 163, 115, 0.7);
  text-align: center;
  opacity: 0;
  animation: btnFadeIn 0.6s cubic-bezier(0.2, 0.85, 0.2, 1) 3.3s forwards;
  white-space: nowrap;
}

/* POST-STATE: framed video display.
   Sized to EXACTLY match the video frame (same width formula, same
   aspect ratio) so its centre lands on viewport-centre identically
   to the .logo-overlay. The progress rail is positioned absolutely
   *below* the frame so it does not push the frame upward in flow. */
.post-state {
  position: absolute;
  left: 50%; top: 50%;
  width: var(--frame-w);
  aspect-ratio: var(--video-ar);
  transform: translate(-50%, -50%) scale(0.96);
  z-index: 4;
  opacity: 0;
  pointer-events: none;
  transition:
    opacity   0.75s cubic-bezier(0.2, 0.85, 0.2, 1),
    transform 0.95s cubic-bezier(0.2, 0.85, 0.2, 1);
}
.post-state.visible {
  opacity: 1;
  transform: translate(-50%, -50%) scale(1);
}

.video-frame {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 10px;
  overflow: hidden;
  background: #000;
  box-shadow:
    inset 0  1px 0 rgba(255, 255, 255, 0.07),
    inset 0 -1px 0 rgba(0, 0, 0, 0.55),
    0 30px 80px  rgba(0, 0, 0, 0.75),
    0 60px 140px rgba(0, 0, 0, 0.5);
  transition: opacity 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.video-frame.fading-out { opacity: 0; }
.video-frame video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.video-frame::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  border: 1px solid rgba(255, 255, 255, 0.04);
  pointer-events: none;
  z-index: 2;
}

/* BUFFERING LOADER (v=84) — a soft amber spinner shown over the black
   video frame from the tap until the first frame actually plays. iOS
   won't fetch video data until the user gesture, so this fills the
   5-7s gap with feedback instead of a dead black box. Toggled by
   .post-state.is-buffering (set in intro.js handleStart, cleared on the
   first real playback). Amber ring matches the intro palette. */
.video-loader {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 3;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.3s cubic-bezier(0.2, 0.85, 0.2, 1);
}
.post-state.is-buffering .video-loader { opacity: 1; }
.video-loader__spinner {
  width: 46px;
  height: 46px;
  border-radius: 50%;
  border: 2px solid rgba(212, 163, 115, 0.22);
  border-top-color: rgba(212, 163, 115, 0.92);
  box-shadow: 0 0 24px rgba(212, 163, 115, 0.18);
  animation: video-loader-spin 0.85s linear infinite;
}
@keyframes video-loader-spin { to { transform: rotate(360deg); } }

.progress-rail {
  position: absolute;
  top: calc(100% + 22px);   /* sits below .post-state without altering its height */
  left: 0;
  width: 100%;
  height: 1px;
  background: rgba(212, 163, 115, 0.08);
  overflow: hidden;
  border-radius: 999px;
}
.progress-fill {
  height: 100%;
  width: 0%;
  background: linear-gradient(90deg,
                              transparent 0%,
                              rgba(212, 163, 115, 0.55) 60%,
                              rgba(212, 163, 115, 0.95) 100%);
}

.brand-mark {
  position: fixed;
  bottom: 28px;
  right: 36px;
  z-index: 1000;
  font-family: 'Geist Mono', 'Geist', ui-monospace, monospace;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: rgba(212, 163, 115, 0.55);
  pointer-events: none;
  opacity: 0;
  animation: btnFadeIn 1.0s cubic-bezier(0.2, 0.85, 0.2, 1) 3.6s forwards;
}

/* =================================================================
   PERSISTENT LOGO OVERLAY
   Lives OUTSIDE the .intro container in the DOM. It mirrors the
   video frame's width and aspect ratio via --frame-w / --video-ar,
   so the logo image inside stays at the EXACT pixel-position of
   the video's centred logo on every device. Fades in near the end
   of the video and stays in place after the intro disappears.
   Phase 3 will animate this element to the header via GSAP Flip.
   ================================================================= */
.logo-overlay {
  position: fixed;
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  width: var(--frame-w);
  aspect-ratio: var(--video-ar);
  z-index: 1500;            /* above the intro (999), survives its fade */
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.5s cubic-bezier(0.2, 0.85, 0.2, 1);
}
.logo-overlay.visible { opacity: 1; }
.logo-overlay img {
  position: absolute;
  left: calc(50% + var(--logo-x, 0%));
  top:  calc(50% + var(--logo-y, 0%));
  width: var(--logo-size);
  height: auto;
  transform: translate(-50%, -50%);
  user-select: none;
  -webkit-user-drag: none;
}

@media (max-width: 768px) {
  .post-state { width: 92vw; }
  .start-btn__circle { width: 88px; height: 88px; }
  .start-btn__icon { width: 28px; height: 28px; }
  .brand-mark { bottom: 14px; right: 14px; font-size: 10px; letter-spacing: 0.22em; }
  .hint {
    font-size: 10px;
    letter-spacing: 0.28em;
    top: calc(50% + 80px);  /* smaller button (44px radius) + 32px gap + 4px baseline */
  }
  .logo-overlay { width: 92vw; }
}

/* =================================================================
   ENTRY ANIMATION — a spark drops from above the viewport to the
   top of where the start-button circle will appear, then orbits
   clockwise while a synced SVG path traces the circle's outline
   behind it. When the line is fully drawn the flare + outline
   fade out and the button (with sparkle icon) + hint reveal.
   No flash, no rays — the drawing IS the moment.
   Timeline:
     0.0s         — only background + particles
     0.4 → 1.5s   — flare falls from above to top of circle
     1.5 → 2.8s   — flare orbits clockwise; SVG path draws in sync
     2.8 → 3.3s   — flare + drawn outline fade out
     2.8 → 3.4s   — button materialises in the centre
     3.3 → 3.9s   — hint fades in below the button
     3.6 → 4.6s   — brand-mark fades in (bottom right)
   ================================================================= */

/* Zero-size wrapper at viewport centre. Rotating it around its own
   origin (which sits exactly on viewport centre) drags the flare,
   which is offset --circle-r upward, around in a perfect circle. */
.flare-wrapper {
  position: absolute;
  left: 50%; top: 50%;
  width: 0; height: 0;
  z-index: 7;
  pointer-events: none;
  transform: rotate(0deg);
  animation: orbitSpin 1.3s cubic-bezier(0.4, 0, 0.6, 1) 1.5s forwards;
}
@keyframes orbitSpin {
  from { transform: rotate(0deg);   }
  to   { transform: rotate(360deg); }
}

.entry-flare {
  position: absolute;
  left: 0; top: 0;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: radial-gradient(circle,
    rgba(255, 250, 240, 1) 0%,
    rgba(255, 226, 190, 0.92) 35%,
    rgba(212, 163, 115, 0.4) 70%,
    transparent 100%);
  box-shadow:
    0 0 14px rgba(255, 240, 220, 0.95),
    0 0 36px rgba(212, 163, 115, 0.75),
    0 0 90px rgba(212, 163, 115, 0.45);
  /* Anchored at wrapper origin; starts 70vh above. The drop ends
     at (translate -50%, -50% - circle-r) so its centre lands
     exactly on the top of the circle, where the SVG path starts. */
  transform: translate(-50%, -70vh);
  opacity: 0;
  animation:
    flareDrop      1.1s cubic-bezier(0.4, 0, 0.2, 1)    0.4s forwards,
    flareFinalFade 0.5s cubic-bezier(0.2, 0.85, 0.2, 1) 2.8s forwards;
}
/* Trailing streak above the spark — shrinks + fades as it lands,
   so nothing remains pointing inward once the orbit starts. */
.entry-flare::before {
  content: '';
  position: absolute;
  left: 50%;
  bottom: 100%;
  width: 1px;
  height: 200px;
  background: linear-gradient(to top,
    rgba(255, 240, 220, 0.95),
    rgba(212, 163, 115, 0.4) 60%,
    transparent 100%);
  transform: translateX(-50%);
  filter: blur(0.5px);
  animation: streakFade 1.1s cubic-bezier(0.4, 0, 0.2, 1) 0.4s forwards;
}
@keyframes flareDrop {
  0%   { transform: translate(-50%, -70vh); opacity: 0; }
  15%  { opacity: 1; }
  100% { transform: translate(-50%, calc(-50% - var(--circle-r))); opacity: 1; }
}
@keyframes flareFinalFade {
  from { opacity: 1; }
  to   { opacity: 0; }
}
@keyframes streakFade {
  0%   { opacity: 1; height: 200px; }
  78%  { opacity: 1; }
  100% { opacity: 0; height: 0;     }
}

/* The circle the flare draws — same diameter as the button,
   centred on viewport centre. viewBox padding of 8px on each
   side keeps the stroke + glow from being clipped. */
.circle-draw {
  position: absolute;
  left: 50%; top: 50%;
  width: var(--circle-d);
  height: var(--circle-d);
  transform: translate(-50%, -50%);
  z-index: 6;
  pointer-events: none;
  opacity: 0;
  overflow: visible;
  animation:
    circleAppear 0.05s linear                            1.5s forwards,
    circleFade   0.5s  cubic-bezier(0.2, 0.85, 0.2, 1)   2.8s forwards;
}
.circle-draw path {
  fill: none;
  stroke: rgba(212, 163, 115, 0.85);
  stroke-width: 1.25;
  stroke-linecap: round;
  /* 2π × 52 ≈ 326.726 — must match the radius in viewBox units. */
  stroke-dasharray: 326.726;
  stroke-dashoffset: 326.726;
  filter: drop-shadow(0 0 6px rgba(212, 163, 115, 0.55));
  animation: drawCircle 1.3s cubic-bezier(0.4, 0, 0.6, 1) 1.5s forwards;
}
@keyframes circleAppear { to { opacity: 1; } }
@keyframes circleFade   { to { opacity: 0; } }
@keyframes drawCircle   { to { stroke-dashoffset: 0; } }

/* Placeholder for the homepage — phase 2 will replace this with the
   real site styles in site.css. Kept here so the intro file works
   standalone if site.css is removed. */
.site {
  position: fixed;
  inset: 0;
  z-index: 1;
  background: var(--bg);
  opacity: 0;
  transition: opacity 0.9s cubic-bezier(0.2, 0.85, 0.2, 1);
}
.site.ready { opacity: 1; }

#err {
  position: fixed; top: 0; left: 0; right: 0; z-index: 9999;
  background: #2a0808; color: #ff9b9b;
  padding: 14px 18px;
  font: 12px/1.5 ui-monospace, monospace;
  white-space: pre-wrap; max-height: 50vh; overflow: auto;
  display: none;
}

/* =================================================================
   CALIBRATION PANEL — appears only when the URL has ?cal=1
   Drag the sliders to align the DOM logo with the video's logo
   paused on the final frame. The "OPACITY" slider lets you ghost
   the DOM logo to 50% so both are visible at once for alignment.
   ================================================================= */
.cal-panel {
  position: fixed;
  top: 18px;
  left: 18px;
  z-index: 2500;
  display: none;
  flex-direction: column;
  gap: 10px;
  padding: 16px 18px;
  background: rgba(7, 6, 10, 0.86);
  -webkit-backdrop-filter: blur(14px) saturate(160%);
          backdrop-filter: blur(14px) saturate(160%);
  border: 1px solid rgba(212, 163, 115, 0.45);
  border-radius: 10px;
  font-family: 'Geist Mono', ui-monospace, monospace;
  font-size: 11px;
  letter-spacing: 0.06em;
  color: rgba(212, 163, 115, 0.95);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
  min-width: 320px;
}
.cal-panel.show { display: flex; }
.cal-title {
  font-size: 9px;
  letter-spacing: 0.32em;
  text-transform: uppercase;
  color: rgba(212, 163, 115, 0.55);
  padding-bottom: 8px;
  border-bottom: 1px solid rgba(212, 163, 115, 0.15);
}
.cal-row {
  display: grid;
  grid-template-columns: 64px 1fr 56px;
  align-items: center;
  gap: 12px;
}
.cal-row label {
  font-size: 10px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: rgba(212, 163, 115, 0.75);
}
.cal-row input[type=range] {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 2px;
  background: rgba(212, 163, 115, 0.18);
  border-radius: 999px;
  outline: none;
}
.cal-row input[type=range]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #d4a373;
  cursor: pointer;
  box-shadow: 0 0 12px rgba(212, 163, 115, 0.55);
}
.cal-row input[type=range]::-moz-range-thumb {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  background: #d4a373;
  cursor: pointer;
  border: 0;
  box-shadow: 0 0 12px rgba(212, 163, 115, 0.55);
}
.cal-row span {
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-size: 11px;
  color: rgba(245, 241, 234, 0.95);
}
.cal-final {
  margin-top: 6px;
  padding-top: 10px;
  border-top: 1px solid rgba(212, 163, 115, 0.15);
  font-size: 10px;
  color: rgba(245, 241, 234, 0.78);
  user-select: text;
  line-height: 1.7;
  white-space: pre-wrap;
}
.cal-final code {
  color: rgba(212, 163, 115, 1);
  font-family: inherit;
  user-select: all;
}

/* =================================================================
   CINEMATIC AMBIENT BACKGROUND
   A pre-blurred low-bitrate H.264 video (assets/video/intro-ambient.mp4,
   ~1.65 MB · 480x270 · 24 fps) plays full-screen in lockstep behind
   the main frame. The amber light spilling around the frame is
   therefore driven by whatever's on screen, so the room appears to
   be lit by the screen itself.

   Why a separate video file (not a CSS mirror of the main video):
     · Pre-baked blur — zero CSS filter work on the GPU (the iOS
       compositor was choking on heavy CSS blur layers).
     · H.264 codec — different hardware decoder path than the
       main HEVC, so the two videos don't compete for silicon.
     · Tiny resolution — ~16x cheaper to decode per frame.
     · Tiny file — ~1.65 MB downloads quickly after the user
       gesture (iOS won't preload before that, by design).

   The uniform body-radial glow is dimmed out while this is active
   so the cinematic light dominates. Triggered by .video-playing on
   <html>, set in handleStart().
   ================================================================= */
.ambient-bg {
  position: fixed;
  inset: 0;
  width: 100vw;
  height: 100vh;
  object-fit: cover;
  pointer-events: none;
  z-index: 2;            /* above body::before, behind .intro */
  opacity: 0;
  transition: opacity 1.2s cubic-bezier(0.2, 0.85, 0.2, 1);
}
html.video-playing .ambient-bg { opacity: 1; }
/* Dim the uniform amber radial glow while the ambient projection
   is acting as the light source. Restored when the intro ends. */
html.video-playing body::before {
  opacity: 0;
  transition: opacity 1s cubic-bezier(0.2, 0.85, 0.2, 1);
}

/* ===================================================================
   CUSTOM INTRO CURSOR — desktop only.
     - Hides the native cursor over the entire intro overlay.
     - A 6px warm amber dot tracks the pointer instantly and gently
       pulses (scale + alpha) on a 1.6s loop.
     - A soft 44px amber flare lerps toward the pointer in JS so it
       lags smoothly behind on fast moves and catches up on idle.
     - Both screen-blended on the dark intro bg so they read as light.
     - Initial opacity 0 — JS adds .active on first mousemove to
       fade them in (avoids a stuck-at-origin flicker on page load).
   =================================================================== */
@media (hover: hover) and (pointer: fine) {
  .intro { cursor: none; }
  .intro * { cursor: none; }
}

.intro-cursor-dot,
.intro-cursor-flare {
  position: fixed;
  left: 0;
  top: 0;
  pointer-events: none;
  border-radius: 50%;
  opacity: 0;
  z-index: 100;
  mix-blend-mode: screen;
  transition: opacity 0.4s cubic-bezier(0.2, 0.85, 0.2, 1);
  /* Hide on touch / no-hover devices — the JS gate also bails, but
     the CSS gate keeps any FOUC-style flash from sneaking in. */
  display: none;
}
@media (hover: hover) and (pointer: fine) {
  .intro-cursor-dot,
  .intro-cursor-flare { display: block; }
}

.intro-cursor-dot {
  width: 10px;
  height: 10px;
  background: rgba(244, 217, 178, 0.95);
  /* Pulse animates box-shadow only — the dot's transform is reserved
     for JS-driven translation, so the two animations don't fight. */
  animation: intro-cursor-pulse 1.6s cubic-bezier(0.45, 0, 0.55, 1) infinite;
  will-change: transform;
}

/* Soft amber haze that trails the dot. Sized to ~96px with a very
   gradual gradient (closest-side so the corners stay transparent),
   then filter:blur smudges the edges further so it reads as light
   bleeding through, not as a defined circle. The lerp lives in JS
   — see wireIntroCursor. */
.intro-cursor-flare {
  width: 96px;
  height: 96px;
  background: radial-gradient(closest-side at 50% 50%,
                              rgba(244, 217, 178, 0.42) 0%,
                              rgba(212, 163, 115, 0.22) 22%,
                              rgba(212, 163, 115, 0.10) 48%,
                              rgba(212, 163, 115, 0.03) 72%,
                              transparent 100%);
  filter: blur(6px);
  will-change: transform;
}

.intro-cursor-dot.active,
.intro-cursor-flare.active { opacity: 1; }

@keyframes intro-cursor-pulse {
  0%, 100% {
    box-shadow:
      0 0 8px rgba(212, 163, 115, 0.55),
      0 0 18px rgba(212, 163, 115, 0.22);
  }
  50% {
    box-shadow:
      0 0 18px rgba(212, 163, 115, 0.95),
      0 0 36px rgba(212, 163, 115, 0.50);
  }
}
