/* =========================================================================
   GulfRabit · Noor — THE SKY
   =========================================================================

   Noor gave the shop a night. This gives that night a SKY: a moon that hangs
   over the whole site, meteors that fall when they feel like it, fireflies
   that drift up off the water, and — rarely enough that catching one is
   luck — a phoenix crossing the dark.

   THE ONE IDEA
   ------------
   Ambient motion earns its place by being UNPREDICTABLE and UNIMPORTANT.
   Predictable motion becomes wallpaper: the eye files a 27-second loop as
   furniture by the third pass and stops seeing it. So nothing here runs on
   a fixed cycle — noor-sky.js rolls a die for what happens next and when,
   and every element that appears is thrown away when it lands. And because
   it is unimportant, it is all pointer-events:none, aria-hidden, behind the
   content, and gone entirely for anyone who asked for reduced motion. A
   customer reading a price must never be competing with a bird.

   THE LAYERING — AND THE RULE BEHIND IT
   -------------------------------------
   Permanent bodies are FAR; transient events are NEAR.

     z-index -1   the lagoon glow (theme-noor.css), the moon, the stars
      1          fireflies rising off the water
      3          meteors, the phoenix

   The moon earns -1 the hard way: at 0 it painted over the product grid, and
   a hundred-pixel pale disc sitting on a photograph of dates does not read as
   a moon, it reads as a broken image. Anything permanent and opaque must go
   behind the merchandise. The streaks may cross over content — that is the
   shipped Noor ember's own precedent, and a thin translucent line passing the
   page reads as weather rather than damage.

   Nothing here may ever rise over the header, a drawer or a modal.

   PERFORMANCE, BECAUSE THIS RUNS ON EVERY PAGE
   --------------------------------------------
   Every animation here moves `transform` and `opacity` only — the two
   properties a compositor can run without touching layout or paint. No
   element is animated by width, top or filter. The conductor caps how many
   exist at once, deletes each one on animationend, and stops rolling
   entirely while the tab is hidden.
   ========================================================================= */

/* =========================================================================
   1 · THE MOON
   =========================================================================
   One object, always there, never moving — the anchor everything else is
   read against. It is a disc lit from the upper left with a soft terminator,
   and it breathes: a 14-second halo swell so slight it reads as air rather
   than animation.

   The crescent is cut with a second radial rather than a mask, because a
   masked element cannot be composited as cheaply, and this thing is on
   screen for the entire visit.
   ========================================================================= */
[data-theme="noor"] .noor-moon {
  position: fixed;
  /* Below the header, deliberately. At 7vh the disc sat behind the account
     and cart icons — white glyphs on a bright moon, the one place in this
     file where decoration could cost someone a tap. Clear of the chrome it
     is a moon; inside it, it is a contrast bug. */
  top: clamp(132px, 19vh, 230px);
  right: clamp(20px, 5vw, 96px);
  width: clamp(54px, 7vw, 104px);
  aspect-ratio: 1;
  z-index: -1;                       /* behind the merchandise — see the header note */
  pointer-events: none;
  border-radius: 50%;
  background:
    radial-gradient(circle at 34% 30%, #FFF6DE 0%, #F3E4B8 42%, #D8C089 68%, #B99F63 100%);
  box-shadow:
    0 0 30px rgba(244, 231, 190, .28),
    0 0 90px rgba(201, 162, 75, .18),
    inset -8px -6px 18px rgba(90, 70, 30, .35);
  opacity: .92;
  animation: noor-moon-breathe 14s var(--ease-inout, ease-in-out) infinite alternate;
}

/* The maria — the dark seas. Three faint blots, so the disc reads as a
   moon rather than a lamp. Purely decorative, purely still. */
[data-theme="noor"] .noor-moon::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background:
    radial-gradient(circle at 62% 38%, rgba(120, 96, 48, .22) 0 14%, transparent 15%),
    radial-gradient(circle at 44% 62%, rgba(120, 96, 48, .18) 0 11%, transparent 12%),
    radial-gradient(circle at 70% 68%, rgba(120, 96, 48, .14) 0 8%, transparent 9%);
}

/* The halo, on its own element so the disc's own opacity stays put. */
[data-theme="noor"] .noor-moon::after {
  content: "";
  position: absolute;
  inset: -60%;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(244, 231, 190, .16) 0%, transparent 62%);
  animation: noor-moon-halo 14s var(--ease-inout, ease-in-out) infinite alternate;
}

@keyframes noor-moon-breathe {
  from { transform: scale(1);     opacity: .88; }
  to   { transform: scale(1.018); opacity: .96; }
}
@keyframes noor-moon-halo {
  from { transform: scale(.94); opacity: .55; }
  to   { transform: scale(1.06); opacity: 1; }
}

/* =========================================================================
   2 · THE STARS
   =========================================================================
   A handful of fixed points around the moon, each twinkling on its own
   offbeat. Not a full starfield: a dark page needs the RESTRAINT of a few
   real points more than it needs a planetarium, and every extra element is
   a composited layer on a phone.

   Position and delay come from the conductor as custom properties, so the
   sky is arranged differently on every load without a line of CSS knowing.
   ========================================================================= */
[data-theme="noor"] .noor-star {
  position: fixed;
  top: var(--star-y, 10vh);
  left: var(--star-x, 50vw);
  width: var(--star-size, 2px);
  aspect-ratio: 1;
  z-index: -1;
  pointer-events: none;
  border-radius: 50%;
  background: #FFF6DE;
  box-shadow: 0 0 6px rgba(255, 246, 222, .9);
  opacity: 0;
  animation: noor-star-twinkle var(--star-dur, 6s) ease-in-out var(--star-delay, 0s) infinite;
}
/* Scintillation is not a sine wave. A star twinkles because pockets of air of
   different density drift across the line of sight, so the brightness jitters
   on no schedule at all — it lifts, half-drops, surges, settles. The old
   two-stop keyframe was a metronome, and nine metronomes at nine tempos still
   read as machinery. These stops are deliberately uneven; each star gets its
   own duration and offset from the conductor on top. */
@keyframes noor-star-twinkle {
  0%   { opacity: .16; transform: scale(.86); }
  17%  { opacity: .62; transform: scale(1.05); }
  28%  { opacity: .31; transform: scale(.93); }
  47%  { opacity: .92; transform: scale(1.18); }
  59%  { opacity: .44; transform: scale(1.00); }
  76%  { opacity: .74; transform: scale(1.11); }
  88%  { opacity: .27; transform: scale(.91); }
  100% { opacity: .16; transform: scale(.86); }
}

/* =========================================================================
   3 · THE METEORS — "star falls"
   =========================================================================
   A head with a tail behind it, thrown across the sky at an angle the
   conductor picks. The tail is a gradient on the same element (no second
   node), and the whole streak travels on ONE transform.

   Two sizes, weighted heavily toward the small: a sky where every meteor is
   a fireball is a video game. The rare bright one is what makes the small
   ones feel like a sky at all.

   THE THREE THINGS THAT MAKE A STREAK READ AS A FALLING BODY
   ----------------------------------------------------------
   All three were wrong here, and all three are the same class of mistake:
   the streak was DRAWN by one number and MOVED by another, and nothing
   forced the two to agree.

   1. IT MUST POINT WHERE IT IS GOING. The meteor travels down and to the
      LEFT, so its long axis must too — `rotate(180deg - angle)`, not
      `rotate(angle)`. Rotating by the raw angle points the streak down-RIGHT
      while it slides down-left: a mirror image of its own velocity. That is
      the single detail that makes a meteor read as a decal being dragged
      across the page, and the file previously claimed to have fixed it while
      doing exactly the opposite. Because the axis flipped, the gradient
      flipped with it — the bright head now sits at the 100% (leading) end.

   2. ITS ANGLE MUST SURVIVE THE VIEWPORT. `--m-dy` is now in **vw**, the
      same unit as `--m-dx`. It used to be vh, which sounds harmless and is
      not: 1vh and 1vw are different lengths, so a streak declared at 30deg
      actually fell at 18deg on a 16:9 desktop and at something else again on
      a phone. One unit for both legs of the triangle is what makes the drawn
      angle and the travelled angle the same angle on every screen.

   3. ITS SPEED MUST FOLLOW FROM ITS DISTANCE. The conductor now rolls a
      VELOCITY and divides, instead of rolling a duration and hoping. Rolling
      both independently put a 4.2x speed spread in one sky — two meteors of
      the same size, one strolling and one snapping past.

   And the taper: `transform-origin` is the head, so the tail retracts INTO
   the head as the rock burns out rather than shrinking from both ends. Real
   meteors are longest at peak burn and collapse to a point at the end.
   ========================================================================= */
[data-theme="noor"] .noor-meteor {
  position: fixed;
  top: var(--m-y, 12vh);
  left: var(--m-x, 100vw);
  width: var(--m-len, 130px);
  height: var(--m-thick, 2px);
  z-index: 3;
  pointer-events: none;
  border-radius: 999px;
  /* Bright head on the LEADING end, tail dissolving behind it. With the axis
     now pointing along the velocity, the leading end is 100%, not 0%. */
  background: linear-gradient(90deg,
    transparent 0%,
    rgba(232, 207, 142, .06) 22%,
    rgba(255, 243, 196, .55) 72%,
    #FFF8DC 100%);
  box-shadow: 0 0 12px rgba(255, 243, 196, .55);
  opacity: 0;
  /* The head is the pivot AND the anchor the tail retracts toward. */
  transform-origin: 100% 50%;
  transform: rotate(var(--m-angle, 158deg));
  /* Linear. A meteor crosses in about a second; over that span its apparent
     motion is constant, and the ease-out that used to be here made every one
     of them look like it was landing in treacle. */
  animation: noor-meteor-fall var(--m-dur, 1.5s) linear forwards;
  will-change: transform, opacity;
}

/* The bright one. Same geometry, richer light — and a whisper of cyan at the
   head, because the lagoon glow above it is cyan and a purely gold sky
   ignores its own weather.
 *
 * 90deg, matching the base rule. It was left at 270deg when the base rule was
 * flipped, which put the white head on the TRAILING end — so roughly one
 * meteor in six flew tail-first, wearing the exact defect the commit that
 * flipped the base rule was written to remove, on the only meteors bright
 * enough for anyone to notice. */
[data-theme="noor"] .noor-meteor.is-bright {
  background: linear-gradient(90deg,
    transparent 0%,
    rgba(232, 207, 142, .10) 20%,
    rgba(255, 243, 196, .75) 68%,
    #FFFFFF 100%);
  box-shadow:
    0 0 18px rgba(255, 246, 222, .8),
    0 0 42px rgba(27, 180, 212, .35);
}

/* -- the earthgrazer ------------------------------------------------------ */
/* A rock that hits the atmosphere at a shallow enough angle to skip along it
   instead of diving through. It travels an enormously longer path in much
   thinner air, so it is SLOW — seconds, not tenths — long, and dim. It is
   the rarest thing a real meteor watcher hopes for and the most convincing
   thing in this file, precisely because it breaks the rule the other streaks
   establish: everything else falls fast and steeply, and then one crawls. */
[data-theme="noor"] .noor-meteor.is-grazer {
  background: linear-gradient(90deg,
    transparent 0%,
    rgba(232, 207, 142, .04) 30%,
    rgba(255, 243, 196, .3) 76%,
    rgba(255, 248, 220, .85) 100%);
  box-shadow: 0 0 9px rgba(255, 243, 196, .35);
}

/* -- the bolide, and its fragments ---------------------------------------- */
/* A fireball big enough to survive down into dense air, where the pressure
   in front of it finally exceeds what the rock can take and it BREAKS. The
   pieces separate slowly, on very slightly diverging tracks, and each burns
   out well before the parent does because each is smaller. The conductor
   places them at the real position of the break and gives each its own
   fractionally different angle — they must not fan out symmetrically, which
   is what makes a firework read as a firework. */
[data-theme="noor"] .noor-meteor.is-bolide {
  box-shadow:
    0 0 24px rgba(255, 248, 220, .95),
    0 0 60px rgba(240, 169, 60, .5),
    0 0 96px rgba(27, 180, 212, .28);
}

/* Every keyframe carries the same three functions in the same order, or the
   browser cannot interpolate them componentwise and falls back to a matrix
   blend that wobbles the angle mid-flight. */
@keyframes noor-meteor-fall {
  0% {
    transform: translate3d(0, 0, 0) rotate(var(--m-angle, 158deg)) scaleX(.3);
    opacity: 0;
  }
  10% { opacity: .6; }
  /* Peak burn: densest air, longest tail, brightest head. */
  55% {
    transform: translate3d(calc(var(--m-dx, -110vw) * .55), calc(var(--m-dy, 38vw) * .55), 0)
               rotate(var(--m-angle, 158deg)) scaleX(1);
    opacity: 1;
  }
  78% { opacity: .8; }
  100% {
    transform: translate3d(var(--m-dx, -110vw), var(--m-dy, 38vw), 0)
               rotate(var(--m-angle, 158deg)) scaleX(.18);
    opacity: 0;
  }
}

/* =========================================================================
   4 · THE FIREFLIES
   =========================================================================
   theme-noor.css already carries a pair of drifting motes on ::before and
   ::after. These are different animals: individual insects with a WANDER —
   each one climbs on its own irregular path, pulsing at its own rate, and
   dies when it reaches the top.

   Two nested elements, and the reason is worth stating: the outer node owns
   the drift (a long, slow translate), the inner owns the pulse (opacity and
   a small scale). Combining them on one node means one animation must
   describe both, and any change to the path re-times the glow.
   ========================================================================= */
[data-theme="noor"] .noor-fly {
  position: fixed;
  bottom: -10px;
  left: var(--f-x, 50vw);
  width: var(--f-size, 4px);
  aspect-ratio: 1;
  z-index: 1;
  pointer-events: none;
  animation: noor-fly-rise var(--f-dur, 18s) linear forwards;
  will-change: transform;
}

/* The gust (section 12) leans the ANIMATED bodies that are light enough. It
   rides the standalone `translate` property, NOT `transform`, and that is the
   entire trick: `translate` composes with whatever `transform` animation the
   element is already running instead of overwriting it, so a firefly can be
   blown sideways in the middle of its own climb. Declared on the base class
   so that REMOVING .is-gusted eases the element back rather than snapping it.

   The SIMULATED bodies are deliberately absent. They do not need leaning —
   noor-physics.js measures their drag against a moving air mass, so the same
   gust reaches them as an actual force and each one answers it according to
   its own mass. Leaning them as well would push them twice. */
[data-theme="noor"] .noor-fly,
[data-theme="noor"] .noor-smoke {
  transition: translate 1.5s var(--ease-inout, ease-in-out);
}
[data-theme="noor"] .is-gusted { translate: var(--gust, 0) 0; }

/* =========================================================================
   4b · THE BODIES THE INTEGRATOR OWNS
   =========================================================================
   Four of the effects in this file are not animated at all. noor-physics.js
   steps them through gravity, buoyancy, quadratic drag and a shared wind
   field, and writes their transform every frame. See that file for why those
   four and not the other twenty-four.

   THE CONTRACT, WHICH IS THE ONLY THING CSS MUST HONOUR HERE
   ----------------------------------------------------------
   A simulated body is anchored at the ORIGIN and positioned entirely by its
   transform, because the engine writes absolute viewport pixels into that
   transform. Anchoring it with `left`/`bottom` as well would mean two systems
   each believing they decide where the element is, and the bug that produces
   only shows up once the viewport is resized.

   `translate: -50% -50%` is how the body's position comes to mean its CENTRE
   rather than its top-left corner — and it has to be the standalone property
   rather than a transform function, for the same composition reason the gust
   uses it: the engine owns `transform` outright and would overwrite it.

   No `animation` may be declared on these four. A running keyframe writing
   the same property every frame would fight the engine and win, silently,
   because animations beat inline styles in the cascade. Sub-element flourishes
   (the lantern's flame, the spark's two layers) are fine — they animate
   opacity on a ::before, which the engine never touches.
   ========================================================================= */
[data-theme="noor"] .noor-spark,
[data-theme="noor"] .noor-bubble,
[data-theme="noor"] .noor-fish,
[data-theme="noor"] .noor-fanous {
  translate: -50% -50%;
  /* Invisible until the engine has placed it. The engine writes an inline
     opacity on its very first step, so this only ever applies to a node that
     is in the document but not yet simulated — and it makes the failure mode
     "nothing appears" rather than "a bright dot is parked in the top-left
     corner of the shop", which is the difference between a bug nobody
     notices and a bug on the home page. */
  opacity: 0;
}
[data-theme="noor"] .noor-fly::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: #FFF3C4;
  box-shadow:
    0 0 8px rgba(255, 243, 196, .9),
    0 0 18px rgba(201, 162, 75, .5);
  animation: noor-fly-pulse var(--f-pulse, 2.4s) ease-in-out infinite;
}

/* The wander is written into the path itself rather than a second animation:
   percentage keyframes with alternating sideways drift read as an insect
   making decisions, where a sine-wave transform reads as a machine.
 *
 * TWO PHYSICAL CORRECTIONS TO THAT PATH
 * -------------------------------------
 * 1. NO CORNERS. The old keyframes ran on `linear`, which meant the insect
 *    reversed direction at full speed — an instantaneous velocity flip, which
 *    is an infinite acceleration, which is a visible kink. A body with mass
 *    decelerates into a turn and accelerates out of it, so every stop carries
 *    its own ease-in-out and the sideways velocity passes through zero at
 *    each reversal.
 * 2. NO CRUISE CONTROL. The climb used to be five identical 22vh steps in
 *    five identical slices of time — a constant vertical velocity, which is
 *    an elevator, not an insect. The steps below are uneven (18, 21, 18, 23,
 *    28) so the firefly labours, coasts and labours again, and the eased
 *    horizontal turns never line up into a beat.
 */
@keyframes noor-fly-rise {
  0%   { transform: translate3d(0, 0, 0); animation-timing-function: var(--ease-inout, ease-in-out); }
  20%  { transform: translate3d(calc(var(--f-sway, 3vw) *  1),    -18vh, 0); animation-timing-function: var(--ease-inout, ease-in-out); }
  40%  { transform: translate3d(calc(var(--f-sway, 3vw) * -0.6),  -39vh, 0); animation-timing-function: var(--ease-inout, ease-in-out); }
  60%  { transform: translate3d(calc(var(--f-sway, 3vw) *  1.3),  -57vh, 0); animation-timing-function: var(--ease-inout, ease-in-out); }
  80%  { transform: translate3d(calc(var(--f-sway, 3vw) * -0.3),  -80vh, 0); animation-timing-function: var(--ease-inout, ease-in-out); }
  100% { transform: translate3d(calc(var(--f-sway, 3vw) *  0.7), -108vh, 0); }
}
@keyframes noor-fly-pulse {
  0%, 100% { opacity: .15; transform: scale(.7); }
  45%      { opacity: 1;   transform: scale(1.25); }
}

/* =========================================================================
   5 · THE PHOENIX
   =========================================================================
   The rare one. A bird of fire crossing the sky in a long shallow arc,
   trailing embers, maybe once in several minutes — and never at a fixed
   interval, so it cannot be waited for. Most visits will not see it. That
   is the design: a thing that always happens is a feature, a thing that
   might happen is a memory.

   Drawn as an inline SVG data URI (no network request, no sprite to 404)
   and animated on transform alone. The wing beat is a scaleY on the inner
   element, which is why the bird is two nodes and not one.
   ========================================================================= */
[data-theme="noor"] .noor-phoenix {
  position: fixed;
  top: var(--p-y, 22vh);
  left: -140px;
  width: clamp(46px, 5vw, 78px);
  aspect-ratio: 1;
  z-index: 3;
  pointer-events: none;
  opacity: 0;
  animation: noor-phoenix-cross var(--p-dur, 9s) cubic-bezier(.4, 0, .6, 1) forwards;
  will-change: transform, opacity;
}
[data-theme="noor"] .noor-phoenix::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Cdefs%3E%3ClinearGradient id='f' x1='0' y1='0' x2='1' y2='1'%3E%3Cstop offset='0' stop-color='%23FFF3C4'/%3E%3Cstop offset='.45' stop-color='%23F0A93C'/%3E%3Cstop offset='1' stop-color='%23C0392B'/%3E%3C/linearGradient%3E%3C/defs%3E%3Cpath d='M32 20c3 0 6 2 7 5 3-4 9-7 15-6-4 2-6 5-7 9 4-1 8 0 11 3-5 0-9 2-12 6 3 1 5 3 6 6-4-3-9-4-14-3-1 4-3 7-6 9-3-2-5-5-6-9-5-1-10 0-14 3 1-3 3-5 6-6-3-4-7-6-12-6 3-3 7-4 11-3-1-4-3-7-7-9 6-1 12 2 15 6 1-3 4-5 7-5z' fill='url(%23f)'/%3E%3Cpath d='M32 26c2 0 4 2 4 5s-2 9-4 12c-2-3-4-9-4-12s2-5 4-5z' fill='%23FFF8DC' fill-opacity='.85'/%3E%3C/svg%3E") center/contain no-repeat;
  filter: drop-shadow(0 0 10px rgba(240, 169, 60, .75)) drop-shadow(0 0 26px rgba(192, 57, 43, .45));
  animation: noor-phoenix-beat .42s ease-in-out infinite alternate;
}

/* The ember trail: a tapering streak behind the bird, faded at both ends so
   it has no visible start. It is supposed to sit BEHIND the bird, and it
   never did: the rule said so in a comment and then declared no z-index at
   all, so the default paint order (::after over ::before) laid the trail
   across the phoenix's own body — a bar of fire through its chest. The bird
   carries z-index:3 and position:fixed, so it is its own stacking context and
   a negative z here goes behind the wings without falling behind the page. */
[data-theme="noor"] .noor-phoenix::after {
  content: "";
  position: absolute;
  z-index: -1;
  top: 50%; right: 82%;
  width: 170px; height: 3px;
  transform: translateY(-50%);
  border-radius: 999px;
  background: linear-gradient(90deg,
    transparent 0%,
    rgba(192, 57, 43, .28) 30%,
    rgba(240, 169, 60, .6) 78%,
    rgba(255, 243, 196, .85) 100%);
  filter: blur(1.5px);
}

/* A long shallow arc: up through the middle of the crossing, down at the
   end — a bird gliding, not a projectile. The bank angle is the physics: the
   nose is up while it climbs and down while it falls away, never level except
   at the top of the arc.
 *
 * The vertical legs are in vw, not vh. Mixed units make the drawn bank angle
 * a lie about the actual path the moment the window stops being 16:9 — the
 * same fault the meteor had, and the reason a phoenix on an ultrawide used to
 * look like it was gliding nose-up down a flat line. */
@keyframes noor-phoenix-cross {
  0%   { transform: translate3d(0, 0, 0) rotate(-4deg); opacity: 0; }
  8%   { opacity: .9; }
  35%  { transform: translate3d(38vw, -4vw, 0) rotate(-7deg); }
  65%  { transform: translate3d(72vw, -2.2vw, 0) rotate(2deg); }
  92%  { opacity: .85; }
  100% { transform: translate3d(calc(100vw + 200px), 3.4vw, 0) rotate(7deg); opacity: 0; }
}
@keyframes noor-phoenix-beat {
  from { transform: scaleY(1); }
  to   { transform: scaleY(.72); }
}

/* =========================================================================
   6 · THE SATELLITE — and the flare
   =========================================================================
   The counterpoint to a meteor, and the reason both read correctly: a
   satellite is a POINT, moving in a DEAD STRAIGHT LINE, at a CONSTANT SPEED,
   with NO TRAIL, for twenty seconds. Every one of those is wrong for a
   falling rock and right for a thing in orbit — it is not burning, it is
   only reflecting, and it is not falling, it is going round. Put one in the
   same sky as the meteors and each makes the other more believable.

   The flare is the rarer half. A flat antenna catches the sun square-on for
   a few seconds and the satellite briefly outshines every star, then dies
   back to a dim point — it does not blink, it swells and fades, because what
   is moving is the angle of a mirror. It runs on the satellite's OWN
   duration so the peak always lands mid-crossing.
   ========================================================================= */
[data-theme="noor"] .noor-sat {
  position: fixed;
  top: var(--s-y, 14vh);
  left: var(--s-x, -3vw);
  width: 2.4px;
  aspect-ratio: 1;
  z-index: 3;
  pointer-events: none;
  border-radius: 50%;
  background: #EAF6FF;
  box-shadow: 0 0 5px rgba(234, 246, 255, .85);
  opacity: 0;
  animation: noor-sat-track var(--s-dur, 18s) linear forwards;
}
@keyframes noor-sat-track {
  0%   { transform: translate3d(0, 0, 0); opacity: 0; }
  7%   { opacity: .8; }
  90%  { opacity: .75; }
  100% { transform: translate3d(var(--s-dx, 118vw), var(--s-dy, 6vw), 0); opacity: 0; }
}

[data-theme="noor"] .noor-sat.is-flare::before {
  content: "";
  position: absolute;
  inset: -1px;
  border-radius: 50%;
  background: #FFFFFF;
  box-shadow: 0 0 6px rgba(255, 255, 255, .9), 0 0 20px rgba(200, 235, 255, .6);
  opacity: 0;
  animation: noor-sat-flare var(--s-dur, 18s) var(--ease-inout, ease-in-out) forwards;
}
/* Asymmetric on purpose: a real flare brightens over a couple of seconds and
   takes twice as long to die, because the mirror rotates past. */
@keyframes noor-sat-flare {
  0%, 36% { opacity: 0;   transform: scale(1); }
  50%     { opacity: .85; transform: scale(3.6); }
  55%     { opacity: 1;   transform: scale(5); }
  74%     { opacity: .25; transform: scale(2.2); }
  88%,
  100%    { opacity: 0;   transform: scale(1); }
}

/* =========================================================================
   7 · THE COMET
   =========================================================================
   The slow one. A meteor is over in a second; a comet takes half a minute to
   cross, because it is not in our air — it is far away and enormous, and
   distance is what makes big things look slow.

   AND ITS TAIL DOES NOT TRAIL IT. This is the whole reason the comet is
   here. Every animated comet on the web drags its tail behind it like a
   scarf, and that is not what a tail is: it is dust and ion gas pushed off
   the nucleus by light and solar wind, so it points directly AWAY FROM THE
   LIGHT — regardless of which way the comet is travelling. A comet moving
   toward the sun wears its tail in FRONT of it.

   Noor's sky has exactly one light source, the moon in section 1, so the
   conductor measures the real angle from the moon to the comet and pins the
   tail along it. Watch one cross beneath the moon and the tail swings to
   stay pointed away. Nothing else in this file needed trigonometry.
   ========================================================================= */
[data-theme="noor"] .noor-comet {
  position: fixed;
  top: var(--c-y, 16vh);
  left: var(--c-x, -6vw);
  width: var(--c-size, 7px);
  aspect-ratio: 1;
  z-index: 3;
  pointer-events: none;
  opacity: 0;
  animation: noor-comet-drift var(--c-dur, 22s) linear forwards;
  will-change: transform, opacity;
}
/* The coma: the fuzzy head. Not a disc — a comet has no visible edge. */
[data-theme="noor"] .noor-comet::before {
  content: "";
  position: absolute;
  inset: -55%;
  border-radius: 50%;
  background: radial-gradient(circle,
    #FFF8DC 0%,
    rgba(255, 243, 196, .55) 26%,
    rgba(178, 232, 245, .22) 52%,
    transparent 74%);
}
/* The tail. transform-origin is the nucleus, so the rotation the conductor
   writes here swings the tail about the head rather than about its own
   middle. Wide at the far end, because dust spreads as it is pushed. */
[data-theme="noor"] .noor-comet::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  width: var(--c-tail, 190px);
  height: var(--c-fan, 26px);
  transform-origin: 0 50%;
  transform: translateY(-50%) rotate(var(--c-angle, 200deg));
  border-radius: 0 999px 999px 0 / 0 50% 50% 0;
  background: linear-gradient(90deg,
    rgba(255, 243, 196, .42) 0%,
    rgba(178, 232, 245, .16) 44%,
    rgba(120, 200, 225, .05) 74%,
    transparent 94%);
  filter: blur(3px);
}
@keyframes noor-comet-drift {
  0%   { transform: translate3d(0, 0, 0); opacity: 0; }
  12%  { opacity: .9; }
  84%  { opacity: .85; }
  100% { transform: translate3d(var(--c-dx, 116vw), var(--c-dy, 9vw), 0); opacity: 0; }
}

/* =========================================================================
   8 · WINGS
   =========================================================================
   Five fliers, and not one of them is the same animal twice. What separates
   them is not the silhouette — at this size nobody can see a silhouette — it
   is HOW THEY MOVE, which is the only thing the eye actually reads at 40px:

     gull       soars. No wingbeat at all for seconds at a time, and it BANKS
                into its turns, because a bird with no engine turns by
                rolling and letting gravity do the work.
     owl        beats slowly and deeply and travels in dead silence and a
                dead straight line. Big wings, low wing-loading, no hurry.
     bat        does not fly a path, it flies a SEARCH. Direction changes
                every few hundred milliseconds with no deceleration, because
                each change is a new echo arriving.
     moth       spirals INWARD. A moth holds a fixed angle to a light it
                evolved to read as the moon; near a close light that constant
                angle traces a logarithmic spiral, tightening until it
                arrives. It is a navigation bug, and it is why moths circle.
     dragonfly  hovers, then darts, then hovers. Dead stop to full speed and
                back with nothing in between — four independently driven
                wings and almost no mass to accelerate.
   ========================================================================= */
[data-theme="noor"] .noor-gull,
[data-theme="noor"] .noor-owl,
[data-theme="noor"] .noor-bat {
  position: fixed;
  top: var(--w-y, 26vh);
  left: var(--w-x, -12vw);
  z-index: 3;
  pointer-events: none;
  opacity: 0;
  will-change: transform, opacity;
}
[data-theme="noor"] .noor-gull::before,
[data-theme="noor"] .noor-owl::before,
[data-theme="noor"] .noor-bat::before {
  content: "";
  position: absolute;
  inset: 0;
  background: center / contain no-repeat;
}

/* -- the gull: soaring, banking, no beat ---------------------------------- */
[data-theme="noor"] .noor-gull {
  width: clamp(26px, 3.4vw, 46px);
  aspect-ratio: 64 / 20;
  animation: noor-gull-soar var(--w-dur, 13s) var(--ease-inout, ease-in-out) forwards;
}
[data-theme="noor"] .noor-gull::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 20'%3E%3Cpath d='M2 12 Q 17 1 32 10 Q 47 1 62 12 Q 47 7 32 15 Q 17 7 2 12 Z' fill='%23E8F4F8' fill-opacity='.6'/%3E%3C/svg%3E");
  /* One slow flex of the wingtips over the whole crossing — a soaring bird
     is not rigid, it trims. Nothing like a beat. */
  animation: noor-gull-trim 4.6s var(--ease-inout, ease-in-out) infinite alternate;
}
@keyframes noor-gull-trim { from { transform: scaleY(1); } to { transform: scaleY(.86); } }
/* The bank angle leads the turn, exactly as it does on a real wing: the bird
   rolls FIRST and the path curves as a result. */
@keyframes noor-gull-soar {
  0%   { transform: translate3d(0, 0, 0) rotate(6deg); opacity: 0; }
  9%   { opacity: .85; }
  28%  { transform: translate3d(28vw, -5vw, 0) rotate(-9deg); }
  52%  { transform: translate3d(56vw, -7.5vw, 0) rotate(4deg); }
  76%  { transform: translate3d(86vw, -3vw, 0) rotate(11deg); }
  91%  { opacity: .8; }
  100% { transform: translate3d(125vw, 2vw, 0) rotate(5deg); opacity: 0; }
}

/* -- the owl: slow, deep, silent, straight -------------------------------- */
[data-theme="noor"] .noor-owl {
  width: clamp(34px, 4.6vw, 62px);
  aspect-ratio: 64 / 28;
  animation: noor-owl-cross var(--w-dur, 10s) linear forwards;
}
[data-theme="noor"] .noor-owl::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 28'%3E%3Cpath d='M32 6 Q 42 3 52 8 Q 60 12 62 19 Q 50 14 40 16 L 38 22 Q 32 26 26 22 L 24 16 Q 14 14 2 19 Q 4 12 12 8 Q 22 3 32 6 Z' fill='%23D9CDAC' fill-opacity='.46'/%3E%3Ccircle cx='29' cy='11' r='1.5' fill='%23FFF3C4' fill-opacity='.75'/%3E%3Ccircle cx='35' cy='11' r='1.5' fill='%23FFF3C4' fill-opacity='.75'/%3E%3C/svg%3E");
  transform-origin: 50% 30%;                /* the shoulder, not the belly */
  animation: noor-owl-beat 1.05s var(--ease-inout, ease-in-out) infinite;
}
/* The downstroke is fast and the recovery is slow — that asymmetry is what
   separates a wingbeat from a pulse. */
@keyframes noor-owl-beat {
  0%   { transform: scaleY(1); }
  22%  { transform: scaleY(.42); }
  100% { transform: scaleY(1); }
}
@keyframes noor-owl-cross {
  0%   { transform: translate3d(0, 0, 0); opacity: 0; }
  10%  { opacity: .7; }
  88%  { opacity: .65; }
  100% { transform: translate3d(122vw, var(--w-dy, -3vw), 0); opacity: 0; }
}

/* -- the bat: a search pattern, not a flight path ------------------------- */
[data-theme="noor"] .noor-bat {
  width: clamp(16px, 2vw, 26px);
  aspect-ratio: 64 / 24;
  animation: noor-bat-hunt var(--w-dur, 3.4s) linear forwards;
}
[data-theme="noor"] .noor-bat::before {
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 24'%3E%3Cpath d='M32 7 L 40 3 L 45 10 L 54 4 L 59 13 L 50 12 L 44 17 L 38 14 L 32 20 L 26 14 L 20 17 L 14 12 L 5 13 L 10 4 L 19 10 L 24 3 Z' fill='%23071A22' fill-opacity='.82'/%3E%3C/svg%3E");
  animation: noor-bat-flutter .19s linear infinite;
}
@keyframes noor-bat-flutter { 0%, 100% { transform: scaleY(1); } 50% { transform: scaleY(.5); } }
/* No easing anywhere. A bat corrects on an echo, and an echo arrives all at
   once — the direction changes are supposed to be abrupt. */
@keyframes noor-bat-hunt {
  0%   { transform: translate3d(0, 0, 0); opacity: 0; }
  8%   { opacity: .78; }
  17%  { transform: translate3d(14vw, -6vh, 0); }
  29%  { transform: translate3d(26vw, 3vh, 0); }
  38%  { transform: translate3d(41vw, -4vh, 0); }
  51%  { transform: translate3d(52vw, 5vh, 0); }
  63%  { transform: translate3d(68vw, -2vh, 0); }
  74%  { transform: translate3d(79vw, 6vh, 0); }
  86%  { transform: translate3d(95vw, -3vh, 0); opacity: .7; }
  100% { transform: translate3d(114vw, 2vh, 0); opacity: 0; }
}

/* -- the moth: the logarithmic spiral ------------------------------------- */
[data-theme="noor"] .noor-moth {
  position: fixed;
  bottom: var(--w-b, 22vh);
  left: var(--w-x, 50vw);
  width: clamp(11px, 1.4vw, 17px);
  aspect-ratio: 32 / 24;
  z-index: 1;
  pointer-events: none;
  opacity: 0;
  animation: noor-moth-spiral var(--w-dur, 11s) var(--ease-inout, ease-in-out) forwards;
}
[data-theme="noor"] .noor-moth::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 24'%3E%3Cpath d='M16 8 L 3 3 Q 0 12 6 18 Q 12 20 16 15 Q 20 20 26 18 Q 32 12 29 3 Z' fill='%23E6D8B4' fill-opacity='.46'/%3E%3Cellipse cx='16' cy='13' rx='1.6' ry='5' fill='%23C9A24B' fill-opacity='.66'/%3E%3C/svg%3E") center / contain no-repeat;
  animation: noor-moth-wing .11s linear infinite;
}
@keyframes noor-moth-wing { 0%, 100% { transform: scaleX(1); } 50% { transform: scaleX(.55); } }
/* Each loop overshoots less than the last. The amplitude falls geometrically
   while the period shortens — that IS a logarithmic spiral, and it is why the
   moth ends up at the light instead of orbiting it forever. Then it breaks
   away, because a moth that arrives is a moth that has stopped being
   interesting. */
@keyframes noor-moth-spiral {
  0%   { transform: translate3d(0, 0, 0); opacity: 0; }
  10%  { opacity: .8; }
  22%  { transform: translate3d(-9vw, -7vh, 0); }
  38%  { transform: translate3d(5.4vw, -12vh, 0); }
  52%  { transform: translate3d(-4.4vw, -8.4vh, 0); }
  64%  { transform: translate3d(2.6vw, -12.6vh, 0); }
  74%  { transform: translate3d(-1.6vw, -10vh, 0); }
  82%  { transform: translate3d(1vw, -12.4vh, 0); }
  88%  { transform: translate3d(-.6vw, -11.2vh, 0); opacity: .8; }
  100% { transform: translate3d(11vw, -19vh, 0); opacity: 0; }
}

/* -- the dragonfly: hover, dart, hover ------------------------------------ */
[data-theme="noor"] .noor-dragonfly {
  position: fixed;
  bottom: var(--w-b, 12vh);
  left: var(--w-x, 30vw);
  width: clamp(16px, 2.2vw, 30px);
  aspect-ratio: 40 / 20;
  z-index: 1;
  pointer-events: none;
  opacity: 0;
  animation: noor-dfly-dart var(--w-dur, 9s) linear forwards;
}
[data-theme="noor"] .noor-dragonfly::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 20'%3E%3Cellipse cx='19' cy='10' rx='16' ry='1.4' fill='%2350C8DC' fill-opacity='.5'/%3E%3Cellipse cx='15' cy='6' rx='9' ry='2.1' fill='%23A8E8F4' fill-opacity='.26'/%3E%3Cellipse cx='15' cy='14' rx='9' ry='2.1' fill='%23A8E8F4' fill-opacity='.26'/%3E%3Ccircle cx='34' cy='10' r='2.3' fill='%2350C8DC' fill-opacity='.55'/%3E%3C/svg%3E") center / contain no-repeat;
  animation: noor-dfly-wing .07s linear infinite;
}
@keyframes noor-dfly-wing { 0%, 100% { transform: scaleY(1); } 50% { transform: scaleY(.66); } }
/* The holds are flat runs of the SAME transform — a true hover, not a slow
   drift — and the darts use a near-instant curve. That contrast is the whole
   animal; smooth it out and it becomes a fly. */
@keyframes noor-dfly-dart {
  0%   { transform: translate3d(0, 0, 0); opacity: 0; animation-timing-function: ease-out; }
  6%   { opacity: .8; }
  14%  { transform: translate3d(0, 0, 0); animation-timing-function: cubic-bezier(.85, 0, .15, 1); }
  22%  { transform: translate3d(17vw, -5vh, 0); animation-timing-function: linear; }
  38%  { transform: translate3d(17vw, -5vh, 0); animation-timing-function: cubic-bezier(.85, 0, .15, 1); }
  45%  { transform: translate3d(9vw, 4vh, 0); animation-timing-function: linear; }
  60%  { transform: translate3d(9vw, 4vh, 0); animation-timing-function: cubic-bezier(.85, 0, .15, 1); }
  68%  { transform: translate3d(31vw, -2vh, 0); animation-timing-function: linear; }
  84%  { transform: translate3d(31vw, -2vh, 0); opacity: .8; animation-timing-function: cubic-bezier(.85, 0, .15, 1); }
  100% { transform: translate3d(48vw, -14vh, 0); opacity: 0; }
}

/* =========================================================================
   9 · THE FANOUS
   =========================================================================
   A paper lantern let go at the shore. It rises because the air inside it is
   hot, and everything about how it moves follows from that one fact: it is
   SLOW, it is light enough that the smallest breath pushes it sideways, and
   it never travels in a straight line for more than a few seconds.

   And it SHRINKS. Not because it is deflating — because it is getting
   further away. Nothing else in this sky recedes, and one object that does
   is what gives the whole thing depth. It dims as it shrinks, at the same
   rate, the way a real light does through kilometres of night air.
   ========================================================================= */
[data-theme="noor"] .noor-fanous {
  position: fixed;
  top: 0;
  left: 0;
  width: clamp(13px, 1.7vw, 22px);
  aspect-ratio: 24 / 34;
  z-index: 3;
  pointer-events: none;
  will-change: transform, opacity;
}
[data-theme="noor"] .noor-fanous::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 34'%3E%3Cpath d='M9 2 h6 v4 h-6 z' fill='%23C9A24B' fill-opacity='.85'/%3E%3Cpath d='M5 7 h14 l-2 18 h-10 z' fill='%23F0C960' fill-opacity='.5'/%3E%3Cpath d='M6 25 h12 v3 h-12 z' fill='%23C9A24B' fill-opacity='.85'/%3E%3Cpath d='M9 10 h6 v12 h-6 z' fill='%23FFF3C4' fill-opacity='.8'/%3E%3C/svg%3E") center / contain no-repeat;
  filter: drop-shadow(0 0 7px rgba(240, 201, 96, .75));
  /* The flame gutters. A hot-air lantern's light is never steady, and the
     wobble of the envelope hangs off the same beat. */
  animation: noor-fanous-flame 2.3s var(--ease-inout, ease-in-out) infinite alternate;
}
@keyframes noor-fanous-flame {
  from { opacity: .78; transform: rotate(-2.4deg) scale(.98); }
  to   { opacity: 1;   transform: rotate(2.8deg) scale(1.02); }
}
/* The lantern's climb, its wander and its shrinking are all integrated in
   noor-physics.js — there are no keyframes here, because section 4b forbids
   an animation on a simulated body. Its scale is set from how far it has
   risen; its opacity is the engine's own fade, not a function of scale. */
/* =========================================================================
   10 · WEATHER
   =========================================================================
   Four events that are not objects. Nothing here has edges or a body; they
   are things the whole sky does at once, and they are the reason the sky
   reads as a place with air in it rather than a black rectangle with sprites
   on top.
   ========================================================================= */

/* -- the cloud: proof the stars are behind something ---------------------- */
/* It sits at z-index -1 with the stars, so it OCCLUDES them as it passes and
   that occlusion is the only thing that makes it a cloud — a dark blob on a
   dark sky is otherwise invisible. Its top-right edge is lit, because that
   is where the moon is; a cloud lit from the wrong side is the fastest way
   to break a night sky. */
[data-theme="noor"] .noor-cloud {
  position: fixed;
  top: var(--cl-y, 10vh);
  left: -46vw;
  width: var(--cl-w, 52vw);
  height: var(--cl-h, 15vh);
  z-index: -1;
  pointer-events: none;
  border-radius: 50%;
  opacity: 0;
  background:
    radial-gradient(58% 62% at 68% 34%, rgba(244, 231, 190, .10), transparent 62%),
    radial-gradient(62% 72% at 42% 52%, rgba(5, 24, 32, .78), rgba(5, 24, 32, .34) 56%, transparent 78%);
  animation: noor-cloud-drift var(--cl-dur, 52s) linear forwards;
}
@keyframes noor-cloud-drift {
  0%   { transform: translate3d(0, 0, 0) scaleX(1); opacity: 0; }
  14%  { opacity: 1; }
  50%  { transform: translate3d(70vw, 1.5vh, 0) scaleX(1.12); }
  86%  { opacity: 1; }
  100% { transform: translate3d(154vw, 3vh, 0) scaleX(1.2); opacity: 0; }
}

/* -- the airglow veil ----------------------------------------------------- */
/* Not an aurora — this is the Gulf, not the Arctic. Airglow: a faint sheet
   of light the upper atmosphere emits on its own. The rays are VERTICAL
   because they follow magnetic field lines, and the whole sheet shears
   slowly rather than rippling. Any horizontal banding here would be wrong. */
[data-theme="noor"] .noor-veil {
  position: fixed;
  top: -8vh;
  left: var(--v-x, 20vw);
  width: var(--v-w, 46vw);
  height: 46vh;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(180deg,
    rgba(27, 180, 212, .17) 0%,
    rgba(27, 180, 212, .08) 40%,
    rgba(40, 200, 190, .03) 66%,
    transparent 88%);
  animation: noor-veil-hang var(--v-dur, 26s) var(--ease-inout, ease-in-out) forwards;
}
[data-theme="noor"] .noor-veil::before {
  content: "";
  position: absolute;
  inset: 0;
  background: repeating-linear-gradient(90deg,
    rgba(150, 240, 255, .085) 0 7px,
    transparent 7px 27px);
  animation: noor-veil-ray 7.5s var(--ease-inout, ease-in-out) infinite alternate;
}
@keyframes noor-veil-hang {
  0%   { opacity: 0; transform: scaleY(.8); }
  24%  { opacity: 1; }
  72%  { opacity: .9; }
  100% { opacity: 0; transform: scaleY(1.08); }
}
@keyframes noor-veil-ray {
  from { transform: skewX(-8deg) translateX(-2.5%); }
  to   { transform: skewX(6deg) translateX(3%); }
}

/* -- heat lightning ------------------------------------------------------- */
/* A storm below the horizon. You never see the bolt and you never hear the
   thunder — only the cloud lighting from underneath, which is why this is a
   glow rising from the BOTTOM edge with no shape in it at all.
   The rhythm is the tell: real lightning is a multi-stroke discharge, three
   or four flashes in a fifth of a second, then nothing, then one more late
   one. Evenly spaced flashes read as a broken CSS animation. */
[data-theme="noor"] .noor-flash {
  position: fixed;
  left: var(--fl-x, 20vw);
  bottom: 0;
  width: var(--fl-w, 44vw);
  height: 34vh;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  background: radial-gradient(62% 100% at 50% 100%,
    rgba(196, 238, 252, .5) 0%,
    rgba(120, 200, 232, .17) 42%,
    transparent 74%);
  animation: noor-flash-sheet var(--fl-dur, 5s) linear forwards;
}
@keyframes noor-flash-sheet {
  0%, 3%   { opacity: 0; }
  4%       { opacity: .9; }
  6%       { opacity: .15; }
  8%       { opacity: .75; }
  10%      { opacity: .28; }
  12%      { opacity: .62; }
  16%      { opacity: 0; }
  46%      { opacity: 0; }
  48%      { opacity: .5; }
  52%      { opacity: .06; }
  100%     { opacity: 0; }
}

/* -- the gust ------------------------------------------------------------- */
/* The only event in this file that touches the others. A faint shear crosses
   the page, and everything light enough to be moved — fireflies, smoke,
   sparks, bubbles — leans downwind for a couple of seconds and drifts back.
   See the `translate` note in section 4 for why that lean can coexist with
   each element's own running animation.
   Wind is a thing that happens TO a sky, not a thing in it. */
[data-theme="noor"] .noor-gust {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(101deg,
    transparent 40%,
    rgba(204, 242, 252, .055) 50%,
    transparent 60%);
  animation: noor-gust-pass var(--g-dur, 3.4s) var(--ease-inout, ease-in-out) forwards;
}
@keyframes noor-gust-pass {
  0%   { opacity: 0; transform: translate3d(calc(var(--g-dir, 1) * -130vw), 0, 0); }
  20%  { opacity: 1; }
  78%  { opacity: 1; }
  100% { opacity: 0; transform: translate3d(calc(var(--g-dir, 1) * 130vw), 0, 0); }
}

/* =========================================================================
   11 · FIRE
   =========================================================================
   Two things rising off a shore you cannot see: sparks from someone's fire,
   and the smoke of bakhoor burning somewhere along it.
   ========================================================================= */

/* -- the spark: a cooling ember ------------------------------------------- */
/* SIMULATED — see the placement note in section 15. It leaves the fire
   white-hot and it dies red, and that colour change is real physics: a black
   body radiates bluer the hotter it is. It is still done WITHOUT animating a
   colour — two stacked layers, a white-hot core over a red glow — but the
   crossfade is no longer two keyframe curves I drew to agree. `--sp-heat` is
   the integrator's own heat variable, written every step, so the colour and
   the flight path are two readings of ONE number. When the ember cools past
   the point where buoyancy stops beating gravity it starts to fall, and it
   goes red at the same instant, because that instant is the same instant. */
[data-theme="noor"] .noor-spark {
  position: fixed;
  top: 0;
  left: 0;
  width: var(--sp-size, 3px);
  aspect-ratio: 1;
  z-index: 1;
  pointer-events: none;
  border-radius: 50%;
}
/* the red glow — always there, revealed as the core cools off it */
[data-theme="noor"] .noor-spark::after {
  content: "";
  position: absolute;
  inset: -40%;
  border-radius: 50%;
  background: radial-gradient(circle, #E8622B 0%, rgba(192, 57, 43, .55) 45%, transparent 72%);
  opacity: calc(.95 - var(--sp-heat, 1) * .6);
}
/* the white-hot core */
[data-theme="noor"] .noor-spark::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: #FFFDF2;
  box-shadow: 0 0 6px rgba(255, 243, 196, .95), 0 0 14px rgba(240, 169, 60, .6);
  opacity: var(--sp-heat, 1);
}

/* -- bakhoor: laminar, then turbulent ------------------------------------- */
/* A smoke column leaves the coal as a straight, still ribbon and holds that
   shape for a surprisingly long way up — then crosses a threshold and breaks
   into turbulence all at once, widening and writhing. That TRANSITION is the
   most recognisable thing about rising smoke and almost nobody animates it.
   Here the first 35% of the climb is dead straight and narrow; everything
   after it skews, widens and fades. */
[data-theme="noor"] .noor-smoke {
  position: fixed;
  bottom: var(--sm-b, 0);
  left: var(--sm-x, 50vw);
  width: var(--sm-w, 9px);
  height: 42vh;
  z-index: 1;
  pointer-events: none;
  transform-origin: 50% 100%;
  opacity: 0;
  background: linear-gradient(180deg,
    transparent 0%,
    rgba(214, 226, 230, .05) 22%,
    rgba(206, 220, 226, .13) 62%,
    rgba(198, 214, 222, .2) 100%);
  filter: blur(3px);
  animation: noor-smoke-rise var(--sm-dur, 16s) linear forwards;
}
@keyframes noor-smoke-rise {
  0%   { opacity: 0; transform: translate3d(0, 24vh, 0) scale(.5, .3) skewX(0deg); }
  12%  { opacity: .85; }
  /* laminar: still narrow, still straight */
  35%  { transform: translate3d(0, 8vh, 0) scale(.75, .72) skewX(1deg); opacity: .9; }
  /* the break */
  58%  { transform: translate3d(2.2vw, 0, 0) scale(1.5, .92) skewX(-11deg); opacity: .6; }
  80%  { transform: translate3d(-1.4vw, -6vh, 0) scale(2.4, 1.05) skewX(14deg); opacity: .3; }
  100% { transform: translate3d(3.4vw, -14vh, 0) scale(3.6, 1.15) skewX(-8deg); opacity: 0; }
}

/* =========================================================================
   12 · THE WATER
   =========================================================================
   Everything below lives at the bottom edge, because that is where Noor's
   lagoon is — the same waterline the fireflies rise off. Four events, and
   each one is a different piece of surface physics.
   ========================================================================= */

/* -- the ripple ----------------------------------------------------------- */
/* A ring spreading from a disturbance. It DECELERATES, because a spreading
   gravity wave hands its energy to an ever-longer circumference, and it dims
   as it grows for the same reason — the energy per unit of ring falls as the
   ring gets bigger. A ring that expands at constant speed and constant
   brightness is a radar sweep, not water. */
[data-theme="noor"] .noor-ripple {
  position: fixed;
  bottom: var(--r-b, 5vh);
  left: var(--r-x, 50vw);
  width: var(--r-size, 90px);
  aspect-ratio: 4 / 1;                       /* seen at a glancing angle */
  z-index: 1;
  pointer-events: none;
  border-radius: 50%;
  border: 1px solid rgba(232, 240, 214, .75);
  box-shadow: 0 0 8px rgba(200, 232, 240, .3);
  opacity: 0;
  animation: noor-ripple-out var(--r-dur, 3.4s) cubic-bezier(.12, .72, .36, 1) forwards;
}
@keyframes noor-ripple-out {
  0%   { transform: translate(-50%, 0) scale(.12); opacity: 0; }
  12%  { opacity: .8; }
  100% { transform: translate(-50%, 0) scale(1); opacity: 0; }
}

/* -- the fish: a parabola, and a nose that follows it --------------------- */
/* SIMULATED. Once it leaves the water nothing pushes it but gravity, so the
   arc is a parabola — not a sine, which is what almost every jumping-fish
   animation uses and why they look like they are on a wire.
   This used to be ten keyframes sampled from 4h.t.(1-t), which was the right
   curve and still a recording: the landing point was decided the moment the
   element was created, so a gust could lean the fish but never actually
   change where it came down. Now it is thrown once and gravity does the rest,
   the splash is spawned at the position it really reached, and the nose
   angle is read from the live velocity vector rather than approximated at
   eight sampled instants. */
[data-theme="noor"] .noor-fish {
  position: fixed;
  top: 0;
  left: 0;
  width: var(--fi-w, 30px);
  aspect-ratio: 48 / 16;
  z-index: 1;
  pointer-events: none;
}
[data-theme="noor"] .noor-fish::before {
  content: "";
  position: absolute;
  inset: 0;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 16'%3E%3Cpath d='M14 8 Q 25 1 39 8 Q 25 15 14 8 Z' fill='%23DCEEF4' fill-opacity='.8'/%3E%3Cpath d='M14 8 L 4 3 L 7 8 L 4 13 Z' fill='%23BEDDE8' fill-opacity='.6'/%3E%3Ccircle cx='33' cy='7' r='.9' fill='%230A2028' fill-opacity='.8'/%3E%3C/svg%3E") center / contain no-repeat;
}
/* A fish going left is drawn facing left. The flip lives on the SPRITE and
   not on the element, because the element's transform is fully spoken for by
   the arc — putting a scaleX there would have to be repeated in all ten
   keyframes and would silently mirror the rotation with it. */
[data-theme="noor"] .noor-fish.is-left::before { transform: scaleX(-1); }
/* -- the pearl diver's bubbles -------------------------------------------- */
/* SIMULATED, and it is the clearest case for the engine in the whole file.
   A bubble ACCELERATES as it rises and GROWS while it does it, and those are
   not two behaviours — they are one: the water pressure above it falls, so it
   expands, so it displaces more, so it is pushed harder.
   As a keyframe that was an ease-in curve sitting next to a scale curve, two
   drawings I had made agree with each other. In the integrator `grow` is the
   only input and the buoyancy is recomputed from the volume every step, so
   the acceleration is a consequence rather than a coincidence. Then it
   reaches the surface and POPS — a hard expansion, gone in two frames. */
[data-theme="noor"] .noor-bubble {
  position: fixed;
  top: 0;
  left: 0;
  width: var(--bu-size, 5px);
  aspect-ratio: 1;
  z-index: 1;
  pointer-events: none;
  border-radius: 50%;
  border: 1px solid rgba(214, 240, 248, .55);
  background: radial-gradient(circle at 32% 28%, rgba(255, 255, 255, .45), rgba(180, 225, 240, .12) 55%, transparent 72%);
}
/* -- the glitter path ----------------------------------------------------- */
/* The moon's reflection on moving water. Two details make it read:
   it is directly BELOW the moon and nowhere else, because a glitter path
   always points at the observer; and it is a wedge that WIDENS toward the
   bottom of the screen, because the near water tilts through a wider range
   of angles than the far water does. It is not a beam of light, it is a
   thousand separate reflections — hence the mask, which cuts it into
   horizontal glints on the wavelets rather than leaving a solid column. */
[data-theme="noor"] .noor-glitter {
  position: fixed;
  bottom: 0;
  left: var(--gl-x, 50vw);
  width: var(--gl-w, 200px);
  height: 24vh;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  background: linear-gradient(0deg,
    rgba(244, 231, 190, .26) 0%,
    rgba(244, 231, 190, .11) 44%,
    transparent 84%);
  clip-path: polygon(36% 0, 64% 0, 100% 100%, 0 100%);
  -webkit-mask-image: repeating-linear-gradient(0deg, #000 0 2px, transparent 2px 8px);
          mask-image: repeating-linear-gradient(0deg, #000 0 2px, transparent 2px 8px);
  animation: noor-glitter-lie var(--gl-dur, 20s) var(--ease-inout, ease-in-out) forwards;
}
/* The glints do not travel sideways — the water moves through them, so the
   path shimmers in place. A glitter path that slides is a searchlight. */
[data-theme="noor"] .noor-glitter::before {
  content: "";
  position: absolute;
  inset: 0;
  background: inherit;
  animation: noor-glitter-shimmer 3.6s var(--ease-inout, ease-in-out) infinite alternate;
}
@keyframes noor-glitter-lie {
  0%   { opacity: 0; }
  18%  { opacity: 1; }
  76%  { opacity: .9; }
  100% { opacity: 0; }
}
@keyframes noor-glitter-shimmer {
  from { transform: translateY(0) scaleX(.94); opacity: .5; }
  to   { transform: translateY(-4px) scaleX(1.06); opacity: 1; }
}

/* =========================================================================
   13 · THE STARS ANSWER
   =========================================================================
   Three events with no element of their own. They reach into the stars that
   section 2 already hung and change what those stars are doing — which means
   they cost nothing, and, more importantly, they make the fixed points feel
   like objects rather than decoration. The sky's furniture is allowed to
   participate.
   ========================================================================= */

/* -- the flare star ------------------------------------------------------- */
/* One star brightens by several magnitudes over a second or two and settles
   back. Real, common, and it happens to red dwarfs. It rides a ::after so
   the star's own twinkle keeps running underneath, and the two together are
   what a flare actually looks like: a normal star, briefly enormous. */
[data-theme="noor"] .noor-star.is-nova::after {
  content: "";
  position: absolute;
  inset: -2px;
  border-radius: 50%;
  background: radial-gradient(circle, #FFFFFF 0%, rgba(255, 246, 222, .8) 30%, transparent 70%);
  box-shadow: 0 0 14px rgba(255, 246, 222, .9), 0 0 34px rgba(180, 225, 245, .5);
  opacity: 0;
  animation: noor-star-nova 2.6s var(--ease-out, ease-out) forwards;
}
/* Fast rise, slow decay — the shape of every flare in nature. */
@keyframes noor-star-nova {
  0%   { opacity: 0; transform: scale(1); }
  14%  { opacity: 1; transform: scale(5.5); }
  38%  { opacity: .6; transform: scale(3.6); }
  100% { opacity: 0; transform: scale(1.2); }
}

/* -- the occultation ------------------------------------------------------ */
/* A star goes out, stays out for a second, and comes back. Something passed
   in front of it — a satellite, an asteroid, the limb of something you
   cannot see. The edges are SHARP, because the occulting body has a hard
   edge; fade it and it just reads as a slow twinkle. */
/* The twinkle is restated so it keeps running underneath. No `forwards` on
   the occultation: a filled final keyframe would pin opacity at 1 and leave
   the star permanently un-twinkling, since the later animation in the list
   wins any property both of them touch. */
[data-theme="noor"] .noor-star.is-occult {
  animation: noor-star-twinkle var(--star-dur, 6s) ease-in-out var(--star-delay, 0s) infinite,
             noor-star-occult 2.4s linear;
}
@keyframes noor-star-occult {
  0%, 14%   { opacity: 1; }
  18%, 62%  { opacity: 0; }
  66%, 100% { opacity: 1; }
}

/* -- the constellation resolves ------------------------------------------- */
/* Hairlines between four or five stars that are genuinely near each other on
   THIS load — the conductor walks nearest-neighbour through the real
   positions, so the figure is different every visit and tends not to cross
   itself. Nearest-neighbour is a strong bias against a self-crossing path,
   not a proof: on an unlucky scatter the chain can still double back, and an
   earlier version of this comment claimed a guarantee it does not have.
   It fades in rather than being drawn stroke by stroke, and that is a
   deliberate refusal: a drawn line means animating stroke-dashoffset, which
   is geometry the compositor cannot touch. Opacity is free, and "a shape
   resolving out of noise" is the better metaphor anyway — which is what
   finding a constellation actually feels like. */
[data-theme="noor"] .noor-lines {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  animation: noor-lines-resolve var(--ln-dur, 7s) var(--ease-inout, ease-in-out) forwards;
}
[data-theme="noor"] .noor-lines polyline {
  fill: none;
  stroke: #E8CF8E;
  stroke-opacity: .5;
  stroke-width: 1;
  stroke-linecap: round;
  stroke-linejoin: round;
}
@keyframes noor-lines-resolve {
  0%   { opacity: 0; }
  22%  { opacity: 1; }
  74%  { opacity: 1; }
  100% { opacity: 0; }
}

/* =========================================================================
   14 · STILLNESS, ON REQUEST
   =========================================================================
   Reduced motion removes the sky entirely rather than freezing it. A frozen
   meteor is a gold smear across the page with no explanation; a stopped
   firefly is a dead one. The conductor also refuses to start, so this is a
   second lock on a door already bolted — deliberately, because the cost of
   being wrong here is a page a motion-sensitive customer cannot use.

   ONE SELECTOR, NOT A LIST. This used to name each class, which meant every
   new effect had to remember to add itself to two lists at the bottom of the
   file or it would keep moving for someone who asked it not to — an
   accessibility failure that fails SILENTLY and only for the people it hurts.
   `[data-noor-sky]` is stamped by the conductor on everything it creates, so
   the door cannot be left open by forgetting.

   It is a different attribute from [data-noor-fx], which is theme.js's kill
   switch for the whole night and also carries the vignette — and the
   vignette is composition rather than motion, so it must SURVIVE this rule.
   ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  [data-theme="noor"] [data-noor-sky] { display: none; }
}

/* Print: none of this is ink. */
@media print {
  [data-theme="noor"] [data-noor-sky] { display: none; }
}
