/* ============================================================
   Chamber — shared atmospheric objects that recur in the hero
   and the footer (and could be reused elsewhere later).

   The page's "room" is built from three atomic, reusable objects:

     • .horizon        — 1px Casel-Red hairline at ~14% opacity.
                         The "floor" of the chamber.
     • .pool-of-light  — soft elliptical red glow on the floor.
                         Pulses with the orb on the 5s heartbeat.
     • .motes          — a few absolutely-positioned particles drifting
                         inside the beam. Atmosphere; dust in the spotlight.

   All three are "geometry, not motion" — their *resting* state must be
   visible. We never use `fade-in … forwards` to bring them in (that
   collapses to ~0ms under reduced-motion and the `forwards` fill mode
   doesn't reliably hold across browsers — the R4 beam bug). Instead:
   set baseline opacity directly, then optionally animate decoratively.
   ============================================================ */

/* ---------- Horizon line — the floor ---------- */

.horizon {
  position: absolute;
  left: 0;
  right: 0;
  /* Default — sits at 38% from the top, the AD spec. Hosts override
     per-section via --horizon-top (e.g. footer wants it higher). */
  top: var(--horizon-top, 38%);
  height: 1px;
  background: linear-gradient(
    to right,
    rgba(233, 69, 96, 0) 0%,
    rgba(233, 69, 96, 0.14) 18%,
    rgba(233, 69, 96, 0.22) 50%,
    rgba(233, 69, 96, 0.14) 82%,
    rgba(233, 69, 96, 0) 100%
  );
  /* Baseline visible — never start hidden. */
  opacity: 1;
  pointer-events: none;
  z-index: -1;
}

/* gentle ambient fade-in on first paint — but only as decoration on top
   of an already-visible element. Skipped entirely under reduced-motion. */
@media (prefers-reduced-motion: no-preference) {
  .horizon {
    /* slight initial state, transitions to full on .is-armed (set by JS
       or just immediately by the host). Resting state remains opacity: 1. */
    animation: horizon-settle 1600ms var(--ease-out) 200ms backwards;
  }
}

@keyframes horizon-settle {
  from {
    opacity: 0;
    transform: scaleX(0.6);
  }
  to {
    opacity: 1;
    transform: scaleX(1);
  }
}

/* ---------- Pool of light — the lamp on the floor ---------- */

.pool-of-light {
  position: absolute;
  left: 50%;
  /* default position — centred under .pool-of-light's host. Override per
     host (hero centres under the orb; footer is the threshold scene). */
  top: var(--pool-top, 38%);
  width: var(--pool-width, clamp(280px, 36vw, 520px));
  /* foreshortened ellipse, ~2.4:1 aspect ratio */
  aspect-ratio: 2.4 / 1;
  transform: translate(-50%, -50%);
  pointer-events: none;
  z-index: -1;
  /* Resting visible — base opacity carries the "lamp light hitting floor"
     even without animation. JS toggles .is-pulsing for the 30% lift on
     heartbeat tick. */
  opacity: var(--pool-rest-opacity, 1);
  /* The actual red is in the radial gradient; the element opacity acts
     as the master dimmer for hover / pulse / reduced-motion fallback. */
  background: radial-gradient(
    ellipse at center,
    rgba(233, 69, 96, var(--pool-peak, 0.16)) 0%,
    rgba(233, 69, 96, calc(var(--pool-peak, 0.16) * 0.55)) 32%,
    rgba(233, 69, 96, calc(var(--pool-peak, 0.16) * 0.22)) 58%,
    rgba(233, 69, 96, 0) 80%
  );
  filter: blur(8px);
  transition: opacity 600ms var(--ease-in-out),
    transform 600ms var(--ease-in-out),
    filter 600ms var(--ease-in-out);
  will-change: opacity, transform, filter;
}

/* Heartbeat-driven brighten: ~30% lift, decay over ~900ms.
   Triggered by pulses.js by adding .is-pulsing on the pool element. */
.pool-of-light.is-pulsing {
  animation: pool-pulse 900ms var(--ease-in-out);
}

@keyframes pool-pulse {
  0% {
    opacity: var(--pool-rest-opacity, 1);
    transform: translate(-50%, -50%) scale(1);
    filter: blur(8px);
  }
  35% {
    opacity: calc(var(--pool-rest-opacity, 1) * 1.3);
    transform: translate(-50%, -50%) scale(1.04);
    filter: blur(10px);
  }
  100% {
    opacity: var(--pool-rest-opacity, 1);
    transform: translate(-50%, -50%) scale(1);
    filter: blur(8px);
  }
}

/* Hover-on-host brighten — footer uses this for the door-handle response.
   Hosts opt in by adding .pool-hover-host to themselves. */
.pool-hover-host:hover .pool-of-light {
  opacity: calc(var(--pool-rest-opacity, 1) * 1.25);
  filter: blur(10px);
}

/* Tap-to-expand burst for mobile (footer-only). JS adds .is-tapped. */
.pool-of-light.is-tapped {
  animation: pool-tap 600ms var(--ease-out);
}
@keyframes pool-tap {
  0% {
    transform: translate(-50%, -50%) scale(1);
    opacity: var(--pool-rest-opacity, 1);
  }
  100% {
    transform: translate(-50%, -50%) scale(1.15);
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .pool-of-light,
  .pool-of-light.is-pulsing,
  .pool-of-light.is-tapped {
    animation: none !important;
    transition: none !important;
    /* keep the resting glow visible — it's geometry, not motion */
    opacity: var(--pool-rest-opacity, 1) !important;
    transform: translate(-50%, -50%) !important;
  }
  .pool-hover-host:hover .pool-of-light {
    opacity: var(--pool-rest-opacity, 1) !important;
    filter: blur(8px) !important;
  }
}

/* ---------- Motes — dust in the spotlight ---------- */

.motes {
  position: absolute;
  top: 0;
  /* the host (e.g. .hero__beam) provides --motes-x and --motes-h via
     the same JS that anchors the beam, so motes track the beam exactly. */
  left: var(--beam-x, calc(var(--gutter) + clamp(28px, 2.5vw, 36px)));
  height: var(--beam-h, 50vh);
  /* a narrow corridor either side of the beam — motes can sway within. */
  width: 18px;
  transform: translateX(-9px);
  pointer-events: none;
  z-index: -1;
  /* No fade-in animation — motes appear immediately. The motion is the
     drift, not the entrance. */
  opacity: 1;
}

.mote {
  position: absolute;
  top: -8px;
  left: 50%;
  width: var(--mote-size, 2px);
  height: var(--mote-size, 2px);
  border-radius: 50%;
  background: rgba(233, 69, 96, var(--mote-opacity, 0.18));
  filter: blur(0.4px);
  transform: translate(-50%, 0);
  will-change: transform, opacity;
  animation:
    mote-fall var(--mote-fall, 9s) linear var(--mote-delay, 0s) infinite,
    mote-sway var(--mote-sway, 5s) ease-in-out var(--mote-delay, 0s) infinite;
}

@keyframes mote-fall {
  0% {
    transform: translate(-50%, 0);
    opacity: 0;
  }
  6% {
    opacity: 1;
  }
  90% {
    opacity: 1;
  }
  100% {
    transform: translate(-50%, calc(var(--beam-h, 50vh) - 12px));
    opacity: 0;
  }
}

@keyframes mote-sway {
  0%, 100% {
    margin-left: -2px;
  }
  50% {
    margin-left: 2px;
  }
}

@media (prefers-reduced-motion: reduce) {
  /* Static dust field — geometry retained, motion killed. Each mote
     is frozen at a slightly different y via the per-mote --mote-frozen-y.
     The JS sets a deterministic y for each so the field looks intentional. */
  .mote {
    animation: none !important;
    opacity: 1;
    transform: translate(-50%, var(--mote-frozen-y, 30%));
  }
}
