/* =========================================================================
   ember.css — THE EMBER ATMOSPHERE.

   The third scene. Chosen per owner with one manifest key:

       "theme": { "atmosphere": "ember" }

   `artwork` (the default, and what saying nothing means) is backdrop.css +
   backdrop.js — the owner's own painting as a living scene. `space` is
   space.css + space.js — a greyscale deep-space field. `ember` is this sheet
   + js/ember.js — a warm night with embers rising through it. Exactly one of
   the three ships to any owner, in the same seat in the link list, so nothing
   else on the site can tell which one it got.

   FIRST OWNER: Everfall, 2026-08-29. The scene exists because `space` cannot
   reach it: space.js DESATURATES every mark before it paints, by contract, so
   it can only ever draw a grey field of dust and stars. This one draws a
   DENSER, LIVELIER field — out-of-focus bokeh, drifting motes and bright
   sparks rising past falling ash — and it keeps whatever hue the owner's
   `--accent` carries. That contract is right for space and is left alone.
   This is a second scene, not a flag on the first.

   ★ THE NAME IS THE FIRST TUNING, NOT THE ONLY ONE. It was written against
   an ember palette (he had named https://islapandora.eu/ as the bar: "particles
   should be animated fully. fully animation."), and then he ruled "I need them
   to keep their current colors" and the owner went back to monochrome. THE
   SCENE DID NOT CHANGE - not one line - because every mark is `--accent` and
   Everfall's accent is a pale grey. On a warm palette it is embers; on a cool
   one it is dust and ash in a shaft of light. Judge a change to this file by
   whether it survives BOTH.

   ---------------------------------------------------------------------------
   THE PALETTE RULE, AND IT IS THE SAME STRICT ONE
   ---------------------------------------------------------------------------
   This sheet holds NO COLOUR OF ITS OWN. Every value is a role token from
   theme.css or a color-mix() of one, so an owner's palette re-tints the whole
   field without an edit here and one island's ground can never reach another.
   run_all.py's TOKEN_SHEETS scan refuses this file if a raw hex or an
   rgb()/hsl()/oklch() ever appears in it, in source AND in every assembled
   build.

   WHERE THE LIGHT COMES FROM. The ground is the owner's `--bg` / `--bg-deep`,
   as it is on every atmosphere. Every mark is the owner's `--accent`, and that
   is the one line that separates this scene from its sibling: space grinds the
   ink to grey on purpose, this one keeps whatever the owner has on purpose. An
   owner on a green palette gets a green field and nothing here changes - which
   is exactly what was proven when this owner's palette was reverted from ember
   to monochrome and not a byte of this sheet or its engine moved.

   ---------------------------------------------------------------------------
   TWO FLOORS, AND FLOOR ONE IS A FINISHED PAGE
   ---------------------------------------------------------------------------
   FLOOR 1 — this sheet alone, no JavaScript. `.night` becomes a warm night:
   a low horizon glow, two banked heat lobes, a closing vignette. That is a
   correct, complete backdrop. If ember.js 404s, throws on an old engine, or
   is switched off, this is what the visitor gets and nothing looks broken.

   FLOOR 2 — ember.js sets `data-ember="on"` on `.night` once it has a real
   element, a real canvas and a real context, and only then do the moving
   layers exist. There is no flash of an unbuilt scene: every rule below
   starts at the value it holds at rest.

   ---------------------------------------------------------------------------
   WHY THE HEAT IS CSS AND THE EMBERS ARE CANVAS
   ---------------------------------------------------------------------------
   Same split space.css makes, for the same reason. A soft heat lobe is two
   or three screen-sized radial gradients whose whole character is that they
   barely move; as CSS that is one composited layer the compositor drifts for
   free, and on canvas it would be three full-viewport composites per frame.
   The embers are the opposite — several hundred small marks, each with its
   own drift, flicker phase and depth — so they are one canvas rather than
   several hundred style recalculations.

   ---------------------------------------------------------------------------
   THE STACK, in paint order (DOM order, no z-index anywhere inside)
   ---------------------------------------------------------------------------
     .night             the bed — horizon glow, heat lobes, vignette. FLOOR 1.
     .wc-ember          the scene wrapper, clipped, inert.
       .wc-ember-heat     two drifting warm lobes (CSS, compositor).
       .wc-ember-canvas   the ember field (ember.js).
       .wc-ember-veil     the reading scrim, shaped across the column.
       .wc-ember-edge     the closing vignette over everything.
   ========================================================================= */

/* -------------------------------------------------------------------------
   THE TOKENS THIS SHEET DERIVES.

   Named here rather than repeated inline for the reason theme.py exists: a
   value used in five rules is a value that drifts in four of them.
   ------------------------------------------------------------------------- */
:root {
  /* the floor everything closes on */
  --em-floor: var(--bg-deep);
  /* the bed, a touch above the floor and warmed by the owner's own ground */
  --em-bed: color-mix(in srgb, var(--bg) 78%, var(--bg-deep));
  /* THE HEAT. Three strengths of the owner's accent, brightest first. Every
     one of them is under a tenth: a heat lobe you can name has become a
     graphic, and the embers have to be the brightest thing in the scene. */
  --em-heat-hi: color-mix(in srgb, var(--accent) 14%, transparent);
  --em-heat: color-mix(in srgb, var(--accent) 9.5%, transparent);
  --em-heat-lo: color-mix(in srgb, var(--accent) 5%, transparent);
  /* the low horizon the embers rise off, warmest thing on the ground */
  --em-horizon: color-mix(in srgb, var(--accent) 19%, transparent);
  /* THE SCRIPTLESS MOTES (FLOOR 1b). Three strengths of the owner's own
     accent, and they are deliberately STRONGER than the canvas alphas: this
     layer has to carry the whole backdrop on a machine where the canvas never
     runs, so it is tuned to be SEEN rather than to sit under something. */
  --em-mote-hi: color-mix(in srgb, var(--accent-hi) 92%, transparent);
  --em-mote: color-mix(in srgb, var(--accent) 72%, transparent);
  --em-mote-lo: color-mix(in srgb, var(--accent) 46%, transparent);
  /* the soft blooms in the near field: the scriptless answer to the canvas's
     bokeh plane, and the mark that makes this layer read as depth rather than
     as a dot screen. */
  --em-mote-bloom: color-mix(in srgb, var(--accent) 13%, transparent);
  /* ★ HOW MUCH OF THE SCRIPTLESS FIELD IS ACTUALLY SHOWN. 1 when it is the
     ONLY field there is; stepped down below when the canvas is also running,
     because the two stack and full strength on both is a screen so busy the
     wordmark has to fight it. Redefining ONE number is how the same sheet
     serves both machines. */
  --em-mote-on: 1;
  /* the reading scrim: dense across the column, open at the margins */
  --em-veil-core: color-mix(in srgb, var(--bg-deep) 8%, transparent);
  --em-veil-soft: color-mix(in srgb, var(--bg-deep) 3%, transparent);
  /* the closing vignette */
  --em-edge: color-mix(in srgb, var(--bg-deep) 32%, transparent);
  /* How far the whole page settles onto the floor below the first screen.
     Deliberately NOT opaque — see THE GROUND, further down.

     ★ 68% -> 40% (2026-08-29). An audit of the whole stack put the light loss
     over the canvas at 17% on the first screen and SEVENTY-THREE PERCENT one
     screen down, and this token was ~8x every other dimmer combined down
     there. The field was not dim below the fold, it was very nearly gone -
     which is half of "theres no animation", because the half of the site a
     reader actually scrolls through had almost no field left in it. The
     reading is not at risk from the change: every panel on this theme carries
     a fully OPAQUE face (`--ev-face` and its two siblings are all opaque
     mixes), so body copy never sits on the field at all. */
  --em-page-floor: color-mix(in srgb, var(--bg-deep) 40%, transparent);
}

/* =========================================================================
   FLOOR 1 — THE BED.

   site.css paints `.night` with the owner's ARTWORK and four veils over it.
   This sheet loads after site.css and replaces that outright: a scene whose
   subject is an ember-lit night has no painting in it, and leaving the art
   layer underneath would be a photograph showing through the fire.

   `background`, never `background-image`. The longhand takes IMAGES ONLY, so
   one position or size token anywhere in the list invalidates the whole
   declaration and the browser drops it in silence. That exact word cost this
   core its entire backdrop on every owner for months; the note is in
   site.css, repeated in space.css, and repeated here because this is the
   next place somebody will type it.

   THE HORIZON IS AT THE BOTTOM, and that is the whole reason this bed reads
   as fire rather than as sky. Heat pools low and rises; the lift on the
   space bed is at `50% 16%` because a galaxy's light is overhead. Putting
   this one at the top would give a sunrise, which is a different picture.
   ========================================================================= */
.night {
  background:
    radial-gradient(88% 42% at 50% 104%, var(--em-horizon) 0%, transparent 66%),
    radial-gradient(46% 34% at 18% 68%, var(--em-heat-lo) 0%, transparent 70%),
    radial-gradient(44% 32% at 84% 58%, var(--em-heat-lo) 0%, transparent 68%),
    radial-gradient(150% 124% at 50% 46%, var(--em-bed) 0%, var(--em-floor) 78%);
  background-color: var(--em-floor);
}

/* =========================================================================
   ★★★★★ FLOOR 1b - THE FIELD THAT NEEDS NO JAVASCRIPT.

   THE DEFECT THIS EXISTS TO FIX. The owner said, three times, that there were
   no background animations, while every headless measurement of the same page
   showed the canvas running at 60 fps. Both were true, and the reason is
   architectural rather than a bug: EVERY moving thing in this scene depended
   on `js/ember.js` - a canvas, a 2d context and a requestAnimationFrame loop.
   Any one of a dozen ordinary things on a real machine takes all three away
   at once and leaves a still photograph:

     * `prefers-reduced-motion: reduce`, which freezes positions by design;
     * a privacy extension that makes `getContext('2d')` return null, which is
       a common class of extension and which the engine correctly gives up on;
     * the script 404ing, being blocked, or throwing on an older engine;
     * the loop parking after a scroll;
     * a backgrounded tab that never fires `visibilitychange` on the way back.

   The site the owner compared it to does not have this problem, and the
   reason is worth stating plainly: it animates DOM ELEMENTS WITH CSS
   KEYFRAMES. Its JavaScript only creates the spans; once they exist the
   compositor animates them, with no loop to stop and nothing to block.

   So this scene gets the same floor. TWO DRIFTING DOT FIELDS THAT BREATHE,
   painted as background images on `.night`'s own two pseudo-elements and
   animated entirely by CSS. No script creates them, no script drives them and
   no script can stop them. If `ember.js` never runs at all, the backdrop still
   moves.

   HOW THE LOOP IS SEAMLESS. Each field is a `radial-gradient` tiled by
   `background-size`, and its keyframe translates by EXACTLY one tile in Y, so
   the end state is pixel-identical to the start and an infinite linear
   animation never shows a seam. `inset: -20%` keeps the translated edges off
   screen at every window shape.

   WHAT IT COSTS. Two composited layers animating `transform` and `opacity`
   only - the two properties that never touch layout or paint - over one
   rasterised texture each, reused for the life of the page.
   ========================================================================= */

/* THE NEAR FIELD: bigger, brighter, faster. Four gradients per tile so the
   pattern does not read as a grid, and a tile that is taller than it is wide
   for the same reason. */
.night::before {
  content: "";
  position: absolute;
  inset: -20%;
  pointer-events: none;
  background-image:
    radial-gradient(2.6px 2.6px at 18% 24%, var(--em-mote-hi) 44%, transparent 46%),
    radial-gradient(3.4px 3.4px at 62% 11%, var(--em-mote) 44%, transparent 46%),
    radial-gradient(2.2px 2.2px at 39% 68%, var(--em-mote) 44%, transparent 46%),
    radial-gradient(3.0px 3.0px at 84% 82%, var(--em-mote-hi) 44%, transparent 46%),
    radial-gradient(2.4px 2.4px at 7% 57%, var(--em-mote) 44%, transparent 46%),
    radial-gradient(2.0px 2.0px at 52% 41%, var(--em-mote-hi) 44%, transparent 46%),
    radial-gradient(2.8px 2.8px at 74% 33%, var(--em-mote) 44%, transparent 46%),
    radial-gradient(2.2px 2.2px at 29% 92%, var(--em-mote-hi) 44%, transparent 46%),
    radial-gradient(26px 26px at 46% 78%, var(--em-mote-bloom) 0%, transparent 70%),
    radial-gradient(34px 34px at 88% 18%, var(--em-mote-bloom) 0%, transparent 70%);
  background-size: 260px 340px;
  animation: wc-em-rise 34s linear infinite,
             wc-em-breathe 7s ease-in-out infinite;
  opacity: var(--em-mote-on);
  will-change: transform, opacity;
}

/* THE FAR FIELD: smaller, dimmer, slower, on a different tile size so the two
   never beat against each other. `.night::after` is where site.css hangs its
   own 128s accent drift; that is REPLACED rather than merely switched off,
   because this scene wants one coordinated ground rather than two. */
.night::after {
  content: "";
  position: absolute;
  inset: -20%;
  pointer-events: none;
  background-image:
    radial-gradient(1.8px 1.8px at 8% 44%, var(--em-mote-lo) 44%, transparent 46%),
    radial-gradient(2.1px 2.1px at 47% 27%, var(--em-mote) 44%, transparent 46%),
    radial-gradient(1.6px 1.6px at 71% 61%, var(--em-mote-lo) 44%, transparent 46%),
    radial-gradient(1.9px 1.9px at 92% 15%, var(--em-mote-lo) 44%, transparent 46%),
    radial-gradient(1.7px 1.7px at 26% 72%, var(--em-mote-lo) 44%, transparent 46%),
    radial-gradient(2.0px 2.0px at 63% 88%, var(--em-mote) 44%, transparent 46%);
  background-size: 190px 250px;
  animation: wc-em-rise-far 61s linear infinite,
             wc-em-breathe 11s ease-in-out infinite;
  opacity: var(--em-mote-on);
  will-change: transform, opacity;
}

/* ★ A THIRD MOTE LAYER, AND WHY A THIRD ONE MATTERS MORE THAN MORE DOTS.
   Each of these layers is ONE tiled background, so its breathe dims every dot
   in it AT THE SAME INSTANT. The reference twinkles each of its 150 stars
   independently, with a random 2.5-7.5 s period and a negative random delay -
   roughly thirty troughs a second, somewhere, always. One synchronised dimmer
   reads as a page being turned up and down; many independent ones read as
   alive. That is a real perceptual difference and no amount of extra dots in
   a synchronised layer closes it.

   Three layers is the most this scene can have without adding an element (a
   ground has exactly two pseudo-elements, and `.wc-ember-heat` is a real one
   already present for the heat lobes). Three periods - 7 s, 11 s, 17 s, chosen
   pairwise coprime so the pattern does not repeat for 1,309 s - is enough that
   at any moment one layer is bright, one is mid and one is dim, and the ground
   never flattens.

   ★ IT KEEPS ITS HEAT LOBES. This rule adds motes to the SAME element, as
   extra background layers listed BEFORE the lobes, so both draw. */
.wc-ember-heat {
  background-image:
    radial-gradient(2.0px 2.0px at 33% 12%, var(--em-mote) 44%, transparent 46%),
    radial-gradient(2.6px 2.6px at 77% 49%, var(--em-mote-hi) 44%, transparent 46%),
    radial-gradient(1.8px 1.8px at 12% 81%, var(--em-mote) 44%, transparent 46%),
    radial-gradient(2.2px 2.2px at 58% 66%, var(--em-mote-hi) 44%, transparent 46%),
    radial-gradient(38% 46% at 28% 86%, var(--em-heat-hi) 0%, transparent 72%),
    radial-gradient(34% 42% at 74% 92%, var(--em-heat) 0%, transparent 70%),
    radial-gradient(52% 30% at 50% 34%, var(--em-heat-lo) 0%, transparent 76%);
  background-size: 310px 410px, 310px 410px, 310px 410px, 310px 410px,
                   auto, auto, auto;
  background-repeat: repeat, repeat, repeat, repeat,
                     no-repeat, no-repeat, no-repeat;
}

/* Its own drift and its own breathe, on the third period. The heat lobes ride
   along with the motes, which is why this animation REPLACES `wc-em-draw`
   rather than joining it. */
.night[data-ember] .wc-ember-heat {
  animation: wc-em-rise-mid 47s linear infinite,
             wc-em-breathe 17s ease-in-out infinite;
}

@keyframes wc-em-rise-mid {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(0, -410px, 0); }
}

/* EXACTLY ONE TILE, so the loop is invisible. 340px and 250px are the two
   `background-size` heights above; changing one without the other is what puts
   a visible jump in the field once a minute. */
@keyframes wc-em-rise {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(0, -340px, 0); }
}

@keyframes wc-em-rise-far {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(0, -250px, 0); }
}

/* The collective twinkle. Per-dot flicker is what the canvas is for; this is
   the whole field breathing, which is what has to carry when the canvas is
   absent. The two layers run it at 7 s and 11 s so their troughs drift in and
   out of phase and the ground never pulses as one flat sheet. */
@keyframes wc-em-breathe {
  0%, 100% { opacity: var(--em-mote-on); }
  50%      { opacity: calc(var(--em-mote-on) * 0.45); }
}

/* ★ THE STEP-DOWN, AND WHAT IT IS KEYED ON. `data-ember` is written by
   js/ember.js AFTER it has a real element, a real canvas AND a real 2d
   context - so its presence is proof the canvas field is actually painting.
   Absent it (script blocked, 404, thrown, an extension refusing getContext,
   an engine too old) the motes above stay at full strength and carry the
   backdrop alone. Present, they step back to a third and become the far
   layer under the canvas. One attribute, two correct pictures.

   ★ AND IT IS KEYED ON THE CANVAS, NOT ON THE ENGINE. `data-ember` is set
   once the engine has BOOTED, which it does even when `getContext` returned
   null - so a reader whose privacy extension blocks canvas was getting the
   step-down for a canvas that never painted a pixel: a third-strength field
   under nothing at all. `data-ember-canvas` is written only where `S.ctx` is
   real, which is the condition that actually matters. */
.night[data-ember-canvas] { --em-mote-on: 0.34; }

/* ★ AND THE 'off' TIER RESTORES THEM. `data-q="off"` means the canvas has
   stood itself down - `saveData`, a data-saver, or two measured step-downs on
   a slow machine - so the motes are once again the only field there is and
   they go back to full strength. `connection.saveData` alone was measured
   taking this page to zero moving pixels. */
.night[data-q="off"] { --em-mote-on: 1; }

/* =========================================================================
   FLOOR 2 — THE LIVE SCENE.
   ========================================================================= */
.wc-ember {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
}

/* THE HEAT LOBES, AND THEY SIT WHERE THE EMBERS ARE THICKEST.

   ember.js releases a share of every plane from two vents along the bottom
   edge — at 28% and 74% of the width, which is what stops the field reading
   as an even wash of noise. These two lobes are the LIGHT of those vents, so
   they are positioned over them and the agreement holds at every viewport
   aspect: a radial gradient's `at x% y%` is normalised to its own box,
   exactly like the engine's normalised particle coordinates.

   `inset: -16%` gives the drift somewhere to go without ever showing an
   edge: the layer is 32% wider and taller than the viewport and the keyframe
   below never moves it more than 4%.
   ------------------------------------------------------------------------- */
.wc-ember-heat {
  position: absolute;
  inset: -16%;
  background:
    radial-gradient(38% 46% at 28% 86%, var(--em-heat-hi) 0%, transparent 72%),
    radial-gradient(34% 42% at 74% 92%, var(--em-heat) 0%, transparent 70%),
    radial-gradient(52% 30% at 50% 34%, var(--em-heat-lo) 0%, transparent 76%);
  will-change: transform;
}

.night[data-ember] .wc-ember-heat {
  animation: wc-em-draw 96s ease-in-out infinite alternate;
}

/* THE DRAW. Heat does not slide sideways, it BREATHES — so the dominant
   term is a vertical scale rather than a translate, and the small rotation
   is what keeps the two lobes from moving as one rigid object.

   ★ THE ANGLE STAYS UNDER THE INSET. `inset: -16%` gives 16% of slack on
   every side; a rotation of R degrees about the centre of a box needs about
   (w/2)(|sin R| + |cos R| - 1) of it. At 1.4 degrees that is ~1.2% on the
   long axis and the translate never spends more than 2.2%, so the corner
   never reaches the viewport at any window shape. */
@keyframes wc-em-draw {
  from { transform: translate3d(-1.8%, 1.6%, 0) scale(1.02, 1.04) rotate(-1.4deg); }
  to   { transform: translate3d(2.2%, -1.2%, 0) scale(1.08, 1.12) rotate(1.4deg); }
}

/* THE FIELD. One canvas, sized in device pixels by ember.js and stretched to
   the layer by CSS, so a DPR change is a texture change and never a layout
   change. */
.wc-ember-canvas {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
}

/* THE READING SCRIM. Same argument backdrop.css and space.css make: the
   reading column is `.wrap` at 1200px centred, so on a wide screen the outer
   margins carry no text at all and do not need defending. Dense across the
   middle, open at the edges, so the field is visibly the room the page sits
   in. */
.wc-ember-veil {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(90deg,
      transparent 0%,
      var(--em-veil-soft) 8%,
      var(--em-veil-core) 30%,
      var(--em-veil-core) 70%,
      var(--em-veil-soft) 92%,
      transparent 100%),
    linear-gradient(180deg,
      var(--em-veil-soft) 0%,
      transparent 24%,
      transparent 62%,
      var(--em-veil-soft) 100%);
}

/* THE CLOSING EDGE. Above everything, so the page ends on a corner at every
   scroll position. One composited layer that never repaints. */
.wc-ember-edge {
  position: absolute;
  inset: 0;
  background: radial-gradient(126% 104% at 50% 46%,
    transparent 0%, transparent 44%, var(--em-edge) 100%);
}

/* =========================================================================
   THE GROUND, AND THE ONE PLACE THIS SHEET DEPARTS FROM backdrop.css.

   site.css ramps `main` from clear to `--veil-deep` — 94% of the floor — so
   the artwork owns the first screen and nothing below it. That is right for
   a PAINTING and wrong for a field: the ask was "particles in the
   background", not "particles above the fold", and embers that stop at 760px
   are a title card. So the ramp is re-pointed to 68% of the floor: the
   reading is still defended (panels carry their own opaque faces on top of
   this, and body copy sits on a scrim that closes before the first
   paragraph), and roughly a third of the field survives the whole way down.

   (0,2,1) beats site.css's bare `main` (0,0,1) and cannot be reached by an
   owner who is not on this atmosphere, because the attribute it hangs on is
   written by ember.js and ember.js only ships when the knob says so.
   ========================================================================= */
.night[data-ember] ~ main {
  background-image: linear-gradient(180deg,
    transparent 0,
    transparent clamp(360px, 68vh, 760px),
    var(--em-page-floor) clamp(720px, 116vh, 1280px));
}

/* =========================================================================
   THE QUALITY LADDER'S CSS HALF.

   ember.js writes `data-q` on `.night` — 'high', 'low' or 'off' — the same
   contract backdrop.js and space.js publish. Only the layers that cost
   something are named here; the bed and the veil are free and stay in every
   tier, because a page that loses its ground on a slow machine looks broken
   rather than kind.
   ========================================================================= */
.night[data-q="low"] .wc-ember-heat { opacity: .68; }
.night[data-q="off"] .wc-ember-heat { animation: none; will-change: auto; }
.night[data-q="off"] .wc-ember-canvas { opacity: .82; }

/* =========================================================================
   REDUCED MOTION — the still field, and it is the same picture.

   Nothing is removed. The bed, the heat, the embers, the veil and the edge
   are all still painted; only the clocks stop. ember.js draws exactly ONE
   frame here and never arms a rAF, so the scene a reduced-motion visitor
   sees is the scene everybody else sees, held still.
   ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  .night[data-ember] .wc-ember-heat { animation: none; }
  .wc-ember-heat { will-change: auto; }
  /* ★ THE MOTES STOP TRAVELLING AND KEEP BREATHING. Translation is the half
     that provokes discomfort, so `wc-em-rise` goes; the opacity breathe stays,
     slowed to a third of its rate, so a reader who asked for less motion still
     gets a page that is alive rather than a photograph. That distinction is
     the whole complaint this layer was built for. */
  .night::before { animation: wc-em-breathe 21s ease-in-out infinite; }
  .night::after  { animation: wc-em-breathe 33s ease-in-out infinite; }
}

/* =========================================================================
   PHONE. A 390px screen is the smallest budget and the biggest battery risk.
   The heat stays (it is one composited layer and it is most of the
   character); the veil flattens, because at 390px text spans the whole width
   and there is no open margin left to give the picture.
   ========================================================================= */
@media (max-width: 820px) {
  .night {
    background:
      radial-gradient(120% 34% at 50% 104%, var(--em-horizon) 0%, transparent 64%),
      radial-gradient(120% 100% at 50% 48%, var(--em-bed) 0%, var(--em-floor) 76%);
    background-color: var(--em-floor);
  }
  .wc-ember-veil {
    background: linear-gradient(180deg,
      var(--em-veil-soft) 0%,
      var(--em-veil-soft) 16%,
      var(--em-veil-core) 46%,
      var(--em-veil-core) 100%);
  }
  .wc-ember-heat { inset: -24%; }
}

/* =========================================================================
   FORCED COLOURS / WINDOWS HIGH CONTRAST.

   site.css already sets `.night { display: none }` here and everything this
   sheet builds lives inside `.night`, so the whole layer leaves with it.
   Restated anyway, for the reason backdrop.css and space.css restate it:
   `display: none` on an ancestor is the kind of protection that disappears
   the moment somebody makes the backdrop a sibling.

   `main`'s re-pointed ramp goes too. A reader in forced colours has asked
   the OS to decide every colour on the page, and a ramp is texture with no
   information in it.
   ========================================================================= */
/* =========================================================================
   FORCED COLOURS / WINDOWS HIGH CONTRAST — and this block is now the one that
   decides whether the owner sees anything move at all.

   site.css sets `.night { display: none }` here, and every layer this scene
   builds lives inside `.night`, so the whole backdrop left with it. That is
   the ordinary, correct instinct for a decorative layer — and it is why a
   sweep of thirteen browser configurations found this page at LITERALLY ZERO
   moving pixels under High Contrast while the engine went on reporting
   `running: true, tier: "high", fps: 60`. A canvas engine cannot see a CSS
   `display: none` above it, so the telemetry was healthy and the screen was a
   photograph, both true at once.

   WHAT STAYS AND WHAT GOES. The CANVAS still goes: hundreds of soft alpha
   glows are exactly the kind of low-contrast texture High Contrast exists to
   remove, and there is no honest way to draw them in system colours. The
   SCRIPTLESS MOTES stay, at half strength, with `forced-color-adjust: none`
   so their own colour survives — the same deliberate opt-out this theme
   already takes for the nav's current-page marker. Two small drifting dot
   fields at 50% are not a legibility risk: nothing on this site reads against
   the backdrop, because every panel carries a fully opaque face.

   ★ `.night` HAS TO BE UN-HIDDEN HERE, because the motes are its own
   pseudo-elements and a `display: none` ancestor takes them with it. Its
   BACKGROUND is stood down to nothing so the ground stays whatever the OS
   chose; only the two mote layers are drawn back.
   ========================================================================= */
@media (forced-colors: active) {
  .night {
    display: block;
    background: none;
    background-color: transparent;
  }
  .night::before,
  .night::after {
    display: block;
    forced-color-adjust: none;
    opacity: calc(var(--em-mote-on) * 0.5);
  }
  .wc-ember { display: none; }
  .night[data-ember] ~ main { background-image: none; }
}

@media print {
  .wc-ember { display: none; }
  .night::before, .night::after { display: none; }
  .night[data-ember] ~ main { background-image: none; }
}
