/* ─────────────────────────────────────────────────────────────────────────
   Bishop Systems seasonal kit: HALLOWEEN ORNAMENTS
   Bats and jack-o-lanterns, in depth, with motion.
   Pairs with halloween-ornaments.js. Link AFTER halloween.css.

   Optional. halloween.css alone is the restrained atmosphere-only install;
   this adds the scene on top. A client can take one without the other.

   THE RULE EVERYTHING HERE OBEYS
   Every ornament sits BEHIND the content, at low opacity, in the margins of
   the layout, and never crosses text. A visitor should feel that the site is
   dressed for October before they notice there is a bat on it. That is the
   line between seasonal and costume, and these are sites where people book a
   roof repair and file an insurance claim.

   Turn the scene off on one page without removing the files:
     <html data-season-ornaments-off>
   ───────────────────────────────────────────────────────────────────────── */

:root[data-season="halloween"] {
  --hwo-bat: #1A1526;          /* bat silhouette. Near-black, faintly violet. */
  --hwo-bat-rim: #FFB347;      /* ember rim, so the silhouette reads on dark too */
  --hwo-pumpkin: #E8802A;      /* pumpkin body */
  --hwo-pumpkin-deep: #B4530F; /* ribs and shading */
  --hwo-pumpkin-face: #FFD08A; /* lit carving */
  --hwo-pk-violet: #A855F7;    /* the purple half of the 3D pumpkin's ribs */
  --hw-stem: #3F6B3A;         /* stem */
  --hwo-scene-opacity: 1;      /* global dimmer for the whole scene */
}

/* ── The stage ─────────────────────────────────────────────────────────────
   `fixed` + `pointer-events: none` + z-index 0 is the contract that makes this
   safe to drop onto a site nobody has read: it cannot shift a single element,
   cannot trap a click, cannot break a sticky header, and cannot cover a form.

   `perspective` here rather than on each ornament so every bat shares one
   vanishing point. Per-element perspective gives each its own, and the flock
   stops looking like it occupies the same sky. */
.hwo-scene {
  position: fixed;
  inset: 0;
  /* ABOVE the content, not behind it.
     At z-index 0 this sat under the first opaque section on every real client
     site and the bats were never seen once: verified on a dealership hero that
     is a full-bleed photograph. The atmosphere fog in halloween.css must stay
     BELOW content, because it is a full-viewport wash and raising it dims the
     nav and the cards. This layer is the opposite: sparse transparent SVG over
     a transparent stage, so above the content it obscures nothing and is the
     only way the flock is ever visible. `pointer-events: none` still means it
     cannot take a click from anything underneath it. */
  z-index: 40;
  pointer-events: none;
  overflow: hidden;
  perspective: 900px;
  opacity: var(--hwo-scene-opacity);
  contain: strict;
}

/* ── Bats ──────────────────────────────────────────────────────────────────
   Two nested animations, deliberately separated:
     the wrapper flies      (long, linear, crosses the screen)
     the inner bobs         (short, eased, rises and falls)
   One element doing both would need a single keyframe track combining a 30s
   traverse with a 1.5s bob, which means either a very long keyframe list or a
   bob that stutters. Nesting lets each run at its own tempo. */
.hwo-bat {
  position: absolute;
  /* `--hwo-sky-offset` is added by any site with a fixed header, so the flock
     starts below the chrome instead of behind it. Defaults to 0. */
  top: calc(var(--top, 20%) + var(--hwo-sky-offset, 0px));
  left: 0;
  width: calc(74px * var(--depth, 1));
  /* Depth does three things at once, which is what sells distance: further
     away is smaller, dimmer and softer. Changing only size reads as "small
     bat", not "far bat". */
  /* Tuned up from 0.10-0.30 after the first smoke test: as a dark silhouette
     on a light client site the flock was technically present and effectively
     invisible, which is the worst of both, all of the cost and none of the
     season. Still a silhouette, never a solid. */
  opacity: calc(0.34 + 0.42 * var(--depth, 1));
  filter: blur(calc((1 - var(--depth, 1)) * 2.4px));
  animation: hwo-fly var(--dur, 28s) linear infinite;
  animation-delay: var(--delay, 0s);
  animation-direction: var(--dir, normal);
  will-change: transform;
}

.hwo-bat-svg {
  display: block;
  width: 100%;
  height: auto;
  fill: var(--hwo-bat);
  /* A warm rim, and it is doing real work rather than decoration.
     A near-black silhouette is legible on a light page and completely gone on
     a dark one, and these four sites are a mix, one of them a full-bleed dusk
     photograph where the flock was invisible. The ember glow reads against a
     dark hero, the dark body reads against a light one, so one treatment
     covers both without having to detect which kind of site it landed on. */
  filter: drop-shadow(0 0 3px color-mix(in oklch, var(--hwo-bat-rim) 85%, transparent))
          drop-shadow(0 0 9px color-mix(in oklch, var(--hwo-bat-rim) 45%, transparent));
  animation: hwo-bob 2.6s ease-in-out infinite;
  animation-delay: var(--delay, 0s);
}

/* Wings beat around the shoulder, not the centre of the bounding box, or the
   whole bat appears to shear rather than flap. */
.hwo-bat-wing {
  transform-origin: 60px 30px;
  animation: hwo-flap 0.42s ease-in-out infinite;
}

.hwo-bat-wing-r {
  /* Mirrored timing so the two wings beat together rather than paddling. */
  animation-name: hwo-flap-r;
}

/* translate3d, not `left`. A transform stays on the compositor; animating
   `left` relayouts the page on every frame of a 30-second loop, on every
   client site, forever. */
@keyframes hwo-fly {
  0%   { transform: translate3d(-14vw, 0, 0) rotateY(0deg); }
  50%  { transform: translate3d(52vw, 0, -120px) rotateY(9deg); }
  100% { transform: translate3d(118vw, 0, 0) rotateY(0deg); }
}

@keyframes hwo-bob {
  0%, 100% { transform: translate3d(0, 0, 0) rotate(-3deg); }
  50%      { transform: translate3d(0, -14px, 0) rotate(3deg); }
}

@keyframes hwo-flap {
  0%, 100% { transform: rotateY(0deg) rotateZ(0deg); }
  50%      { transform: rotateY(58deg) rotateZ(-13deg); }
}

@keyframes hwo-flap-r {
  0%, 100% { transform: rotateY(0deg) rotateZ(0deg); }
  50%      { transform: rotateY(-58deg) rotateZ(13deg); }
}

/* ── Pumpkins ──────────────────────────────────────────────────────────────
   Bottom corners only, where no layout puts content. A row along the footer
   reads as a border; two in the corners reads as a porch. */
.hwo-pumpkin {
  position: absolute;
  /* Sits ON the bottom edge, not through it. Negative bottom cropped the
     carved face, which is the only part of a jack-o-lantern worth drawing. */
  bottom: clamp(10px, 2vh, 26px);
  width: var(--size, 72px);
  opacity: calc(0.62 + 0.3 * var(--depth, 1));
  filter: blur(calc((1 - var(--depth, 1)) * 1.1px));
}

/* The right-hand pumpkin is pushed well inboard because the bottom-right
   corner is where every chat widget on earth docks, and on the dealership site
   it landed directly behind the bubble. Bottom-left is the only reliably empty
   corner, so the larger pumpkin lives there. */
.hwo-pumpkin-left  { left: clamp(6px, 2.5vw, 44px); }
.hwo-pumpkin-right { right: clamp(84px, 11vw, 190px); }

/* The light it throws into the room. Separate element so it can pulse without
   the SVG repainting. */
.hwo-pk-glow {
  position: absolute;
  left: 50%;
  bottom: 0;
  width: 240%;
  aspect-ratio: 1;
  transform: translate(-50%, 32%);
  border-radius: 50%;
  background: radial-gradient(
    circle,
    color-mix(in oklch, var(--hwo-pumpkin-face) 24%, transparent) 0%,
    transparent 64%
  );
  animation: hwo-candle 3.4s ease-in-out infinite;
  animation-delay: var(--delay, 0s);
}

/* Irregular on purpose. A clean sine reads as a pulsing LED; a candle gutters. */
@keyframes hwo-candle {
  0%, 100% { opacity: 1; }
  18%      { opacity: 0.82; }
  31%      { opacity: 1; }
  47%      { opacity: 0.7; }
  58%      { opacity: 0.95; }
  74%      { opacity: 0.78; }
}

/* ── Reduced motion ────────────────────────────────────────────────────────
   The scene stays, the motion goes. Stripping the ornaments entirely would
   punish someone for a comfort setting by giving them a plainer site; freezing
   them keeps the season and removes the thing that actually causes trouble.
   The bats are parked at readable positions rather than at 0% of their flight
   path, which is off-screen. */
@media (prefers-reduced-motion: reduce) {
  .hwo-bat,
  .hwo-bat-svg,
  .hwo-bat-wing,
  .hwo-pk-face,
  .hwo-pk-glow {
    animation: none !important;
  }

  .hwo-bat { transform: translate3d(22vw, 0, 0); }
  .hwo-bat:nth-of-type(even) { transform: translate3d(68vw, 0, 0); }
}

.hwo-scene[data-reduced="true"] .hwo-bat,
.hwo-scene[data-reduced="true"] .hwo-bat-svg,
.hwo-scene[data-reduced="true"] .hwo-bat-wing,
.hwo-scene[data-reduced="true"] .hwo-pk-face,
.hwo-scene[data-reduced="true"] .hwo-pk-glow {
  animation: none !important;
}

/* ── Restraint on small screens ────────────────────────────────────────────
   The JS already skips bats under 640px. This shrinks the pumpkins so they sit
   in the corner of a phone rather than squatting across the footer. */
@media (max-width: 640px) {
  .hwo-pumpkin { --size: 46px; opacity: 0.32; }
  .hwo-pumpkin-right { display: none; }
}

/* A printed estimate does not arrive with bats on it. */
@media print {
  .hwo-scene { display: none !important; }
}


/* ═══════════════════════════════════════════════════════════════════════════
   THE 3D PUMPKIN
   ═══════════════════════════════════════════════════════════════════════════

   A real rotating volume built from CSS 3D transforms. See the comment in
   halloween-ornaments.js for why this is not WebGL: a shader that fails to
   compile paints nothing and says nothing, and this exact site has shipped a
   black hero because of it. CSS 3D either composites or degrades to a readable
   flat stack. There is no silent-black failure mode.

   Orange AND purple at the same time, which is the brief: the ribs run copper
   at the front of the sweep to violet at the back, so the mix visibly changes
   as the object turns rather than being one flat colour on a spinning shape. */

.hwo-pk3d {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 1;
  /* The camera. On the wrapper rather than the spinner, so the vanishing point
     stays put while the object rotates inside it. Perspective on the rotating
     element itself makes the whole volume appear to wobble. */
  perspective: 420px;
}

.hwo-pk3d-spin {
  position: absolute;
  inset: 0;
  transform-style: preserve-3d;
  animation: hwo-pk-spin 15s linear infinite;
}

/* One rib. Twelve of these, each rotated a further 180/n degrees around Y,
   sweep out a solid of revolution. `--i` and `--n` come from the markup so the
   count can change in one place in the JS without touching this file. */
.hwo-pk3d-rib {
  position: absolute;
  /* Squat, not spherical. Even insets gave a wireframe globe: a pumpkin is
     wider than it is tall, so the ribs are ellipses pinched on the vertical. */
  inset: 13% 3%;
  border-radius: 50%;
  border: 2px solid;
  /* The colour mix. `--i / --n` walks 0 to 1 across the ribs, and feeding that
     into the hue interpolation gives copper on one side and violet on the
     other, with the blend visible through the middle of the sweep. */
  border-color: color-mix(
    in oklch,
    var(--hwo-pumpkin) calc(100% - (var(--i) / var(--n)) * 100%),
    var(--hwo-pk-violet)
  );
  transform: rotateY(calc(var(--i) * (180deg / var(--n))));
  /* Each rib is a hollow outline, so the stack reads as ridges rather than as a
     dozen opaque discs. The faint fill gives the volume a body without hiding
     the ribs behind it. */
  /* Heavier fill than the first pass. Thin outlines alone read as a wireframe;
     a pumpkin needs a body with ridges on it, so each rib carries enough fill
     that twelve of them stack into something solid while the borders still
     show as ribs. */
  background: radial-gradient(
    circle at 36% 30%,
    color-mix(in oklch, var(--hwo-pumpkin) 46%, transparent) 0%,
    color-mix(in oklch, var(--hwo-pumpkin) 30%, transparent) 44%,
    color-mix(in oklch, var(--hwo-pk-violet) 26%, transparent) 76%,
    transparent 92%
  );
}

/* The lit carving, on a plane pushed to the front of the volume so it rotates
   away and comes back around rather than being painted on the surface. */
.hwo-pk3d-face {
  position: absolute;
  inset: 26% 20%;
  /* Far enough forward to sit ON the front of the volume, not inside it.
     At 34px the face was behind the front ribs, and once those ribs got a
     heavier fill it disappeared entirely. The ribs span roughly 47% of the
     box, so the front surface is a bit under half the width out.

     ⚠ translateZ does NOT accept a percentage. `translateZ(46%)` is invalid,
     the whole transform is dropped, and the face silently stays buried inside
     the body with nothing logged. It has to be a length, so it is computed
     from --size, which the JS already sets per pumpkin. */
  transform: translateZ(calc(var(--size, 96px) * 0.42));
  transform-style: preserve-3d;
}

.hwo-pk3d-face svg {
  width: 100%;
  height: 100%;
  display: block;
  fill: var(--hwo-pumpkin-face);
  filter: drop-shadow(0 0 4px color-mix(in oklch, var(--hwo-pumpkin-face) 95%, transparent))
          drop-shadow(0 0 12px color-mix(in oklch, var(--hwo-pumpkin) 80%, transparent));
  animation: hwo-candle 3.4s ease-in-out infinite;
  animation-delay: var(--delay, 0s);
}

/* `backface-visibility` on the face plane would hide it for half the rotation,
   which looks like a bug rather than like the back of a pumpkin. Instead it
   fades as it turns away, via the same spin timing. */
.hwo-pk3d-face {
  animation: hwo-pk-face-fade 15s linear infinite;
}

.hwo-pk3d-stem {
  position: absolute;
  top: 2%;
  left: 46%;
  width: 8%;
  height: 15%;
  border-radius: 3px 3px 1px 1px;
  background: var(--hwo-stem);
  transform: rotateZ(-11deg) translateZ(6px);
}

/* The candle inside. Sits at the centre of the volume and does not rotate, so
   the light stays put while the ribs sweep across it, which is what a real
   lantern does. */
.hwo-pk3d-core {
  position: absolute;
  inset: 28% 20%;
  border-radius: 50%;
  background: radial-gradient(
    circle,
    color-mix(in oklch, var(--hwo-pumpkin-face) 62%, transparent) 0%,
    color-mix(in oklch, var(--hwo-pumpkin) 26%, transparent) 46%,
    transparent 72%
  );
  filter: blur(6px);
  animation: hwo-candle 3.4s ease-in-out infinite;
  animation-delay: var(--delay, 0s);
}

@keyframes hwo-pk-spin {
  from { transform: rotateY(0deg) rotateX(-8deg); }
  to   { transform: rotateY(360deg) rotateX(-8deg); }
}

/* Bright when the face is toward the camera, dim when it has turned away.
   Tied to the same 15s period as the spin so the two never drift apart. */
@keyframes hwo-pk-face-fade {
  0%, 100% { opacity: 1; }
  28%      { opacity: 0.18; }
  50%      { opacity: 0.08; }
  72%      { opacity: 0.18; }
}

/* Reduced motion: the volume stays, the rotation stops, parked at a three
   quarter angle so it still reads as a solid rather than as a flat disc. */
@media (prefers-reduced-motion: reduce) {
  .hwo-pk3d-spin {
    animation: none !important;
    transform: rotateY(28deg) rotateX(-8deg);
  }
  .hwo-pk3d-face { animation: none !important; opacity: 1; }
  .hwo-pk3d-core { animation: none !important; }
}

.hwo-scene[data-reduced="true"] .hwo-pk3d-spin {
  animation: none !important;
  transform: rotateY(28deg) rotateX(-8deg);
}
.hwo-scene[data-reduced="true"] .hwo-pk3d-face,
.hwo-scene[data-reduced="true"] .hwo-pk3d-core { animation: none !important; }
.hwo-scene[data-reduced="true"] .hwo-pk3d-face { opacity: 1; }
