/* The living background's own styling.
 *
 * js/scene.js builds the nodes; this file says how they sit and move. The sky
 * colours come in as custom properties that the script re-sets as the hour
 * changes, so a CSS transition does the fading between dawn, day, dusk and
 * night rather than any per-frame work.
 */

/* Pinned to the top and as tall as the screen gets with the address bar
   hidden, rather than stretched between top and bottom. With `inset: 0` the
   box changed height every time a phone's address bar slid in or out during
   a scroll, and everything placed by percentage jumped with it. `lvh` is
   that largest height; plain `vh` is the fallback and means the same thing
   on the phones that lack it. */
.mood {
  position: fixed; top: 0; left: 0; right: 0;
  height: 100vh; height: 100lvh;
  z-index: 0; pointer-events: none; overflow: hidden;
}

/* --- sky ---------------------------------------------------------------- */
.scene-sky {
  position: absolute; inset: 0;
  /* Light comes from overhead and falls away downward, and that is all. There
     used to be a second pool low on the left; with a bright --sky-2 behind it
     that read as a lamp shining in from off the bottom corner rather than as
     sky, and it fought the downward fall of the gradient under it. */
  background:
    radial-gradient(120% 80% at 50% -10%, var(--sky-1, #2c3a22) 0%, transparent 62%),
    linear-gradient(180deg, var(--sky-2, #151c11) 0%, var(--sky-3, #10150c) 100%);
  /* long, so walking from dusk into night is never a visible step */
  transition: background 4s linear;
}

/* --- image layers ------------------------------------------------------- */
/* No blanket `will-change` on any of these. It asks the browser to keep each
   layer as its own full-screen picture in graphics memory, and a scene is
   fifty-odd layers - including every other season's and hour's, which sit
   there invisible. On a phone that was a quarter of a gigabyte and a
   stutter on every scroll. Anything that actually animates gets its own
   layer from the browser anyway, while it is animating. */
.scene-layer {
  position: absolute; inset: 0;
  image-rendering: pixelated;
  image-rendering: crisp-edges;
}
/* Painted plates are anchored to an edge and fade toward the middle, so the
   scenery frames the interface instead of sitting behind the text. */
.scene-plate {
  /* painted, not pixel art - let the browser smooth these */
  image-rendering: auto;
}

/* The fades toward the middle of the screen.
 *
 * These are written with many stops on purpose. A three-stop fade changes its
 * rate abruptly at the middle stop, and the eye reads that change of rate as a
 * line - a hard vertical edge appeared down the standing stone exactly where
 * the 40% stop sat. The extra stops trace a smooth curve instead, so there is
 * no point along the fade where anything happens suddenly.
 */
.scene-edge-left {
  -webkit-mask-image: linear-gradient(90deg, #000 0%, rgba(0,0,0,.97) 18%,
    rgba(0,0,0,.88) 32%, rgba(0,0,0,.72) 44%, rgba(0,0,0,.5) 55%,
    rgba(0,0,0,.28) 65%, rgba(0,0,0,.1) 76%, transparent 88%);
  mask-image: linear-gradient(90deg, #000 0%, rgba(0,0,0,.97) 18%,
    rgba(0,0,0,.88) 32%, rgba(0,0,0,.72) 44%, rgba(0,0,0,.5) 55%,
    rgba(0,0,0,.28) 65%, rgba(0,0,0,.1) 76%, transparent 88%);
}
.scene-edge-right {
  -webkit-mask-image: linear-gradient(270deg, #000 0%, rgba(0,0,0,.97) 18%,
    rgba(0,0,0,.88) 32%, rgba(0,0,0,.72) 44%, rgba(0,0,0,.5) 55%,
    rgba(0,0,0,.28) 65%, rgba(0,0,0,.1) 76%, transparent 88%);
  mask-image: linear-gradient(270deg, #000 0%, rgba(0,0,0,.97) 18%,
    rgba(0,0,0,.88) 32%, rgba(0,0,0,.72) 44%, rgba(0,0,0,.5) 55%,
    rgba(0,0,0,.28) 65%, rgba(0,0,0,.1) 76%, transparent 88%);
}
.scene-band-top {
  -webkit-mask-image: linear-gradient(180deg, #000 0%, rgba(0,0,0,.95) 22%,
    rgba(0,0,0,.8) 40%, rgba(0,0,0,.55) 58%, rgba(0,0,0,.25) 76%, transparent 94%);
  mask-image: linear-gradient(180deg, #000 0%, rgba(0,0,0,.95) 22%,
    rgba(0,0,0,.8) 40%, rgba(0,0,0,.55) 58%, rgba(0,0,0,.25) 76%, transparent 94%);
}
.scene-band-bottom {
  -webkit-mask-image: linear-gradient(0deg, #000 0%, rgba(0,0,0,.95) 22%,
    rgba(0,0,0,.8) 40%, rgba(0,0,0,.55) 58%, rgba(0,0,0,.25) 76%, transparent 94%);
  mask-image: linear-gradient(0deg, #000 0%, rgba(0,0,0,.95) 22%,
    rgba(0,0,0,.8) 40%, rgba(0,0,0,.55) 58%, rgba(0,0,0,.25) 76%, transparent 94%);
}
/* For things lying on the ground rather than hanging in the air. The long
   fade above is right for foliage that should dissolve into the dark, and
   wrong for a heap of leaves: it turned the top half of the drift into a
   haze of half-transparent leaves climbing the face of the stone behind it.
   This one stays solid and then stops. */
.scene-band-sharp {
  -webkit-mask-image: linear-gradient(0deg, #000 0%, #000 62%,
    rgba(0,0,0,.72) 80%, rgba(0,0,0,.3) 92%, transparent 100%);
  mask-image: linear-gradient(0deg, #000 0%, #000 62%,
    rgba(0,0,0,.72) 80%, rgba(0,0,0,.3) 92%, transparent 100%);
}

/* A scrim over the scenery and under the interface. The art is busy and the
   cards are translucent, so without this the leaves read straight through the
   text. It is darkest down the middle, exactly where the reading happens. */
.scene-scrim {
  position: absolute; inset: 0;
  background:
    linear-gradient(90deg, transparent 0%, rgba(var(--scrim), .78) 24%,
                    rgba(var(--scrim), .84) 50%, rgba(var(--scrim), .78) 76%, transparent 100%);
}
/* Desktop: the interface is a centred column, so the scrim narrows to match
   and the scenery either side stays bright. */
@media (min-width: 620px) {
  /* A soft ellipse, not a banded gradient: the stepped version drew a
     visible rectangle edge down the middle of the screen. */
  .scene-scrim {
    background:
      radial-gradient(58% 120% at 50% 45%,
                      rgba(var(--scrim), .88) 0%,
                      rgba(var(--scrim), .78) 45%,
                      rgba(var(--scrim), .35) 72%,
                      transparent 100%);
  }
}
/* --- the light in the carvings ------------------------------------------
 *
 * Its own plate, laid exactly over its stone, holding only the glow. It is
 * dark nearly all the time: the runes wake, hold for a moment, and go out.
 * A steady glow would just be a highlight painted on the rock.
 *
 * `screen` so the light adds to the stone rather than covering it, and the
 * phase filters below leave it alone - runework does not dim at night, it is
 * the one thing that gets brighter.
 */
.scene-rune-glow {
  mix-blend-mode: screen;
  opacity: 0;
  animation: rune-wake 21s ease-in-out infinite;
}
/* How far the stones wake. Dusk is a stir rather than a waking: the same
   cycle at half strength, so they look like they are coming to rather than
   like a dimmer version of the night. */
.scene-rune-glow { --wake: 1; }
@keyframes rune-wake {
  0%, 62%  { opacity: 0; }
  72%      { opacity: calc(.85 * var(--wake)); }
  78%      { opacity: calc(.55 * var(--wake)); }
  84%      { opacity: calc(.95 * var(--wake)); }
  100%     { opacity: 0; }
}

/* The light a waking stone throws onto the ground at its foot.
 *
 * Shares `rune-wake` with the carvings, and is given the same delay as the
 * stone it belongs to, so the two cannot drift apart: the ground brightens
 * exactly when its own stone does. `screen` because this is light being
 * added to what is already there, not a colour laid over it.
 */
.scene-rune-pool {
  --wake: 1;
  --peak: .5;
  mix-blend-mode: screen;
  opacity: 0;
  animation: rune-pool-wake 21s ease-in-out infinite;
  pointer-events: none;
}
/* The same shape as the carvings' cycle, scaled by both how awake the hour
   makes the stones and how strong this particular pool is. */
@keyframes rune-pool-wake {
  0%, 62%  { opacity: 0; }
  72%      { opacity: calc(.85 * var(--wake) * var(--peak)); }
  78%      { opacity: calc(.55 * var(--wake) * var(--peak)); }
  84%      { opacity: calc(.95 * var(--wake) * var(--peak)); }
  100%     { opacity: 0; }
}
.scene-rune-pool.at-left {
  background: radial-gradient(42% 26% at 14% 88%,
    rgba(120, 196, 255, .85) 0%, rgba(80, 150, 220, .38) 38%, transparent 72%);
}
.scene-rune-pool.at-right {
  background: radial-gradient(38% 24% at 86% 90%,
    rgba(120, 196, 255, .8) 0%, rgba(80, 150, 220, .34) 38%, transparent 72%);
}
/* Faint by day, strongest after dark - the same rule the carvings follow. */
html[data-phase="day"]   .scene-rune-pool { --wake: .16; }
html[data-phase="dawn"]  .scene-rune-pool { --wake: .3; }
html[data-phase="dusk"]  .scene-rune-pool { --wake: .46; }
html[data-phase="night"] .scene-rune-pool { --wake: 1; }

/* Blossom hangs from above, so it swings from where it is attached instead of
   sliding sideways the way a rooted plate does. */
.scene-bloom {
  transform-origin: 50% 0;
  animation-name: scene-hang;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
  animation-direction: alternate;
}
@keyframes scene-hang {
  from { transform: rotate(-0.55deg); }
  to   { transform: rotate(0.55deg); }
}

/* The fairy ring breathes rather than flickers: slow, and never all the way
   down, because the caps are lit from inside. */
.scene-ring-glow {
  --ring: 1;
  animation: ring-breathe 11s ease-in-out infinite alternate;
  transition: filter 6s linear;
}
/* The toadstools are lit from inside, so the hour cannot brighten them - but
   it should be able to put them out. They used to burn as hard at noon as at
   midnight, which meant the grove never actually woke up. --ring scales the
   whole breath rather than replacing it, so they still pulse while dim. */
html[data-phase="night"] .scene-ring-glow { --ring: 1; }
html[data-phase="dusk"]  .scene-ring-glow { --ring: .84; }
html[data-phase="dawn"]  .scene-ring-glow { --ring: .6; }
html[data-phase="day"]   .scene-ring-glow { --ring: .46; }
/* Under autumn cloud there is less daylight to drown them out, so the same
   glow simply tells more. */
html[data-season="autumn"] .scene-ring-glow { --ring-season: 1.4; }
@keyframes ring-breathe {
  from { filter: brightness(calc(.82 * var(--ring) * var(--ring-season, 1))) saturate(.95); }
  to   { filter: brightness(calc(1.22 * var(--ring) * var(--ring-season, 1))) saturate(1.1); }
}

@keyframes scene-sway {
  from { transform: translate3d(-10px, 0, 0); }
  to   { transform: translate3d(10px, 4px, 0); }
}

/* light through the canopy, drifting */
.scene-canopy {
  background:
    radial-gradient(38% 22% at 22% 6%, rgba(210, 238, 168, .20) 0%, transparent 70%),
    radial-gradient(30% 18% at 78% 2%, rgba(186, 222, 146, .15) 0%, transparent 70%);
  animation: scene-drift 34s ease-in-out infinite alternate;
  transition: opacity 3s linear;
}
@keyframes scene-drift {
  from { transform: translate3d(-14px, 0, 0); }
  to   { transform: translate3d(16px, 12px, 0); }
}

/* Light: the hour and the season, combined.
 *
 * These used to be two competing sets of `filter` rules at identical
 * specificity, so whichever came last in the file won outright - which meant
 * that in autumn and winter the time of day did nothing at all, because the
 * season rules were written underneath. A winter midnight looked exactly like
 * a winter noon.
 *
 * So each axis now contributes numbers rather than a whole filter, and one
 * rule multiplies them together. Brightness and saturation multiply, because
 * they are scales; sepia and hue-rotate add, because they are amounts. The
 * two can no longer overwrite one another.
 *
 * The two lit plates sit out of all of it: the runes and the toadstools make
 * their own light, so dusk must not dim them and winter must not drain their
 * colour. Both carry the class `lit`, added by js/scene.js.
 */
:root {
  --ph-bright: 1; --ph-sat: 1; --ph-sepia: 0; --ph-hue: 0deg;
  --se-bright: 1; --se-sat: 1; --se-sepia: 0; --se-hue: 0deg;
  /* What colour the dark between the trees is. A green-black for three
     seasons of the year, because that is what the shade under a canopy in
     leaf actually is - and the one thing it is not in winter. */
  --scrim: 8, 12, 8;
}
/* A third set, --el-*, lets one layer differ from the rest of the scene in a
   season without having to restate the whole filter and lose the other two. */
.scene-layer:not(.lit) {
  filter:
    brightness(calc(var(--ph-bright) * var(--se-bright) * var(--el-bright, 1)))
    saturate(calc(var(--ph-sat) * var(--se-sat) * var(--el-sat, 1)))
    sepia(calc(var(--ph-sepia) + var(--se-sepia) + var(--el-sepia, 0)))
    hue-rotate(calc(var(--ph-hue) + var(--se-hue) + var(--el-hue, 0deg)));
}

/* Layers that belong to one season, one hour, or one hour of one season.
   Kept at nothing the rest of the time and faded in when their moment comes
   round, never hidden outright, so the change is something you can watch
   happen. A layer wanting both writes them as one class, because two rules
   each setting an opacity would mean the later one won rather than the pair
   having to agree. */
/* A season gate that removes the element instead of fading it.
   Needed wherever an animation owns the opacity - see the rune overlays. */
.scene-only { display: none; }
html[data-season="spring"] .scene-only.only-spring,
html[data-season="summer"] .scene-only.only-summer,
html[data-season="autumn"] .scene-only.only-autumn,
html[data-season="winter"] .scene-only.only-winter { display: block; }

.scene-when { opacity: 0; transition: opacity 6s linear; }
html[data-season="autumn"] .scene-when.for-autumn,
html[data-season="winter"] .scene-when.for-winter,
html[data-season="spring"] .scene-when.for-spring,
html[data-season="summer"] .scene-when.for-summer,
html[data-season="autumn"][data-phase="dusk"] .scene-when.for-autumn-dusk,
html[data-season="autumn"][data-phase="night"] .scene-when.for-autumn-night,
html[data-season="autumn"][data-phase="dawn"] .scene-when.for-autumn-dawn,
html[data-season="spring"][data-phase="day"] .scene-when.for-spring-day,
html[data-season="spring"][data-phase="dawn"] .scene-when.for-spring-dawn,
html[data-season="spring"][data-phase="dusk"] .scene-when.for-spring-dusk,
html[data-season="spring"][data-phase="night"] .scene-when.for-spring-night,
html[data-season="winter"][data-phase="dawn"] .scene-when.for-winter-dawn,
html[data-season="winter"][data-phase="dusk"] .scene-when.for-winter-dusk,
html[data-season="winter"][data-phase="night"] .scene-when.for-winter-night { opacity: var(--full, 1); }

/* The inverse of a season gate: present every season but one.
 *
 * `.scene-when` says "only then". This says "never then", which is what a
 * season needs when it does not add something but stands in for it - the
 * winter vines are not laid over the summer ones, they replace them, so the
 * summer ones have to go. Written before .thins deliberately: a layer with
 * both keeps thinning through the seasons it is still in, and the gate below
 * outruns both on specificity when the year gets there. */
.scene-layer[data-unless] { opacity: var(--full, 1); transition: opacity 6s linear; }
html[data-season="spring"] .scene-layer[data-unless~="spring"],
html[data-season="summer"] .scene-layer[data-unless~="summer"],
html[data-season="autumn"] .scene-layer[data-unless~="autumn"],
html[data-season="winter"] .scene-layer[data-unless~="winter"] { opacity: 0; }

/* Foliage loses cover as the year turns, which is also what lets more sky and
   more starlight through the branches in the cold half of it. */
.scene-layer.thins { opacity: calc(var(--full, 1) * var(--cover, 1)); }
html[data-season="autumn"] { --cover: .76; }
/* Winter keeps more of the canopy than you would expect, not less. A bare
   oak is a dense tangle of twigs - it lets the light through without being
   thin - and at this season's saturation the leaves have already gone grey
   and brown, so what is left overhead reads as branches. Taken down to .46
   the top of the screen was simply empty. */
html[data-season="winter"] { --cover: .62; }
/* Except with the moon behind them, when the branches want more body.
   Thinned to let the sky through, bare oak crossing a full moon came out as
   a suggestion of a branch rather than a branch - and the moon is the one
   thing in the sixteen bright enough to show up what is in front of it. */
html[data-season="winter"][data-phase="night"] { --cover: .82; }
/* Where the moon hangs until js/scene.js has looked at the branches and
   chosen a spot (see chooseSpot) - a moment at most. High on a monitor;
   below the header and points bar on a phone, where those span the width. */
/* Where two of spring's night creatures stand. On a monitor, in the open
   scenery either side of the card; on a phone, where the card covers the
   middle and the vines fill both edges, in the gap between the vines. */
:root { --fawn-x: 80%; --rabbit-x: 100%; }
@media (max-width: 619px) { :root { --fawn-x: 32vw; --rabbit-x: 32vw; } }

:root { --moon-top: 40px; }
@media (max-width: 619px) { :root { --moon-top: 150px; } }

/* The blossom goes over in autumn and is nearly gone by winter. A filter
   rather than an opacity, so it browns and thins instead of just fading. */
html[data-season="autumn"] .scene-bloom { --el-sat: .5; --el-sepia: .32; --el-bright: .84; }
/* Winter no longer has to fake it.
 *
 * It used to bleach the summer curtain to almost no colour and push it
 * brighter than life, which is what a hoar frost does and was the best that
 * could be managed while the flowers were painted into the plate. They are
 * not any more - winter has its own bare plates - so all this does now is
 * let the frost read white against a dark grove. */
html[data-season="winter"] .scene-bloom { --el-bright: 1.08; }

/* The climbing growth goes the same way. Autumn still has to do it by
   filter, because there is only the one summer vine plate to work with;
   winter has its own and needs no more than the same lift as the blossom. */
html[data-season="autumn"] .scene-growth { --el-sat: .62; --el-sepia: .26; }
html[data-season="winter"] .scene-growth { --el-bright: 1.06; }

/* Spider silk is faint by daylight and catches what light there is at night. */
.scene-web {
  mix-blend-mode: screen;
  filter: blur(.4px);
}
html[data-phase="day"] .scene-web { --el-bright: .8; }
/* Except on an autumn morning, when the silk is loaded with dew and is the
   brightest thing in the grove. The webs only exist in autumn and the dew
   only forms overnight, so this is the one hour of the year it happens. */
html[data-season="autumn"][data-phase="dawn"] .scene-web {
  --el-bright: 2.1;
  --el-sat: .7;
  filter: blur(.2px) drop-shadow(0 0 3px rgba(198, 226, 255, .55));
}

/* --- low sun -------------------------------------------------------------
 *
 * Near-horizontal shafts through the fog. Every other light in this scene
 * falls from overhead; this one comes in from the side, which is most of
 * what makes it read as a sun that has only just cleared the trees.
 *
 * The shafts are a repeating gradient rather than art, masked so they are
 * strongest where the light enters and gone before the far side. They drift,
 * because a shaft only exists where there is something in the air to catch
 * it and that something is moving.
 */
.scene-sunrays {
  mix-blend-mode: screen;
  pointer-events: none;
  background: repeating-linear-gradient(
    102deg,
    transparent 0px, transparent 26px,
    rgba(255, 236, 198, .1) 34px, rgba(255, 240, 210, .16) 48px,
    rgba(255, 236, 198, .07) 62px, transparent 78px, transparent 116px);
  -webkit-mask-image: radial-gradient(120% 90% at 4% 18%,
    #000 0%, rgba(0,0,0,.65) 34%, rgba(0,0,0,.2) 62%, transparent 84%);
  mask-image: radial-gradient(120% 90% at 4% 18%,
    #000 0%, rgba(0,0,0,.65) 34%, rgba(0,0,0,.2) 62%, transparent 84%);
  animation: sunrays-drift 47s ease-in-out infinite alternate;
}
@keyframes sunrays-drift {
  from { transform: translate3d(-1.5%, 0, 0) skewX(-0.6deg); }
  to   { transform: translate3d(1.5%, 0, 0) skewX(0.6deg); }
}

/* Rain on everything: a soft sheet of highlight low down that slides very
   slowly, so the undergrowth looks wet rather than merely dark. */
.scene-sheen {
  mix-blend-mode: screen;
  background:
    radial-gradient(70% 50% at 30% 90%, rgba(196, 224, 236, .5) 0%, transparent 70%),
    radial-gradient(55% 40% at 78% 96%, rgba(178, 212, 228, .38) 0%, transparent 72%);
  animation: sheen-shift 29s ease-in-out infinite alternate;
}
@keyframes sheen-shift {
  from { transform: translate3d(-3%, 0, 0); }
  to   { transform: translate3d(3%, 0, 0); }
}

/* Snow lying on the ground.
 *
 * The whole difficulty is the top edge. A straight one reads as a sheet of
 * paper laid over the scene, so the shape is four mounds of different
 * heights sitting mostly below the element - only their crowns show - over a
 * base that comes up to meet them. Where two mounds overlap the edge between
 * them dips, which is what makes a drift look drifted rather than drawn.
 *
 * The mounds are nearly flat-topped - .97 to .94 across most of the radius
 * and only then to nothing - because a drift is a solid with an edge. The
 * first version faded over two hundred pixels and read as fog, which taught
 * me that what says "snow" is the edge and not the brightness.
 *
 * It also has to keep out of the bottom of the frame: .mood-vignette and the
 * scrim between them take about half the light out of the last hundred
 * pixels, so anything whose brightest part is down there disappears. The
 * mass sits in the middle of the band and the base carries it down.
 *
 * Nothing here moves. Lying snow does not, and every other ground layer in
 * this scene drifts or breathes, so being the still one is part of how it
 * reads as weight.
 */
.scene-layer.scene-snow-cover {
  background:
    /* The hollows between the mounds, in shadow. Drawn first so the mounds
       come out over them, and blue rather than grey because snow in shade
       takes its colour from the sky and nothing else. */
    radial-gradient(30% 40% at 25% 122%, rgba(126,158,190,.5) 0%, rgba(126,158,190,0) 72%),
    radial-gradient(26% 34% at 80% 126%, rgba(126,158,190,.45) 0%, rgba(126,158,190,0) 72%),
    /* the mounds, brightest along their crests and cooling as they fall away */
    radial-gradient(64% 78% at 10% 100%,
      rgba(252,254,255,.98) 0%, rgba(228,240,250,.95) 78%, rgba(228,240,250,0) 100%),
    radial-gradient(46% 50% at 36% 108%,
      rgba(248,252,255,.97) 0%, rgba(219,233,246,.93) 76%, rgba(219,233,246,0) 100%),
    radial-gradient(72% 86% at 66% 102%,
      rgba(253,254,255,.98) 0%, rgba(230,241,251,.94) 79%, rgba(230,241,251,0) 100%),
    radial-gradient(50% 56% at 94% 106%,
      rgba(250,253,255,.97) 0%, rgba(223,236,248,.93) 76%, rgba(223,236,248,0) 100%),
    /* the body of it, going blue as it recedes under the roots */
    linear-gradient(to bottom,
      rgba(226,238,249,0)   0%,
      rgba(226,238,249,0)  46%,
      rgba(238,246,253,.9) 58%,
      rgba(206,223,240,.97) 100%);
}
/* Snow has no light of its own. Left at full white it went on glowing after
   dark, so the hour dims it - and it has to be written as an opacity here
   rather than through --full, because --full is set inline by the layer
   builder and an inline custom property beats any rule in this file. Two
   attributes plus two classes outruns the season gate, which is what these
   are overriding. */
html[data-season="winter"][data-phase="dawn"]  .scene-layer.scene-snow-cover {
  opacity: calc(var(--full, 1) * .82); filter: brightness(.95) saturate(.9);
}
/* Dusk keeps almost all of it: the snow is still catching light when nothing
   else in the grove is. Cool and a little dimmed here - the warmth is a
   separate layer, because a filter over the whole plate would have lit the
   shadow hollows too, and a hollow the sun cannot reach is the entire reason
   the crests read as crests. */
html[data-season="winter"][data-phase="dusk"]  .scene-layer.scene-snow-cover {
  opacity: calc(var(--full, 1) * .9); filter: brightness(.82) saturate(1.1);
}

/* Alpenglow.
 *
 * The last of the sun arrives almost horizontally, and by this hour the only
 * thing still standing in it is the snow: rose and apricot along the crests,
 * deep blue in everything it cannot reach. It is the one time of day the
 * ground is warmer in colour than the sky above it.
 *
 * Laid on the same four mound centres as the drift underneath, but smaller
 * and higher, so the light sits on the crowns instead of washing the whole
 * field.
 *
 * Multiply, not screen. Screen was the instinct - this is light, after all -
 * but screen against something already near white does nothing at all, and
 * the first version was invisible for exactly that reason. The snow does not
 * need making brighter, it needs its white turned rose, and multiplying a
 * warm colour into it is what does that. */
.scene-layer.scene-alpenglow {
  mix-blend-mode: multiply;
  /* Each centre is its own mound's crown plus a little: the drift's crests
     work out at 22%, 58%, 16% and 50% down the band, from the radii above. */
  background:
    radial-gradient(36% 26% at 10% 30%, rgba(255,142,122,.9), rgba(255,142,122,0) 76%),
    radial-gradient(24% 18% at 36% 66%, rgba(255,132,124,.7), rgba(255,132,124,0) 76%),
    radial-gradient(40% 30% at 66% 24%, rgba(255,158,120,.88), rgba(255,158,120,0) 76%),
    radial-gradient(28% 22% at 94% 58%, rgba(255,138,126,.78), rgba(255,138,126,0) 76%);
}
/* Night, under a full moon - which is not the same as night in the dark.
   Snow has no light of its own and the first version dimmed it almost out,
   but there is a moon up now and a snowfield under one is the brightest
   ground in the whole sixteen: the one night of the year you can read by.
   Cold and blue rather than dim, which is what moonlight on snow is. */
html[data-season="winter"][data-phase="night"] .scene-layer.scene-snow-cover {
  opacity: calc(var(--full, 1) * .76);
  filter: brightness(.66) saturate(.8) hue-rotate(6deg);
}

/* The hour's own --ph-* values are set by js/scene.js, not here.
 *
 * They used to be four fixed sets, one per hour, switched at the boundary.
 * That could only ever step: at 17:29 it was the middle of the afternoon and
 * at 17:31 it was evening. The script now carries each hour's numbers and
 * eases from one into the next across the last part of the hour, so dusk
 * arrives over the late afternoon instead of being thrown. There is nothing
 * for a stylesheet to say about a value that changes every minute.
 *
 * `data-phase` is still set, and is still a hard switch - it decides whether
 * a thing exists at all. Mist should not be half present. */

/* the season. Summer is the baseline. */
/* Spring: brighter and fresher, and turned a little toward yellow-green -
   new leaves, not summer's dark ones. Only a little, because the bluebells
   go through the same filter and must stay violet. */
html[data-season="spring"] { --se-bright: 1.06; --se-sat: 1.12; --se-hue: -6deg; }
html[data-season="autumn"] { --se-sat: 1.15; --se-sepia: .18; }
html[data-season="winter"] { --se-bright: .85; --se-sat: .58; --se-hue: 12deg; }
/* ...except at noon. Winter daylight is the brightest in the set: it comes
   down through bare branches and then back up off the snow, and what it
   loses on the way is its colour, not its strength. The season darkens by
   default because three of its four hours want that, so the fourth has to
   say otherwise - two attributes beat the one above it. */
html[data-season="winter"][data-phase="day"] { --se-bright: 1.06; --se-sat: .34; --se-hue: 9deg; }

/* The shade turns with the season too. Winter's is a blue-black - there are
   no leaves left to make it green, and the pale sky I gave the hour was
   being eaten by a green vignette laid over the top of it. Autumn's keeps a
   little of the brown it has been collecting all season. */
html[data-season="winter"] { --scrim: 13, 19, 29; }
html[data-season="autumn"] { --scrim: 14, 12, 8; }
/* The Wizard's shade is violet-black, whatever the season. */
html[data-class="Wizard"] { --scrim: 16, 8, 28; }

/* And in winter it has to reach less far down.
 *
 * The scrim and the app's vignette between them were taking well over half
 * the light out of the bottom of the frame - fine for three seasons whose
 * ground is dark anyway, fatal for the one whose whole subject is a bright
 * white floor. The snow came out the exact grey of the sky it was supposed
 * to stand against. Both are pulled up and off the ground here; what still
 * covers the card, which is all either was ever for, is untouched. */
@media (min-width: 620px) {
  html[data-season="winter"] .scene-scrim {
    background:
      radial-gradient(58% 86% at 50% 38%,
                      rgba(var(--scrim), .88) 0%,
                      rgba(var(--scrim), .74) 45%,
                      rgba(var(--scrim), .26) 74%,
                      transparent 100%);
  }
}
/* Blue rather than black, so what it does still darken reads as cold. */
html[data-season="winter"] .mood-vignette {
  background: radial-gradient(140% 104% at 50% 40%,
                              transparent 62%, rgba(18, 30, 48, .34) 100%);
}

/* The canopy shafts fade out after dark, because there is no sun to make
   them, and thin in winter because the leaves are gone. */
html[data-season="winter"] .scene-canopy { opacity: .3 !important; }
html[data-phase="night"] .scene-canopy { opacity: .12 !important; }
/* After dark the scene is already dim; a heavy scrim on top of that just
   turns the screen black, so it eases off. */
html[data-phase="night"] .scene-scrim { opacity: .82; }
.scene-layer { transition: filter 4s linear; }

/* Carved light answers the hour backwards: faint by day, strongest after
   dark, which is when a rune-stone is worth looking at. */
html[data-phase="day"]   .scene-rune-glow { --wake: .2;  filter: saturate(.9); }
html[data-phase="dawn"]  .scene-rune-glow { --wake: .34; }
html[data-phase="dusk"]  .scene-rune-glow { --wake: .5;  filter: saturate(1.1); }
html[data-phase="night"] .scene-rune-glow { --wake: 1;   filter: brightness(1.35) saturate(1.2); }

/* Shadows on snow.
 *
 * Multiply, because a shadow is light being taken away rather than blue
 * being added - screened or laid on flat it turned the drift lilac. Blue
 * because a shadow on snow has nothing lighting it but the sky, which is the
 * one fact that makes snow shadows read as cold rather than as dirt. */
.scene-snow-shadow { mix-blend-mode: multiply; }
.scene-snow-shadow i {
  position: absolute;
  border-radius: 50%;
  /* Densest at the stone's own foot, gone by the far end - a long shadow
     loses its edge to the width of the light source as it goes. */
  background: linear-gradient(to left,
    rgba(62,94,156,.88) 0%, rgba(84,116,174,.6) 40%, rgba(120,150,196,0) 92%);
  filter: blur(9px);
}

/* The last of the sun climbing the trunks, and going out at the top. */
.scene-last-light {
  mix-blend-mode: screen;
  background: linear-gradient(to bottom,
    rgba(255,168,118,0) 0%, rgba(255,158,108,.55) 46%, rgba(255,136,96,0) 100%);
  animation: last-light-climb 38s ease-in infinite;
}
/* ...and at dawn the same band runs the other way: the first light lands on
   the treetops, comes down the trunks, and spreads out at the bottom. Rose
   and gold rather than the dusk's amber. */
.scene-last-light.falls {
  background: linear-gradient(to bottom,
    rgba(255,196,170,0) 0%, rgba(255,190,160,.5) 46%, rgba(255,210,170,0) 100%);
  animation-name: first-light-fall;
  animation-timing-function: ease-out;
}
@keyframes first-light-fall {
  0%   { transform: translate3d(0, -280%, 0); filter: brightness(0); }
  20%  { filter: brightness(1); }
  80%  { filter: brightness(.9); }
  100% { transform: translate3d(0, 0, 0);     filter: brightness(0); }
}

/* The rainbow.
 *
 * One radial gradient, most of whose radius is empty: the colours are a thin
 * ring near its edge, and the centre sits below the bottom of the screen so
 * only the top of the ring - the arc - comes into view. The stops are soft
 * rather than hard so the bands blend into one another the way a real one
 * does, without a blur filter to redraw. It fades out toward the ground.
 *
 * Hidden unless it is a spring day and the weather roll was a sun shower:
 * a rainbow needs rain and sun at once.
 */
.scene-rainbow {
  opacity: 0;
  transition: opacity 8s linear;
  mix-blend-mode: screen;
  background: radial-gradient(circle max(900px, 80vw) at 50% 118%,
    rgba(255, 80, 80, 0) 71%,
    rgba(255, 80, 80, .8) 73.5%,
    rgba(255, 165, 60, .8) 75.5%,
    rgba(255, 238, 100, .8) 77%,
    rgba(100, 225, 120, .75) 78.5%,
    rgba(80, 150, 255, .75) 80%,
    rgba(160, 100, 240, .7) 81.5%,
    rgba(160, 100, 240, 0) 84%);
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 40%, transparent 72%);
  mask-image: linear-gradient(180deg, #000 0%, #000 40%, transparent 72%);
}
html[data-season="spring"][data-phase="day"][data-weather="shower"] .scene-rainbow {
  opacity: var(--full, .5);
}

/* A plate turned into light: the bluebells glowing at dusk. Brightened,
   deepened and softened, and added on top of the plate itself. */
.scene-glow {
  mix-blend-mode: screen;
  filter: brightness(1.4) saturate(1.6) blur(3px);
}

/* Things that only come after rain - the evening mist and the wet shine -
   stay away when the hour's weather roll was dry. */
html[data-weather="dry"] .scene-layer.after-rain { opacity: 0 !important; }

/* The evening mist after a spring shower: the dawn mist's plate, lilac. */
.scene-smoke.lilac {
  filter: blur(2.4px) sepia(.35) hue-rotate(215deg) saturate(1.1) brightness(1.02);
}

/* The same mist after dark: silver under the moon instead of lilac. */
.scene-smoke.silver {
  filter: blur(2.4px) saturate(.35) brightness(.92);
}

/* The spring offering is taken in at night - it looked out of place there. */
html[data-season="spring"][data-phase="night"] .scene-layer[data-part="offering-spring"] {
  opacity: 0 !important;
}

/* --- spring dawn --------------------------------------------------------- */

/* The equinox sunrise. A half-risen sun low in the gap between the stones, a
   wide warm glow round it, and - on the second layer - a corridor of gold
   running along the ground toward you and light on the stones' inner faces.
   The sun sits behind the card on a monitor; its glow is wide enough to fill
   the scenery either side. */
.scene-equinox-sun {
  mix-blend-mode: screen;
  background:
    radial-gradient(circle 44px at 50% 72%,
      rgba(255, 252, 236, 1) 0%, rgba(255, 238, 196, .95) 62%, rgba(255, 220, 160, 0) 100%),
    radial-gradient(circle 260px at 50% 72%,
      rgba(255, 214, 156, .75) 0%, rgba(255, 190, 140, 0) 100%),
    radial-gradient(85vw 46vh at 50% 72%,
      rgba(255, 200, 150, .75) 0%, rgba(255, 186, 140, .35) 55%, rgba(255, 170, 130, 0) 100%);
  animation: pillar-breathe 12s ease-in-out infinite alternate;
}
.scene-equinox-path {
  mix-blend-mode: screen;
  background:
    /* the corridor: a wedge from the sun widening toward the viewer */
    conic-gradient(from 150deg at 50% 72%,
      rgba(255, 222, 170, 0) 0deg, rgba(255, 222, 170, .6) 30deg,
      rgba(255, 222, 170, 0) 60deg, rgba(255, 222, 170, 0) 360deg),
    /* the stones' inner faces catching it */
    radial-gradient(110px 260px at calc(min(200px, 32vw) - 30px) 62%,
      rgba(255, 204, 150, .7) 0%, rgba(255, 204, 150, 0) 100%),
    radial-gradient(110px 260px at calc(100% - min(200px, 32vw) + 30px) 62%,
      rgba(255, 204, 150, .7) 0%, rgba(255, 204, 150, 0) 100%);
  -webkit-mask-image: linear-gradient(180deg, transparent 38%, #000 60%);
  mask-image: linear-gradient(180deg, transparent 38%, #000 60%);
}

/* On a phone the scrim leaves only a thin strip of scenery down each edge,
   and the sunbeams, the sunrise glow and the light on the stones all piled
   up in those strips as flat white bands. They are much fainter there. The
   extra classes are to outrank the dawn gate that sets their full strength. */
@media (max-width: 619px) {
  html[data-season="spring"][data-phase="dawn"] .scene-layer.scene-when.scene-sunbeams {
    opacity: calc(var(--full, 1) * .3);
  }
  html[data-season="spring"][data-phase="dawn"] .scene-layer.scene-when.scene-equinox-sun {
    opacity: calc(var(--full, 1) * .5);
  }
  html[data-season="spring"][data-phase="dawn"] .scene-layer.scene-when.scene-equinox-path {
    opacity: calc(var(--full, 1) * .35);
  }
}

/* The Green Man comes into view slowly, as the light reaches him, rather
   than on the usual six-second fade. */
.scene-layer[data-part="green-man"] { transition-duration: 20s; }

/* The dawn mist turns rose and gold in spring, lit by the sunrise. The
   Avatar grove has no dawn mist of its own, so it gets this one as smoke. */
html[data-season="spring"][data-phase="dawn"] .scene-mist:not(.scene-smoke),
.scene-smoke.rosegold {
  filter: blur(1.4px) sepia(.45) saturate(1.7) hue-rotate(-22deg) brightness(1.06);
}

/* Sunbeams: a few soft shafts slanting down through the leaves, in the open
   scenery either side of the card, fading before they reach the ground and
   brightening and dimming as the leaves above them move. */
.scene-sunbeams {
  mix-blend-mode: screen;
  background:
    linear-gradient(100deg, transparent 7%, rgba(255, 236, 190, .3) 10.5%, transparent 14%),
    linear-gradient(96deg, transparent 20%, rgba(255, 230, 182, .22) 23%, transparent 26.5%),
    linear-gradient(103deg, transparent 71%, rgba(255, 236, 190, .28) 74%, transparent 78%),
    linear-gradient(98deg, transparent 84%, rgba(255, 228, 178, .24) 86.5%, transparent 90%);
  -webkit-mask-image: linear-gradient(180deg, #000 0%, rgba(0,0,0,.7) 45%, transparent 85%);
  mask-image: linear-gradient(180deg, #000 0%, rgba(0,0,0,.7) 45%, transparent 85%);
  animation: beams-shimmer 9s ease-in-out infinite alternate;
}
@keyframes beams-shimmer {
  from { filter: brightness(.7); }
  to   { filter: brightness(1.2); }
}

/* New leaves opening. When the scene reaches a spring dawn, both canopies
   start pale and closed - greyer, dimmer, a touch yellow - and open into
   their full fresh green over about a minute. --unfurl runs from 0 to 1 on
   the scene box and the canopies read it; it is registered so the browser
   can animate it, and it lives on the parent because the canopies already
   run their own sway animation, which a second one on them would replace. */
@property --unfurl {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}
html[data-season="spring"][data-phase="dawn"] .mood {
  animation: leaf-unfurl 75s ease-out 1 both;
}
@keyframes leaf-unfurl {
  from { --unfurl: 0; }
  to   { --unfurl: 1; }
}
html[data-season="spring"][data-phase="dawn"] .scene-layer[data-part="canopy-spring"] {
  --el-sat: calc(.4 + .6 * var(--unfurl));
  --el-bright: calc(.78 + .22 * var(--unfurl));
  --el-hue: calc(14deg * (1 - var(--unfurl)));
}

/* The wet shine at sunset: the rained-on look, warm instead of cold. */
.scene-sheen.warm {
  background:
    radial-gradient(70% 50% at 30% 90%, rgba(255, 196, 150, .5) 0%, transparent 70%),
    radial-gradient(55% 40% at 78% 96%, rgba(255, 170, 140, .4) 0%, transparent 72%);
}

/* Sunlight through young leaves: soft pale patches on the ground that drift
   and brighten and dim as the leaves above move. */
.scene-dapple {
  mix-blend-mode: screen;
  background:
    radial-gradient(60px 20px at 12% 64%, rgba(255, 246, 200, .55), rgba(255, 246, 200, 0) 72%),
    radial-gradient(90px 28px at 29% 82%, rgba(255, 240, 190, .5), rgba(255, 240, 190, 0) 72%),
    radial-gradient(70px 22px at 52% 72%, rgba(255, 246, 200, .45), rgba(255, 246, 200, 0) 72%),
    radial-gradient(110px 32px at 73% 86%, rgba(255, 240, 190, .5), rgba(255, 240, 190, 0) 72%),
    radial-gradient(64px 20px at 90% 68%, rgba(255, 246, 200, .5), rgba(255, 246, 200, 0) 72%),
    radial-gradient(80px 24px at 42% 94%, rgba(255, 240, 190, .45), rgba(255, 240, 190, 0) 72%);
  animation: dapple-drift 17s ease-in-out infinite alternate,
             dapple-flicker 5.5s ease-in-out infinite alternate;
}
@keyframes dapple-drift {
  from { transform: translate3d(-10px, 0, 0); }
  to   { transform: translate3d(12px, -3px, 0); }
}
@keyframes dapple-flicker {
  0%   { filter: brightness(.75); }
  50%  { filter: brightness(1.15); }
  100% { filter: brightness(.9); }
}

/* The sun pillar: a soft column of light standing straight up from where
   the sun is about to rise, brightest at its foot and fading upward, with
   the glow of the sun itself just below the treeline. It breathes slowly
   in brightness - never in opacity, which belongs to the dawn gate. */
.scene-sun-pillar {
  mix-blend-mode: screen;
  background:
    radial-gradient(110px 70px at var(--at-x) 60%,
      rgba(255,222,176,.8) 0%, rgba(255,196,150,.3) 55%, rgba(255,190,150,0) 100%),
    radial-gradient(16px 30% at var(--at-x) 38%,
      rgba(255,238,206,.9) 0%, rgba(255,214,186,.45) 50%, rgba(255,200,190,0) 100%),
    radial-gradient(44px 34% at var(--at-x) 40%,
      rgba(255,206,176,.28) 0%, rgba(255,190,180,0) 100%);
  animation: pillar-breathe 12s ease-in-out infinite alternate;
}
@keyframes pillar-breathe {
  from { filter: brightness(.82); }
  to   { filter: brightness(1.12); }
}

@keyframes last-light-climb {
  0%   { transform: translate3d(0, 0, 0);     filter: brightness(1); }
  62%  { transform: translate3d(0, -150%, 0); filter: brightness(.9); }
  100% { transform: translate3d(0, -280%, 0); filter: brightness(0); }
}

/* The aurora.
 *
 * Rays across, band down. The rays are a repeating gradient at an angle -
 * uneven on purpose, with the stops at nothing like regular intervals,
 * because evenly spaced rays read as a comb. The band is a mask, so the
 * curtain fades out top and bottom instead of ending in a line.
 *
 * `screen` because this is light in the sky and there is nothing up there
 * for it to be painted on: over the dark it shows, over the moon it does
 * not, which is correct both times.
 */
.scene-aurora {
  overflow: hidden;
  mix-blend-mode: screen;
  -webkit-mask-image: linear-gradient(180deg,
    transparent 0%, #000 26%, #000 52%, rgba(0,0,0,.45) 76%, transparent 100%);
  mask-image: linear-gradient(180deg,
    transparent 0%, #000 26%, #000 52%, rgba(0,0,0,.45) 76%, transparent 100%);
}
/* The curtain itself. Three times the width of what you can see and hung a
   width to the left of it, so it can slide either way without an edge ever
   coming into frame - which is also why this sways back and forth rather
   than scrolling: there is no loop point to hide. */
.scene-aurora > i {
  position: absolute;
  top: 0; bottom: 0; left: -100%; width: 300%;
  background:
    repeating-linear-gradient(var(--lean, 0deg),
      rgba(120, 255, 190, 0)    0px,
      rgba(120, 255, 190, .34) 26px,
      rgba(150, 255, 205, .1)  54px,
      rgba(120, 255, 190, 0)   78px,
      rgba(110, 230, 255, 0)  122px,
      rgba(140, 200, 255, .22) 150px,
      rgba(120, 255, 190, 0)  196px,
      rgba(180, 150, 255, .17) 232px,
      rgba(120, 255, 190, 0)  290px);
  animation-name: aurora-drift, aurora-pulse;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
  animation-direction: alternate;
  will-change: transform, filter;
}
@keyframes aurora-drift {
  from { transform: translate3d(-7%, 0, 0); }
  to   { transform: translate3d(7%, 0, 0); }
}
/* The whole curtain surges and fades - an aurora is not a steady light and
   a steady one looks like a sticker on the sky. Kept to a narrow band and a
   long period on purpose: the first version swung from .55 to 1.25 over
   thirteen seconds and on a phone that read as the top of the screen
   flashing rather than as light moving. */
@keyframes aurora-pulse {
  0%   { filter: brightness(.76) saturate(1.1); }
  55%  { filter: brightness(1.1) saturate(1.28); }
  100% { filter: brightness(.86) saturate(1.15); }
}
/* Anyone who asked for less motion gets a still curtain, not none. */
@media (prefers-reduced-motion: reduce) {
  .scene-aurora > i { animation: none !important; }
}

/* Moonlight in the air around the disc. */
.scene-moonglow {
  mix-blend-mode: screen;
  background:
    radial-gradient(circle 70px at var(--at-x) var(--at-y),
                    rgba(214,234,255,.5) 0%, rgba(214,234,255,0) 100%),
    radial-gradient(circle 210px at var(--at-x) var(--at-y),
                    rgba(166,198,246,.28) 0%, rgba(166,198,246,0) 100%);
}

/* The first star: a hard white point with a soft bloom around it, breathing
   slowly. Small on purpose - anything bigger stops being a star and becomes
   a lamp, and the whole of this is that it is the only thing in the sky. */
.scene-evening-star {
  background:
    radial-gradient(circle 2.5px at var(--at-x) var(--at-y),
                    rgba(255,255,255,1) 0%, rgba(255,255,255,0) 100%),
    radial-gradient(circle 9px at var(--at-x) var(--at-y),
                    rgba(226,240,255,.6) 0%, rgba(226,240,255,0) 100%),
    /* the wide halo is what separates it from a passing flake: snow has a
       hard edge at this size and a star does not */
    radial-gradient(circle 34px at var(--at-x) var(--at-y),
                    rgba(178,212,255,.26) 0%, rgba(178,212,255,0) 100%);
  animation: star-breathe 6.5s ease-in-out infinite alternate;
}
@keyframes star-breathe {
  from { filter: brightness(.68); }
  to   { filter: brightness(1.3); }
}

/* --- ground mist ---------------------------------------------------------
 *
 * Lies in the hollows at first light and is gone by morning. Twice the width
 * of the screen so it can slide by one whole tile and start over without a
 * join; `screen` so it whitens what is under it rather than covering it.
 */
.scene-mist {
  width: 200%;
  mix-blend-mode: screen;
  filter: blur(1.1px);
  opacity: 0;
  transition: opacity 6s linear;
  animation-name: mist-drift;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  -webkit-mask-image: linear-gradient(0deg, #000 0%, rgba(0,0,0,.9) 30%,
    rgba(0,0,0,.55) 58%, rgba(0,0,0,.2) 80%, transparent 100%);
  mask-image: linear-gradient(0deg, #000 0%, rgba(0,0,0,.9) 30%,
    rgba(0,0,0,.55) 58%, rgba(0,0,0,.2) 80%, transparent 100%);
}
/* The second bank runs the other way, so the two slide past each other. */
.scene-mist.flip { animation-direction: reverse; }
@keyframes mist-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}
/* Dawn only. It is the one hour that has it, which is most of the point.
   The smoke is the same machinery with its own gate, so it is excluded. */
html[data-phase="dawn"] .scene-mist:not(.scene-smoke) { opacity: var(--bank, .3); }

/* Woodsmoke: warmer, thinner and further off than the mist, and it hangs
   above the ground rather than lying in it. */
   Its own opacity is left to the .scene-when gate above, so there is one
   mechanism deciding when a layer is present rather than two. */
.scene-smoke { filter: blur(2.4px) sepia(.55) saturate(1.3) brightness(.85); }
/* The same plate again at dawn, but this is steam off wet ground rather than
   a fire out past the trees - so it is cool and pale instead of warm. */
.scene-smoke.for-autumn-dawn {
  filter: blur(2.2px) saturate(.55) brightness(1.08);
}

/* --- starlight -----------------------------------------------------------
 * Only after dark, and only a suggestion of it in the last of the dusk and
 * the first of the dawn. The whole field fades with the hour; each star
 * twinkles on its own clock inside that.
 */
.scene-stars { overflow: hidden; opacity: 0; transition: opacity 5s linear; }
html[data-phase="night"] .scene-stars { opacity: 1; }
html[data-phase="dusk"]  .scene-stars { opacity: .3; }
html[data-phase="dawn"]  .scene-stars { opacity: .22; }
/* Bare branches let more of the sky through. */
html[data-season="winter"][data-phase="night"] .scene-stars { opacity: 1; }

.scene-stars i {
  position: absolute;
  border-radius: 50%;
  background: #eaf4ff;
  box-shadow: 0 0 5px rgba(198, 224, 255, .9);
  animation-name: star-twinkle;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
/* The brighter ones throw a cross of light. Two hairlines crossing is what
   the eye already reads as "star", and because they are children they scale
   with the twinkle, so the glint flares and shuts with it. */
.scene-stars i.bright::before,
.scene-stars i.bright::after {
  content: "";
  position: absolute; left: 50%; top: 50%;
  background: linear-gradient(90deg, transparent, rgba(232, 244, 255, .95), transparent);
  transform: translate(-50%, -50%);
}
.scene-stars i.bright::before { width: 900%; height: 1px; }
.scene-stars i.bright::after {
  width: 1px; height: 900%;
  background: linear-gradient(180deg, transparent, rgba(232, 244, 255, .95), transparent);
}

/* --peak is per star, so they are not all the same brightness. The animation
   has to carry it: an animated property overrides anything set on the
   element itself, so a plain opacity would simply be ignored.
 *
 * Not a simple fade up and down. A star catches, dips, catches again and goes
 * out, so the curve has two sharp peaks with a trough between them - that
 * flicker is the difference between twinkling and breathing. */
@keyframes star-twinkle {
  0%   { opacity: calc(var(--peak) * .12); transform: scale(.5); }
  38%  { opacity: calc(var(--peak) * .22); transform: scale(.68); }
  52%  { opacity: var(--peak);             transform: scale(1.18); }
  61%  { opacity: calc(var(--peak) * .35); transform: scale(.8); }
  72%  { opacity: calc(var(--peak) * .92); transform: scale(1.06); }
  84%  { opacity: calc(var(--peak) * .3);  transform: scale(.72); }
  100% { opacity: calc(var(--peak) * .12); transform: scale(.5); }
}

/* --- apparitions ---------------------------------------------------------
 *
 * Each is a single run, started by js/scene.js adding `run` and ended by it
 * taking the class off again. They are not on a loop of their own: the script
 * decides which one appears and how long the grove waits between them.
 */
.scene-spirit {
  opacity: 0;
  mix-blend-mode: screen;
  filter: blur(1.6px);
  animation-timing-function: ease-in-out;
  animation-iteration-count: 1;
  animation-fill-mode: both;
}
html:not([data-phase="night"]) .scene-spirit { display: none; }

/* The golem is matter, not light: it covers what is behind it instead of
   adding to it, and it takes the night's dimming like the scenery does. */
.scene-spirit.solid {
  mix-blend-mode: normal;
  filter: blur(.7px) brightness(.72) saturate(.9);
}

.scene-spirit.pass.run { animation-name: spirit-pass; }
.scene-spirit.fade.run { animation-name: spirit-fade; }
.scene-spirit.appear.run { animation-name: spirit-appear; }

/* One smooth fade in, a hold, and one fade out - no blink. The blink in
   spirit-fade is for eyes; on a whole figure it read as a stutter. */
@keyframes spirit-appear {
  0%   { opacity: 0; }
  30%  { opacity: var(--peak); }
  75%  { opacity: var(--peak); }
  100% { opacity: 0; }
}

/* In, held, and out again, with nothing moving.
 *
 * An earlier version had the eyes narrow to a slit as though a head were
 * turning away. It was a nice idea and it looked wrong - a squashed image is
 * a squashed image, not a head in three dimensions. One blink is the only
 * movement that survives, because eyes that never blink are a painting. */
@keyframes spirit-fade {
  0%   { opacity: 0; }
  14%  { opacity: var(--peak); }
  30%  { opacity: calc(var(--peak) * .25); }   /* a blink */
  36%  { opacity: var(--peak); }
  78%  { opacity: var(--peak); }
  100% { opacity: 0; }
}
/* It grows out of the ground, so it scales from its feet, not its middle. */
.scene-spirit.rise { transform-origin: 50% 100%; }
.scene-spirit.rise.run { animation-name: spirit-rise; }

/* Fades in, holds, drifts a little, and is gone. It does not walk off:
   something that leaves by moving is an animal, something that stops being
   there is a ghost. */
@keyframes spirit-pass {
  0%   { opacity: 0; transform: translate3d(0, 0, 0); }
  18%  { opacity: calc(var(--peak) * .45); }
  34%  { opacity: var(--peak); }
  64%  { opacity: calc(var(--peak) * .88); }
  86%  { opacity: calc(var(--peak) * .3); }
  100% { opacity: 0; transform: translate3d(20px, -12px, 0); }
}

/* Comes up out of the ground, stands, then goes down faster than it came up
   and settles slightly wider as it falls apart - which is what reads as
   crumbling rather than sinking. */
@keyframes spirit-rise {
  0%   { opacity: 0; transform: translate3d(0, 44px, 0) scaleY(.9); }
  22%  { opacity: calc(var(--peak) * .7); }
  38%  { opacity: var(--peak); transform: translate3d(0, 0, 0) scaleY(1); }
  70%  { opacity: var(--peak); transform: translate3d(0, 0, 0) scaleY(1); }
  84%  { opacity: calc(var(--peak) * .72); transform: translate3d(0, 14px, 0) scaleY(.96); }
  100% { opacity: 0; transform: translate3d(0, 40px, 0) scale(1.04, .86); }
}

/* --- things standing in one place ----------------------------------------
 * A flame never holds still and a sleeping animal never holds quite still
 * either, so the two that ought to be alive are given a small movement each.
 * Everything else here is deliberately motionless.
 */

/* The flicker works in brightness, not opacity.
 *
 * An animation overrides whatever the element's own rules say, so a flicker
 * written in opacity took the property away from the gate that decides which
 * season and hour the thing exists in - the candle and the lantern were
 * burning in midsummer daylight. Brightness is free here, because `lit`
 * already keeps these out of the seasonal grading. */
.flickers {
  animation: perch-flicker 2.9s linear infinite;
  transform-origin: 50% 100%;
}
/* Uneven stops, the same reasoning as any flame: evenly spaced ones throb.
 *
 * The range is deliberately narrow and nothing changes size. An earlier
 * version swung the brightness by a quarter and squashed the candle as it
 * went, which made the whole object pulse - and an object that pulses reads
 * as a cartoon. What flickers on a real candle is the light, not the wax. */
@keyframes perch-flicker {
  0%   { filter: brightness(1); }
  9%   { filter: brightness(.955); }
  17%  { filter: brightness(1.02); }
  26%  { filter: brightness(.975); }
  38%  { filter: brightness(.94); }
  47%  { filter: brightness(1.035); }
  61%  { filter: brightness(.965); }
  73%  { filter: brightness(1.01); }
  84%  { filter: brightness(.95); }
  100% { filter: brightness(1); }
}

/* Breathing: slow, and only a fraction of a percent, swelling from where the
   animal meets the ground so it does not appear to hover. */
.breathes {
  animation: perch-breathe 4.6s ease-in-out infinite alternate;
  transform-origin: 50% 100%;
}
@keyframes perch-breathe {
  from { transform: scale(1, 1); }
  to   { transform: scale(1.012, 1.025); }
}

/* Still, but still present: the gate decides whether they are there at all,
   so this must not set an opacity of its own. */
@media (prefers-reduced-motion: reduce) {
  .flickers, .breathes { animation: none !important; }
}

/* --- the review tag ------------------------------------------------------
 * Shown only while the scene is pinned to a fixed hour and season. Sits along
 * the bottom, out of the scenery being looked at, and pushes the toast up so
 * the two never land on top of each other.
 */
.scene-forced {
  position: fixed; left: 50%; bottom: calc(8px + env(safe-area-inset-bottom));
  transform: translateX(-50%);
  z-index: 60; pointer-events: none;
  padding: 4px 12px; border-radius: 999px;
  background: rgba(10, 14, 10, .82);
  border: 1px solid rgba(190, 210, 170, .28);
  color: #cfe0b8; font-size: .72rem; letter-spacing: .08em;
  text-transform: uppercase; font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
body.is-pinned .toast { bottom: calc(46px + env(safe-area-inset-bottom)); }
@media print { .scene-forced { display: none !important; } }

/* --- particles ---------------------------------------------------------- */
.scene-fx { position: absolute; inset: 0; width: 100%; height: 100%; }

/* The simple theme has no background at all. */
html[data-theme-mode="simple"] .mood { display: none; }

@media (prefers-reduced-motion: reduce) {
  .scene-layer, .scene-canopy { animation: none !important; }
}


/* --- Wizard ---------------------------------------------------------------
 *
 * The arcane field. Each plate is a square larger than the screen, centred,
 * turning slowly, and leaning toward the pointer by --pull-x/--pull-y (set
 * by js/scenes/wizard.js) times its --depth. The turn and the lean use the
 * separate `rotate` and `translate` properties so the animation and the
 * script never fight over one transform.
 */
.scene-layer.scene-spin {
  inset: auto;
  left: 50%;
  top: 50%;
  width: var(--size);
  height: var(--size);
  translate: calc(-50% + var(--pull-x, 0px) * var(--depth, 1))
             calc(-50% + var(--pull-y, 0px) * var(--depth, 1));
  background-position: center;
  background-size: contain;
  background-repeat: no-repeat;
  image-rendering: auto;
  mix-blend-mode: screen;
  animation: arcane-spin 95s linear infinite;
  transition: translate 1.6s cubic-bezier(.2, .7, .3, 1);
}
.scene-spin.reverse { animation-direction: reverse; }
@keyframes arcane-spin { to { rotate: 360deg; } }

/* A surge: the vortex again in another school's colour, fading in and out
   over the one beneath it. */
.scene-layer.scene-surge {
  opacity: 0;
  filter: hue-rotate(var(--surge-hue, 0deg)) saturate(var(--surge-sat, 1))
          brightness(var(--surge-bright, 1));
  transition: opacity 1.2s ease-in-out, translate 1.6s cubic-bezier(.2, .7, .3, 1);
}
.scene-layer.scene-surge.on { opacity: .75; }

/* A held charge brightens the field; --charge (0 to 1) is set on the scene
   box by wizard.js while a finger is held down on the background. */
.scene-layer.scene-spin:not(.scene-surge) {
  filter: brightness(calc(1 + var(--charge, 0) * .8))
          saturate(calc(1 + var(--charge, 0) * .35));
}

/* The rift: the centre of the field tearing open onto another plane for a
   few seconds. It opens as a thin vertical slit, widens into an eye, holds,
   and closes the same way. It leans with the vortex it sits in. */
.scene-layer.scene-rift {
  inset: auto;
  left: 50%;
  top: 50%;
  width: min(64vmin, 470px);
  height: min(64vmin, 470px);
  translate: calc(-50% + var(--pull-x, 0px)) calc(-50% + var(--pull-y, 0px));
  transition: translate 1.6s cubic-bezier(.2, .7, .3, 1);
  image-rendering: auto;
  opacity: 0;
  scale: 0 0;
}
/* the other plane, turning slowly the other way to the vortex */
.scene-rift::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background:
    url("../art/arcane-rift.webp") center / 125% no-repeat,
    radial-gradient(closest-side, #03121a 55%, #0a0618 100%);
  -webkit-mask-image: radial-gradient(closest-side, #000 70%, transparent 82%);
  mask-image: radial-gradient(closest-side, #000 70%, transparent 82%);
  filter: brightness(1.45) saturate(1.2);
  animation: arcane-spin 120s linear infinite reverse;
}
/* the torn edge, burning white-violet */
.scene-rift::after {
  content: "";
  position: absolute;
  inset: -4%;
  border-radius: 50%;
  background: radial-gradient(closest-side,
    transparent 58%, rgba(200, 150, 255, .5) 64%, #fff 68.5%,
    rgba(214, 160, 255, .85) 72%, rgba(150, 80, 255, .35) 79%, transparent 90%);
  mix-blend-mode: screen;
  animation: rift-flicker .35s steps(2) infinite alternate;
}
.scene-layer.scene-rift.open {
  animation: rift-open 9s cubic-bezier(.3, .6, .3, 1) both;
}
@keyframes rift-open {
  0%        { opacity: 0; scale: .02 0; }
  7%        { opacity: 1; scale: .03 .85; }
  24%       { scale: .95 1; }
  32%, 76%  { opacity: 1; scale: 1 1; }
  90%       { opacity: 1; scale: .03 .85; }
  100%      { opacity: 0; scale: .02 0; }
}
@keyframes rift-flicker {
  from { opacity: .85; }
  to   { opacity: 1; }
}

/* --- class scene pieces ----------------------------------------------------
 *
 * Each piece places itself; nothing is inherited from the edge-anchored
 * plates. `inset: auto` first, so a piece that only states a width and a
 * corner is not also stretched across the screen.
 */
.scene-layer.scene-piece { inset: auto; image-rendering: auto; }

/* --- Evoker ----------------------------------------------------------------
 *
 * The day's sky, the day's element rising from the caster's hands, and the
 * caster. evoker.js sets data-element on the scene box; everything that
 * differs by day hangs off that.
 */
.mood {
  --caster-w: min(250px, 46vw);
}
.mood[data-element="death"]     { --ev-1: #10241c; --ev-2: #07130e; --ev-3: #020605; --ev-glow: rgba(120, 255, 160, .6); }
.mood[data-element="ice"]       { --ev-1: #173247; --ev-2: #0a1a28; --ev-3: #03080e; --ev-glow: rgba(170, 220, 255, .7); }
.mood[data-element="flame"]     { --ev-1: #4a1206; --ev-2: #220805; --ev-3: #0b0202; --ev-glow: rgba(255, 110, 50, .75); }
.mood[data-element="storm"]     { --ev-1: #23383a; --ev-2: #111e20; --ev-3: #04090a; --ev-glow: rgba(180, 240, 230, .55); }
.mood[data-element="binding"]   { --ev-1: #221a44; --ev-2: #100d24; --ev-3: #04030b; --ev-glow: rgba(255, 210, 130, .65); }
.mood[data-element="sorcery"]   { --ev-1: #1b2350; --ev-2: #0d1130; --ev-3: #04050f; --ev-glow: rgba(255, 215, 140, .65); }
.mood[data-element="force"]     { --ev-1: #221650; --ev-2: #0e0a28; --ev-3: #04030c; --ev-glow: rgba(190, 150, 255, .75); }

.scene-layer.evoke-tint {
  inset: 0;
  background: linear-gradient(180deg, var(--ev-1, #120c18), var(--ev-2, #0a0710) 55%, var(--ev-3, #030205));
}
/* The element's base is at the caster's raised hands, about one caster
   width above the bottom of the screen; the plate's own foot is faded out
   so it seems to come from the hands rather than from a mound. */
.scene-layer.evoke-element {
  left: 50%;
  bottom: calc(var(--caster-w) * .9);
  /* no taller than the sky above the caster, so the bloom at its top is
     never cut off on a short screen */
  width: min(820px, 140vw, calc((100lvh - var(--caster-w) * .9) * .78));
  aspect-ratio: 3 / 4;
  translate: -50% 0;
  background-position: center bottom;
  background-size: contain;
  background-repeat: no-repeat;
  -webkit-mask-image: linear-gradient(to top, transparent 0, #000 16%);
  mask-image: linear-gradient(to top, transparent 0, #000 16%);
  mix-blend-mode: screen;
  transform-origin: 50% 100%;
  animation: evoke-breathe 7s ease-in-out infinite alternate;
  transition: opacity 1.5s ease;
}
@keyframes evoke-breathe {
  from { scale: 1;    filter: brightness(.85); }
  to   { scale: 1.04; filter: brightness(1.1); }
}
.scene-layer.evoke-element.cast { animation: evoke-cast 1.8s ease-out; }
@keyframes evoke-cast {
  0%   { scale: 1;    filter: brightness(1); }
  15%  { scale: 1.09; filter: brightness(1.9) saturate(1.2); }
  100% { scale: 1;    filter: brightness(.9); }
}
/* the caster, a dark figure on the crag, edged with the element's light */
.scene-layer.evoke-caster {
  left: 50%;
  bottom: -2px;
  width: var(--caster-w);
  aspect-ratio: 3 / 4;
  translate: -50% 0;
  background: var(--plate) center bottom / contain no-repeat;
  filter: brightness(.8) drop-shadow(0 0 12px var(--ev-glow, transparent));
}
/* A flash across the sky - the storm's, and each day's biggest moments */
.scene-layer.evoke-flash {
  inset: 0;
  opacity: 0;
  mix-blend-mode: screen;
  background: radial-gradient(120% 80% at 50% 25%,
    rgba(236, 222, 255, .6), rgba(190, 160, 255, .18) 55%, transparent 80%);
}
.scene-layer.evoke-flash.on { animation: bolt-flash .7s linear; }
@keyframes bolt-flash {
  0%   { opacity: 0; }
  6%   { opacity: 1; }
  14%  { opacity: .08; }
  24%  { opacity: .8; }
  100% { opacity: 0; }
}

/* --- Battlemage ----------------------------------------------------------
 *
 * The swordswoman is layers of one plate: dim and grey; the same in full
 * colour, shown up to --fill from the hem upward; her runes alone, cut from
 * the plate (battlemage-runes.webp) and painted --rune, with a soft glow
 * behind them; and a hot line at the edge of what is burned. When she is
 * finished, battlemage.js sets --rune to her school's colour and the ground
 * glow's (--bm-glow).
 */
@property --fill {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}
@property --rune {
  syntax: "<color>";
  inherits: true;
  initial-value: #b98bff;
}
.scene-layer.bm-piece {
  animation: bm-hover 9s ease-in-out infinite alternate;
}
.bm-piece > div {
  position: absolute;
  inset: 0;
  background: var(--plate) center bottom / contain no-repeat;
  transition: --fill 1.6s cubic-bezier(.3, .7, .3, 1), --rune 2.5s ease;
}
.bm-piece > .cold { filter: grayscale(1) brightness(.62) contrast(1.15); }
.bm-piece > .hot {
  -webkit-mask-image: linear-gradient(var(--dir),
    #000 calc(var(--fill) * 108% - 8%), transparent calc(var(--fill) * 108%));
  mask-image: linear-gradient(var(--dir),
    #000 calc(var(--fill) * 108% - 8%), transparent calc(var(--fill) * 108%));
}
/* the runes: a colour, cut to their shapes and to what is burned so far */
.bm-piece > .runes,
.bm-piece > .glow {
  background: var(--rune);
  -webkit-mask-image:
    linear-gradient(var(--dir), #000 calc(var(--fill) * 108% - 8%), transparent calc(var(--fill) * 108%)),
    url("../art/battlemage-runes.webp");
  mask-image:
    linear-gradient(var(--dir), #000 calc(var(--fill) * 108% - 8%), transparent calc(var(--fill) * 108%)),
    url("../art/battlemage-runes.webp");
  -webkit-mask-size: 100% 100%, contain;
  mask-size: 100% 100%, contain;
  -webkit-mask-position: 0 0, center bottom;
  mask-position: 0 0, center bottom;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
.bm-piece > .glow {
  -webkit-mask-image:
    linear-gradient(var(--dir), #000 calc(var(--fill) * 108% - 8%), transparent calc(var(--fill) * 108%)),
    url("../art/battlemage-runes-glow.webp");
  mask-image:
    linear-gradient(var(--dir), #000 calc(var(--fill) * 108% - 8%), transparent calc(var(--fill) * 108%)),
    url("../art/battlemage-runes-glow.webp");
  mix-blend-mode: screen;
  opacity: .7;
}
/* a touch of white in the burning, so they read as light, not paint */
.bm-piece > .runes { filter: brightness(1.35); }
.bm-piece > .front {
  background: linear-gradient(var(--dir),
    transparent calc(var(--fill) * 108% - 12%),
    rgba(255, 190, 110, .95) calc(var(--fill) * 108% - 5%),
    rgba(255, 120, 60, .6) calc(var(--fill) * 108% - 2%),
    transparent calc(var(--fill) * 108% + 1%));
  -webkit-mask: var(--plate) center bottom / contain no-repeat;
  mask: var(--plate) center bottom / contain no-repeat;
  mix-blend-mode: screen;
  transition: --fill 1.6s cubic-bezier(.3, .7, .3, 1), opacity .8s;
}
.bm-piece.empty > .front,
.bm-piece.full > .front { opacity: 0; }
.bm-piece.struck > .front { animation: bm-strike .5s ease-out; }
@keyframes bm-strike {
  0%   { filter: brightness(2.4); }
  100% { filter: brightness(1); }
}
/* centred, standing on the bottom of the screen */
.scene-layer.bm-woman {
  left: 50%;
  bottom: 0;
  height: min(94vh, 124vw);
  aspect-ratio: 3 / 4;
  translate: -50% 0;
  --dir: to top;
}
@keyframes bm-hover {
  from { translate: -50% 0; }
  to   { translate: -50% -8px; }
}
/* the glow at her feet, rising with what you have spent */
.scene-layer.bm-forge {
  left: 0; right: 0; bottom: 0;
  height: 50vh;
  mix-blend-mode: screen;
  background: radial-gradient(60% 90% at 50% 100%,
    var(--bm-glow, rgba(185, 139, 255, .5)), transparent 72%);
  opacity: calc(.15 + var(--heat, 0) * .75);
  transition: opacity 1.6s ease;
  animation: forge-breathe 5s ease-in-out infinite alternate;
}
@keyframes forge-breathe {
  from { filter: brightness(.8); }
  to   { filter: brightness(1.2); }
}
/* a finished list: the runes breathe light, in the school's colour */
.mood.forged .bm-piece > .glow { animation: bm-temper 3.5s ease-in-out infinite alternate; }
@keyframes bm-temper {
  from { opacity: .7; }
  to   { opacity: 1; }
}
/* The ground for the school: nothing until she is finished; then a sickly
   green mist that rises for Death, fire along the bottom for Flame, and a
   soft light for the rest. Quieter below level 6 (no .grand). */
.scene-layer.bm-ground {
  left: -10%; right: -10%; bottom: 0;
  height: 45vh;
  opacity: 0;
  transition: opacity 2.5s ease;
  mix-blend-mode: screen;
}
.mood.forged .scene-layer.bm-ground { opacity: .55; }
.mood.forged.grand .scene-layer.bm-ground { opacity: 1; }
.mood[data-school="death"] .scene-layer.bm-ground {
  height: 70vh;
  background:
    radial-gradient(45% 55% at 20% 100%, rgba(110, 255, 150, .35), transparent 70%),
    radial-gradient(50% 60% at 80% 100%, rgba(90, 230, 130, .3), transparent 70%),
    linear-gradient(to top, rgba(70, 200, 110, .35), rgba(40, 120, 70, .12) 45%, transparent 80%);
  animation: bm-mist-rise 9s ease-in-out infinite alternate;
}
@keyframes bm-mist-rise {
  from { translate: -3% 6%; filter: blur(2px); }
  to   { translate: 3% -4%; filter: blur(5px); }
}
.mood[data-school="flame"] .scene-layer.bm-ground {
  height: 32vh;
  background:
    radial-gradient(30% 70% at 15% 100%, rgba(255, 200, 90, .7), transparent 70%),
    radial-gradient(28% 80% at 45% 100%, rgba(255, 120, 40, .7), transparent 70%),
    radial-gradient(32% 75% at 75% 100%, rgba(255, 170, 60, .7), transparent 70%),
    radial-gradient(30% 65% at 100% 100%, rgba(255, 90, 30, .6), transparent 70%),
    linear-gradient(to top, rgba(255, 110, 40, .55), transparent 70%);
  animation: bm-fire-flicker .45s steps(3) infinite alternate, bm-fire-sway 3s ease-in-out infinite alternate;
}
@keyframes bm-fire-flicker {
  from { filter: brightness(.9) saturate(1.1); }
  to   { filter: brightness(1.25) saturate(1.3); }
}
@keyframes bm-fire-sway {
  from { transform: scaleY(.9) skewX(-2deg); transform-origin: 50% 100%; }
  to   { transform: scaleY(1.12) skewX(2deg); transform-origin: 50% 100%; }
}
.mood[data-school="sorcery"] .scene-layer.bm-ground,
.mood[data-school="command"] .scene-layer.bm-ground,
.mood[data-school="spirit"] .scene-layer.bm-ground,
.mood[data-school="arcane"] .scene-layer.bm-ground {
  background: radial-gradient(55% 80% at 50% 100%, var(--bm-glow), transparent 70%);
}
/* Command: light standing up behind her */
.mood[data-school="command"].grand .scene-layer.bm-ground {
  height: 100vh;
  background:
    repeating-conic-gradient(from 0deg at 50% 100%, rgba(255, 215, 130, .16) 0 4deg, transparent 4deg 12deg),
    radial-gradient(55% 80% at 50% 100%, var(--bm-glow), transparent 70%);
  -webkit-mask-image: linear-gradient(to top, #000 20%, transparent 90%);
  mask-image: linear-gradient(to top, #000 20%, transparent 90%);
}
/* --- Battlemage: standing out of the dark ---
   A light behind her, and - over the scrim, which is dark enough down the
   middle of a phone to lose her entirely - an edge of light round her
   (battlemage-rim.webp) and her eyes under the hood. All in the colour of
   her runes: violet while they burn in, her school's once she is finished. */
.bm-piece > .halo {
  inset: -4% -45% 18%;
  background: radial-gradient(42% 46% at 50% 42%,
    color-mix(in srgb, var(--rune) 55%, transparent), transparent 72%);
  opacity: calc(.35 + var(--heat, 0) * .3);
  transition: opacity 1.6s ease, --rune 2.5s ease;
}
.mood.forged .bm-piece > .halo { opacity: .85; }
.scene-layer.bm-rim {
  left: 50%;
  bottom: 0;
  height: min(94vh, 124vw);
  aspect-ratio: 3 / 4;
  translate: -50% 0;
  pointer-events: none;
  animation: bm-hover 9s ease-in-out infinite alternate;
  filter: drop-shadow(0 0 6px var(--rune));
  opacity: calc(.2 + var(--heat, 0) * .2);
  transition: opacity 1.6s ease;
}
.mood.forged .scene-layer.bm-rim { opacity: .5; }
.bm-rim > .edge {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--rune) 60%, white);
  -webkit-mask: var(--plate) center bottom / contain no-repeat;
  mask: var(--plate) center bottom / contain no-repeat;
  transition: --rune 2.5s ease;
}
/* her eyes, under the hood, lit once she is finished. Her face is not in
   the middle of the plate - it sits at x 0.509 - so these are hung off that,
   not off 50%, or they read as looking past you. */
.bm-rim > .eyes {
  position: absolute;
  left: 47.1%;
  top: 14.8%;
  width: 7.6%;
  height: 1.2%;
  translate: 0 -50%;
  background:
    radial-gradient(closest-side, #fff, var(--rune) 45%, transparent) left / 22% 100% no-repeat,
    radial-gradient(closest-side, #fff, var(--rune) 45%, transparent) right / 22% 100% no-repeat;
  opacity: 0;
  transition: opacity 1.5s ease 1.2s;
}
.mood.forged .bm-rim > .eyes {
  opacity: 1;
  animation: forge-breathe 2.6s ease-in-out infinite alternate;
}
/* Flame: the edge of her burns and flickers */
.mood.forged[data-school="flame"] .scene-layer.bm-rim {
  filter: drop-shadow(0 -3px 6px #ff7a2a) drop-shadow(0 -10px 16px rgba(255, 70, 20, .8));
  animation: bm-hover 9s ease-in-out infinite alternate, bm-aura-flicker .35s steps(2) infinite alternate;
}
.mood.forged[data-school="flame"] .bm-rim > .edge { background: #ffd08a; }
@keyframes bm-aura-flicker {
  from { opacity: .6; }
  to   { opacity: .9; }
}
/* A phone's screen is small and often bright: more light behind and round
   her, and her robe less dark before the runes burn in. */
@media (max-width: 619px) {
  .bm-piece > .halo { opacity: calc(.6 + var(--heat, 0) * .3); }
  .scene-layer.bm-rim { opacity: calc(.55 + var(--heat, 0) * .25); }
  .mood.forged .scene-layer.bm-rim { opacity: .9; }
  .bm-piece > .cold { filter: grayscale(1) brightness(.85) contrast(1.1); }
}

/* --- Battlemage: the finish ---
   Everything here is nothing until .forged, and the larger parts wait for
   .grand (level 6). */
.scene-layer.bm-sigil,
.scene-layer.bm-pillar,
.scene-layer.bm-wraiths { --bm-h: min(94vh, 124vw); }

/* the rune circle behind her, opening when she is finished and turning */
.scene-layer.bm-sigil {
  left: 50%;
  width: min(calc(var(--bm-h) * .95), 112vw);
  aspect-ratio: 1;
  bottom: calc(var(--bm-h) * .56 - min(calc(var(--bm-h) * .95), 112vw) / 2);
  translate: -50% 0;
  opacity: 0;
  scale: .2;
  filter: drop-shadow(0 0 6px var(--rune)) drop-shadow(0 0 18px var(--rune));
  transition: opacity 1.2s ease, scale 1.6s cubic-bezier(.2, 1.4, .4, 1);
}
.mood.forged .scene-layer.bm-sigil { opacity: .55; scale: .8; }
.mood.forged.grand .scene-layer.bm-sigil { opacity: .8; scale: 1; }
.bm-sigil > div {
  position: absolute;
  inset: 0;
  background: var(--rune);
  -webkit-mask: var(--plate) center / contain no-repeat;
  mask: var(--plate) center / contain no-repeat;
}
.bm-sigil > .ring { animation: bm-spin 70s linear infinite; }
.bm-sigil > .outer {
  inset: -24%;
  opacity: 0;
  animation: bm-spin 110s linear infinite reverse;
  transition: opacity 2s ease 1s;
}
.mood.forged.grand .bm-sigil > .outer { opacity: .4; }
.bm-sigil > .core {
  inset: 30%;
  -webkit-mask: none;
  mask: none;
  border-radius: 50%;
  background: radial-gradient(closest-side, color-mix(in srgb, var(--rune) 50%, white), transparent);
  opacity: .35;
  animation: forge-breathe 3s ease-in-out infinite alternate;
}
@keyframes bm-spin {
  to { rotate: 360deg; }
}

/* a column of light standing up behind her */
.scene-layer.bm-pillar {
  left: 50%;
  top: 0; bottom: 0;
  width: calc(var(--bm-h) * .42);
  translate: -50% 0;
  mix-blend-mode: screen;
  background: linear-gradient(to right, transparent,
    color-mix(in srgb, var(--rune) 35%, transparent) 30%,
    color-mix(in srgb, var(--rune) 40%, white) 50%,
    color-mix(in srgb, var(--rune) 35%, transparent) 70%, transparent);
  -webkit-mask-image: linear-gradient(to top, #000 30%, transparent);
  mask-image: linear-gradient(to top, #000 30%, transparent);
  transform-origin: 50% 100%;
  opacity: 0;
  scale: 1 0;
  transition: opacity 1.5s ease, scale 1.8s cubic-bezier(.3, .8, .3, 1);
}
.mood.forged.grand .scene-layer.bm-pillar {
  opacity: .5;
  scale: 1 1;
  animation: bm-pillar-breathe 4s ease-in-out infinite alternate;
}
@keyframes bm-pillar-breathe {
  from { filter: brightness(.8); width: calc(var(--bm-h) * .38); }
  to   { filter: brightness(1.25); width: calc(var(--bm-h) * .46); }
}

/* Spirit: shafts of light falling on her */
.scene-layer.bm-rays {
  inset: 0;
  mix-blend-mode: screen;
  background: repeating-conic-gradient(from 160deg at 50% -10%,
    rgba(255, 240, 200, .14) 0 3deg, transparent 3deg 9deg, rgba(255, 240, 200, .08) 9deg 11deg, transparent 11deg 16deg);
  -webkit-mask-image: linear-gradient(to bottom, #000, transparent 85%);
  mask-image: linear-gradient(to bottom, #000, transparent 85%);
  opacity: 0;
  transition: opacity 2.5s ease;
}
.mood.forged[data-school="spirit"] .scene-layer.bm-rays {
  opacity: .6;
  animation: bm-rays-sway 12s ease-in-out infinite alternate;
}
.mood.forged.grand[data-school="spirit"] .scene-layer.bm-rays { opacity: 1; }
@keyframes bm-rays-sway {
  from { transform: rotate(-3deg); transform-origin: 50% 0; }
  to   { transform: rotate(3deg); transform-origin: 50% 0; }
}

/* Death: wraiths rising either side of her and fading */
.scene-layer.bm-wraiths { inset: 0; }
.bm-wraiths > div {
  position: absolute;
  bottom: 4vh;
  height: calc(var(--bm-h) * .62);
  aspect-ratio: 3 / 4;
  background: var(--plate) center bottom / contain no-repeat;
  filter: brightness(1.6) sepia(1) hue-rotate(75deg) saturate(2.2);
  mix-blend-mode: screen;
  opacity: 0;
}
.bm-wraiths > .left { left: calc(50% - var(--bm-h) * .66); }
.bm-wraiths > .right { right: calc(50% - var(--bm-h) * .66); scale: -1 1; }
.mood.forged.grand[data-school="death"] .bm-wraiths > div { animation: bm-wraith 10s ease-in-out infinite; }
.mood.forged.grand[data-school="death"] .bm-wraiths > .right { animation-delay: 5s; }
@keyframes bm-wraith {
  0%   { opacity: 0;   translate: 0 8vh; }
  35%  { opacity: .45; }
  65%  { opacity: .4; }
  100% { opacity: 0;   translate: 0 -8vh; }
}

/* Sorcery: her mirror images, either side of her */
.bm-piece > .echo-l,
.bm-piece > .echo-r {
  filter: grayscale(1) brightness(1.3) sepia(1) hue-rotate(190deg) saturate(3);
  mix-blend-mode: screen;
  opacity: 0;
  transition: opacity 1.5s ease;
}
.bm-piece > .echo-l { --side: -1; }
.bm-piece > .echo-r { --side: 1; }
.mood.forged.grand[data-school="sorcery"] .bm-piece > .echo-l,
.mood.forged.grand[data-school="sorcery"] .bm-piece > .echo-r {
  opacity: .3;
  animation: bm-echo 6s ease-in-out infinite alternate;
}
.mood.forged.grand[data-school="sorcery"] .bm-piece > .echo-r { animation-delay: -3s; }
.mood.forged.grand[data-school="sorcery"] .bm-piece.blink > .echo-l,
.mood.forged.grand[data-school="sorcery"] .bm-piece.blink > .echo-r {
  animation: bm-echo 6s ease-in-out infinite alternate, bm-blink 1.2s ease-in-out;
}
@keyframes bm-echo {
  from { translate: calc(var(--side) * 26%) 1%; }
  to   { translate: calc(var(--side) * 31%) -1%; }
}
@keyframes bm-blink {
  0%, 100% { opacity: .3; }
  40%, 60% { opacity: 0; }
}

/* Command: a halo of gold above her hood */
.bm-piece > .crown {
  inset: auto;
  left: 50%;
  top: -1%;
  width: 24%;
  aspect-ratio: 3;
  translate: -50% 0;
  background: none;
  border-radius: 50%;
  border: 2px solid rgba(255, 226, 150, .95);
  box-shadow: 0 0 10px 2px rgba(255, 200, 90, .8), inset 0 0 10px 2px rgba(255, 200, 90, .6);
  opacity: 0;
  transition: opacity 2s ease 1s;
}
.mood.forged[data-school="command"] .bm-piece > .crown {
  opacity: .9;
  animation: forge-breathe 3s ease-in-out infinite alternate;
}

/* runes lifting off her: one cell of the glyph sheet each */
.scene-layer.bm-glyphs {
  inset: 0;
  pointer-events: none;
  filter: drop-shadow(0 0 5px var(--rune));
}
.bm-glyphs > i {
  position: absolute;
  width: var(--size);
  height: var(--size);
  translate: -50% -50%;
  background: color-mix(in srgb, var(--rune) 75%, white);
  -webkit-mask: url("../art/battlemage-glyphs.webp") no-repeat;
  mask: url("../art/battlemage-glyphs.webp") no-repeat;
  -webkit-mask-size: 1000% 100%;
  mask-size: 1000% 100%;
  -webkit-mask-position: calc(var(--cell) / 9 * 100%) 0;
  mask-position: calc(var(--cell) / 9 * 100%) 0;
  opacity: 0;
  animation: bm-glyph 4s ease-out forwards;
}
@keyframes bm-glyph {
  0%   { opacity: 0; transform: scale(.5); }
  15%  { opacity: 1; }
  60%  { opacity: .8; }
  100% { opacity: 0; transform: translate(var(--dx), var(--dy)) rotate(var(--spin)) scale(1.15); }
}

/* the moment she is finished: a flash from her, and the runes flare white */
.scene-layer.bm-flash {
  inset: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  background: radial-gradient(circle at 50% 55%, #fff, color-mix(in srgb, var(--rune) 70%, transparent) 25%, transparent 65%);
  opacity: 0;
}
.mood.climax .scene-layer.bm-flash { animation: bm-flash 1.6s ease-out; }
@keyframes bm-flash {
  0%   { opacity: 0; }
  8%   { opacity: .9; }
  100% { opacity: 0; }
}
.mood.climax .bm-piece > .runes { animation: bm-flare 3s ease-out; }
@keyframes bm-flare {
  0%   { filter: brightness(1.35); }
  10%  { filter: brightness(5) saturate(.3); }
  100% { filter: brightness(1.35); }
}

/* --- Warlock ---------------------------------------------------------------
 *
 * The eye: a dark almond (the ball), the iris inside it, and a burning rim.
 * warlock.js sets --open (0 shut, 1 open) and --dur (how fast it gets
 * there) on the scene box, and --look-x/--look-y for where the iris points.
 * The ball is clipped to an ellipse whose height is --open; the rim is the
 * same ellipse, squashed to match, so the lids close over the iris rather
 * than the iris being squashed.
 */
.scene-layer.wl-eye {
  left: 50%;
  top: 46%;
  width: min(640px, 96vw);
  aspect-ratio: 2 / 1;
  translate: -50% -50%;
}
.wl-eye > .ball {
  position: absolute;
  inset: 0;
  clip-path: ellipse(50% calc(var(--open, 0) * 50%) at 50% 50%);
  transition: clip-path var(--dur, 6s) cubic-bezier(.45, 0, .2, 1);
  background: radial-gradient(ellipse at 50% 50%, #6a0d06 0%, #2c0402 52%, #0a0000 100%);
}
.wl-eye > .ball::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  height: 150%;
  aspect-ratio: 1;
  translate: calc(-50% + var(--look-x, 0px)) calc(-50% + var(--look-y, 0px));
  transition: translate 1s cubic-bezier(.3, .7, .3, 1);
  background:
    var(--plate) center / contain no-repeat,
    radial-gradient(circle, #000 0 22%, #140000 28%, transparent 32%);
  animation: iris-burn 4s ease-in-out infinite alternate;
}
@keyframes iris-burn {
  from { filter: brightness(.9); }
  to   { filter: brightness(1.25); }
}
.wl-eye > .rim {
  position: absolute;
  inset: 0;
  border-radius: 50%;
  transform: scaleY(max(var(--open, 0), .015));
  opacity: calc(.3 + var(--open, 0) * .7);
  transition: transform var(--dur, 6s) cubic-bezier(.45, 0, .2, 1),
              opacity var(--dur, 6s);
  box-shadow: 0 0 28px 6px rgba(255, 70, 20, .55),
              inset 0 0 36px 12px rgba(0, 0, 0, .95);
}
/* the light it throws into the dark around it */
.wl-eye > .glow {
  position: absolute;
  inset: -40% -20%;
  background: radial-gradient(closest-side, rgba(255, 70, 20, .35), transparent);
  mix-blend-mode: screen;
  opacity: var(--open, 0);
  transition: opacity var(--dur, 6s);
}
/* the tendrils creep in while it watches, and draw back when you move */
.scene-layer[data-part^="wl-tendrils"] {
  transition: translate var(--creep-dur, 1s) ease-in-out;
  animation: tendril-writhe 8s ease-in-out infinite alternate;
  transform-origin: 0 50%;
}
.scene-layer[data-part="wl-tendrils-left"]  { translate: calc(var(--creep, 0) * 9vw - 4vw) 0; }
.scene-layer[data-part="wl-tendrils-right"] {
  translate: calc(var(--creep, 0) * -9vw + 4vw) 0;
  transform-origin: 100% 50%;
  animation-delay: -3s;
}
@keyframes tendril-writhe {
  from { transform: skewY(-1.5deg) scaleX(1); }
  to   { transform: skewY(1.5deg) scaleX(1.06); }
}
.scene-layer.wl-glow {
  left: 0; right: 0; bottom: 0;
  height: 55vh;
  mix-blend-mode: screen;
  background: radial-gradient(70% 90% at 50% 100%,
    rgba(255, 70, 20, .42), rgba(160, 20, 10, .18) 45%, transparent 75%);
  animation: forge-breathe 4s ease-in-out infinite alternate;
}
/* Wednesday's storm flashes too, now and then: whiter and colder than
   Thursday's violet. */
.mood[data-element="storm"] .scene-layer.evoke-flash {
  background: radial-gradient(120% 80% at 50% 25%,
    rgba(226, 244, 246, .5), rgba(170, 210, 215, .14) 55%, transparent 80%);
}


/* --- Saturday: the barrage and the rift -------------------------------------
 *
 * No painted element: the missiles and beams are drawn on the particle
 * canvas by evoker.js. What is here is the tear it sometimes opens - placed
 * by --rx/--ry/--rs, set each time - with the horror looking through it.
 * It tears open as a slit and widens (.open), flinches at each hit (.hit),
 * and slams shut when the final beam lands (.closing).
 */
.mood[data-element="force"] .scene-layer.evoke-element { display: none; }
/* Sunday's binding opens it too */
.mood:not([data-element="force"]):not([data-element="binding"]) .scene-layer.force-rift { display: none; }
.scene-layer.force-rift {
  left: var(--rx, 50%);
  top: var(--ry, 30%);
  width: var(--rs, 280px);
  aspect-ratio: 1;
  translate: -50% -50%;
  opacity: 0;
  scale: 0 0;
}
.force-rift::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background:
    var(--plate) center / 118% no-repeat,
    radial-gradient(closest-side, #050a0a 55%, #0c0418 100%);
  -webkit-mask-image: radial-gradient(closest-side, #000 68%, transparent 82%);
  mask-image: radial-gradient(closest-side, #000 68%, transparent 82%);
  transition: scale .5s ease-in, filter .3s;
}
.force-rift::after {
  content: "";
  position: absolute;
  inset: -6%;
  border-radius: 50%;
  background: radial-gradient(closest-side,
    transparent 58%, rgba(120, 255, 190, .45) 64%, #f4fff8 68.5%,
    rgba(190, 130, 255, .85) 72%, rgba(120, 60, 255, .35) 79%, transparent 90%);
  mix-blend-mode: screen;
  animation: rift-flicker .3s steps(2) infinite alternate;
}
.scene-layer.force-rift.open { animation: force-tear 1.3s cubic-bezier(.3, .6, .3, 1) forwards; }
@keyframes force-tear {
  0%   { opacity: 1; scale: .02 0; }
  25%  { opacity: 1; scale: .05 .95; }
  100% { opacity: 1; scale: 1 1; }
}
.force-rift.hit::before { animation: rift-hit .3s ease-out; }
@keyframes rift-hit {
  0%   { filter: brightness(2.2) saturate(1.4); }
  100% { filter: brightness(1); }
}
/* the final blow: it flares, is crushed flat, and is gone - the thing
   inside recoiling as it goes */
.scene-layer.force-rift.closing { animation: force-slam .65s cubic-bezier(.5, 0, .7, .4) forwards; }
.force-rift.closing::before { scale: .55; filter: brightness(1.8); }
@keyframes force-slam {
  0%   { opacity: 1; scale: 1 1; filter: brightness(1); }
  30%  { opacity: 1; scale: 1.14 .14; filter: brightness(3); }
  100% { opacity: 0; scale: 0 0; filter: brightness(1); }
}
.mood[data-element="force"] .scene-layer.evoke-flash {
  background: radial-gradient(120% 80% at var(--rx, 50%) var(--ry, 30%),
    rgba(230, 215, 255, .55), rgba(160, 255, 210, .14) 50%, transparent 80%);
}

/* The barrage always passes behind the caster, never between the caster and
   you: on every Evoker day the particle canvas - missiles, beams, sparks and all -
   has the caster's own outline cut out of it: the caster plate, placed by
   evoker.js at the caster's box (--cut-x/y/w/h, measured against the
   canvas), falling back to where that box normally is. */
.mood[data-element] .scene-fx {
  -webkit-mask:
    linear-gradient(#000 0 0),
    var(--caster) var(--cut-x, 50%) var(--cut-y, calc(100% + 2px)) /
      var(--cut-w, var(--caster-w)) var(--cut-h, calc(var(--caster-w) * 4 / 3)) no-repeat;
  -webkit-mask-composite: xor;
  mask:
    linear-gradient(#000 0 0),
    var(--caster) var(--cut-x, 50%) var(--cut-y, calc(100% + 2px)) /
      var(--cut-w, var(--caster-w)) var(--cut-h, calc(var(--caster-w) * 4 / 3)) no-repeat;
  mask-composite: exclude;
}

/* Saturday's barrier: a bubble of force round the caster, placed by
   evoker.js (--sx/--sy/--sr). It flares up just before the first shot that
   gets through, shivers brighter at each hit, and fades. */
.mood:not([data-element="force"]):not([data-element="binding"]) .scene-layer.force-shield { display: none; }
.scene-layer.force-shield {
  left: var(--sx, 50%);
  top: var(--sy, 70%);
  width: calc(var(--sr, 100px) * 2);
  aspect-ratio: 1;
  translate: -50% -50%;
  border-radius: 50%;
  opacity: 0;
  mix-blend-mode: screen;
  background:
    radial-gradient(closest-side, transparent 62%, rgba(150, 220, 255, .18) 78%,
      rgba(210, 240, 255, .85) 94%, rgba(150, 200, 255, .4) 97%, transparent 100%),
    repeating-conic-gradient(from 0deg, rgba(170, 220, 255, .08) 0 6deg, transparent 6deg 18deg);
}
/* up: flares in and holds; down: fades away. A hit is a short second
   animation alongside, so the one holding it up carries on through it. */
.scene-layer.force-shield.up { animation: shield-in .45s ease-out forwards; }
.scene-layer.force-shield.down { animation: shield-out .9s ease-in forwards; }
.scene-layer.force-shield.up.hit { animation: shield-in .45s ease-out forwards, shield-flash .4s ease-out; }
@keyframes shield-in {
  0%   { opacity: 0; scale: .7; }
  55%  { opacity: 1; scale: 1.05; }
  100% { opacity: .9; scale: 1; }
}
@keyframes shield-out {
  0%   { opacity: .9; scale: 1; }
  100% { opacity: 0; scale: 1.08; }
}
@keyframes shield-flash {
  0%   { filter: brightness(2.2); }
  100% { filter: brightness(1); }
}

/* --- Thursday: death -----------------------------------------------------------
 *
 * A low green fog over the ground, and the pool of shadow that opens in it
 * for Dragged Below - placed by evoker-death.js (--px/--py/--pw).
 */
.mood:not([data-element="death"]) .scene-layer.death-fog,
.mood:not([data-element="death"]):not([data-element="binding"]) .scene-layer.death-pool { display: none; }
.scene-layer.death-fog {
  left: -10%; right: -10%; bottom: 0;
  height: 42vh;
  mix-blend-mode: screen;
  background:
    radial-gradient(40% 60% at 25% 100%, rgba(90, 255, 150, .16), transparent 70%),
    radial-gradient(45% 55% at 75% 100%, rgba(70, 220, 130, .14), transparent 70%),
    radial-gradient(70% 45% at 50% 100%, rgba(40, 120, 80, .22), transparent 75%);
  animation: fog-drift 14s ease-in-out infinite alternate;
}
@keyframes fog-drift {
  from { translate: -3% 0; opacity: .8; }
  to   { translate: 3% 0;  opacity: 1; }
}
.scene-layer.death-pool {
  left: var(--px, 50%);
  top: var(--py, 90%);
  width: var(--pw, 200px);
  aspect-ratio: 3 / 1;
  translate: -50% -50%;
  border-radius: 50%;
  opacity: 0;
  scale: 0 0;
  background: radial-gradient(closest-side,
    #000 0 52%, #020c06 66%, rgba(90, 255, 150, .6) 84%, rgba(90, 255, 150, .15) 92%, transparent 100%);
}
.scene-layer.death-pool.open { animation: pool-open .9s cubic-bezier(.2, .7, .3, 1) forwards; }
@keyframes pool-open {
  0%   { opacity: 0; scale: .1 .1; }
  100% { opacity: 1; scale: 1 1; }
}
.scene-layer.death-pool.closing { animation: pool-close .8s ease-in forwards; }
@keyframes pool-close {
  0%   { opacity: 1; scale: 1 1; filter: brightness(1); }
  30%  { opacity: 1; scale: 1.15 .7; filter: brightness(2.2); }
  100% { opacity: 0; scale: 0 0; filter: brightness(1); }
}

/* --- Monday: ice ----------------------------------------------------------------
 *
 * A cold glow round the edges of the screen, breathing in and out. The rime
 * itself is drawn - fern-like frost growing in from the edges, in
 * evoker-ice.js - so it matches the rest of the scene rather than being a
 * texture laid over it.
 */
.mood:not([data-element="ice"]) .scene-layer.ice-frost { display: none; }
.scene-layer.ice-frost {
  inset: 0;
  background: radial-gradient(ellipse 75% 68% at 50% 46%,
    transparent 58%, rgba(170, 215, 255, .12) 80%, rgba(210, 238, 255, .3) 100%);
  animation: frost-breathe 16s ease-in-out infinite alternate;
}
@keyframes frost-breathe {
  from { opacity: .55; }
  to   { opacity: 1; }
}
.mood[data-element="ice"] .scene-layer.evoke-flash {
  background: radial-gradient(120% 80% at 50% 60%,
    rgba(225, 245, 255, .6), rgba(150, 210, 255, .16) 50%, transparent 80%);
}

/* Tuesday's flash is fire. */
.mood[data-element="flame"] .scene-layer.evoke-flash {
  background: radial-gradient(120% 80% at 50% 35%,
    rgba(255, 220, 170, .6), rgba(255, 120, 50, .18) 50%, transparent 80%);
}

/* Friday's flash is gold. */
.mood[data-element="sorcery"] .scene-layer.evoke-flash {
  background: radial-gradient(120% 80% at 50% 40%,
    rgba(255, 240, 200, .55), rgba(255, 200, 110, .15) 50%, transparent 80%);
}

/* Friday's vanishing: the crag alone is shown while the caster is away. It
   is the same plate cut off below their feet, so it sits exactly on the
   crag of the caster's own plate. */
.scene-layer.evoke-rock { opacity: 0; transition: opacity .3s; }
.mood.rock-only .scene-layer.evoke-rock { opacity: 1; }
.scene-layer.evoke-caster.phase-out { animation: caster-out 1.8s ease-in forwards; }
.scene-layer.evoke-caster.phase-in { animation: caster-in 1.4s ease-out both; }
@keyframes caster-out {
  0%   { opacity: 1; filter: brightness(.8) drop-shadow(0 0 12px var(--ev-glow, transparent)); }
  35%  { opacity: .9; filter: brightness(2.4) drop-shadow(0 0 18px var(--ev-glow, transparent)); }
  100% { opacity: 0; filter: brightness(3.5) blur(3px); }
}
@keyframes caster-in {
  0%   { opacity: 0; filter: brightness(3.5) blur(3px); }
  55%  { opacity: 1; filter: brightness(2) drop-shadow(0 0 20px var(--ev-glow, transparent)); }
  100% { opacity: 1; filter: brightness(.8) drop-shadow(0 0 12px var(--ev-glow, transparent)); }
}
/* the jolt when the world bursts apart */
.mood.quake { animation: world-quake .5s linear; }
@keyframes world-quake {
  0%, 100% { translate: 0 0; }
  15% { translate: -7px 4px; }
  30% { translate: 6px -5px; }
  45% { translate: -5px -3px; }
  60% { translate: 4px 5px; }
  80% { translate: -2px 2px; }
}

/* Friday's likeness in the stars: it fades in like the night's apparitions -
   out of a blur, growing slightly - holds, and fades. Some of its stars
   twinkle over it, on the Druid night sky's twinkle. */
.mood:not([data-element="sorcery"]) .scene-layer.sorcery-constellation { display: none; }
.scene-layer.sorcery-constellation {
  left: 50%;
  top: var(--cy, 6vh);
  width: var(--cw, 360px);
  aspect-ratio: 1;
  translate: -50% 0;
  background: var(--plate) center / contain no-repeat;
  mix-blend-mode: screen;
  opacity: 0;
  scale: .94;
  filter: blur(4px) brightness(1.1);
  transition: opacity 1.8s ease, scale 7s ease-out, filter 1.8s ease;
}
.scene-layer.sorcery-constellation.show {
  opacity: 1;
  scale: 1.02;
  filter: blur(0) brightness(1.6) drop-shadow(0 0 14px rgba(255, 230, 170, .45));
}
.sorcery-constellation i {
  position: absolute;
  translate: -50% -50%;
  border-radius: 50%;
  background: #fff8e2;
  box-shadow: 0 0 6px rgba(255, 238, 196, .95);
  animation-name: star-twinkle;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}
.sorcery-constellation i.bright::before,
.sorcery-constellation i.bright::after {
  content: "";
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);
}
.sorcery-constellation i.bright::before {
  width: 900%; height: 1px;
  background: linear-gradient(90deg, transparent, rgba(255, 244, 214, .95), transparent);
}
.sorcery-constellation i.bright::after {
  width: 1px; height: 900%;
  background: linear-gradient(180deg, transparent, rgba(255, 244, 214, .95), transparent);
}

/* Sunday's flash: gold with a touch of every colour of the week. */
.mood[data-element="binding"] .scene-layer.evoke-flash {
  background: radial-gradient(120% 80% at var(--rx, 50%) var(--ry, 35%),
    rgba(255, 240, 210, .6), rgba(190, 150, 255, .16) 45%, rgba(120, 255, 170, .06) 65%, transparent 80%);
}

/* On a phone the rift sits behind the see-through cards, and there the
   burning edge shone through them while the horror inside, being dark, was
   lost - a ring of neon with nothing in it. So on a narrow screen the horror
   is lit up and the edge is turned down to a thin line. */
@media (max-width: 619px) {
  .force-rift::before {
    filter: brightness(1.9) saturate(1.3) contrast(1.15);
  }
  .force-rift::after {
    animation: none;
    background: radial-gradient(closest-side,
      transparent 64%, rgba(120, 255, 190, .22) 68%, rgba(244, 255, 248, .5) 70.5%,
      rgba(190, 130, 255, .3) 73%, transparent 80%);
  }
}

/* --- Healer: the window -------------------------------------------------
 *
 * One window. The plate gives the stone, the leading and the painted detail;
 * the glass is painted by js/scenes/healer.js onto a canvas laid over it:
 * lit white as the level rises, stained in colour as spells are bought. The
 * same picture is drawn twice more: blurred into a bloom over the reading
 * shade, and cast down onto the floor. The module sets --lit (how much of
 * the window has light through it), --fill (how much is stained) and --holy
 * (the colour of the light) for everything around it.
 */
@property --lit {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}
@property --fill {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}
.mood.chapel {
  --lit: 0;
  --fill: 0;
  --holy: #fff6e6;
  transition: --lit 1.6s ease, --fill 1.6s ease;
}
.scene-layer.hl-window,
.scene-layer.hl-radiance {
  left: 50%;
  bottom: 12vh;
  height: min(82vh, 112vw);
  aspect-ratio: 3 / 4;
  translate: -50% 0;
}
.hl-window > *,
.hl-radiance > *,
.hl-cast > * {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.hl-window > .dark {
  background: var(--plate) center / contain no-repeat;
  filter: grayscale(1) brightness(.3) contrast(1.3);
}
/* the glass: painted light, so it is left at full strength */
.hl-window > canvas { filter: saturate(1.15); }
/* the painting's own detail, laid back over the colour */
.hl-window > .sheen {
  background: var(--plate) center / contain no-repeat;
  mix-blend-mode: soft-light;
  opacity: .35;
}
/* The light radiating from it. Screened over everything, the reading shade
   included - light adds, so it brightens what is behind the text without
   covering it. Kept soft and below full strength for the same reason. */
.scene-layer.hl-radiance {
  pointer-events: none;
  mix-blend-mode: screen;
  opacity: calc(.12 + var(--lit) * .3 + var(--fill) * .3);
}
.hl-radiance > canvas {
  filter: blur(18px) saturate(1.4) brightness(1.35);
  scale: 1.1;
}
/* rays of light streaming out of the window into the room */
.hl-radiance > .rays {
  inset: -55% -90%;
  width: auto;
  height: auto;
  background: repeating-conic-gradient(from 0deg at 50% 44%,
    color-mix(in srgb, var(--holy) 55%, transparent) 0deg 2.2deg,
    transparent 2.2deg 7deg,
    color-mix(in srgb, var(--holy) 30%, transparent) 7deg 8.5deg,
    transparent 8.5deg 15deg);
  -webkit-mask-image: radial-gradient(closest-side at 50% 44%, transparent 12%, #000 22%, transparent 78%);
  mask-image: radial-gradient(closest-side at 50% 44%, transparent 12%, #000 22%, transparent 78%);
  /* the rays are the brilliance: they come with the colour, not the level */
  opacity: calc(var(--lit) * .1 + var(--fill) * .38);
  filter: blur(5px);
  animation: hl-rays 40s linear infinite alternate;
}
@keyframes hl-rays {
  from { rotate: -4deg; }
  to   { rotate: 4deg; }
}
@media (max-width: 619px) {
  .scene-layer.hl-radiance { opacity: calc(.2 + var(--lit) * .35 + var(--fill) * .35); }
}
/* the coloured light on the floor: the window again, flipped and laid flat */
.scene-layer.hl-cast {
  left: 50%;
  bottom: -2vh;
  height: 24vh;
  width: min(92vh, 124vw);
  translate: -50% 0;
  mix-blend-mode: screen;
  opacity: calc(var(--lit) * .3 + var(--fill) * .3);
  -webkit-mask-image: linear-gradient(to bottom, #000 10%, transparent 95%);
  mask-image: linear-gradient(to bottom, #000 10%, transparent 95%);
}
.hl-cast > canvas {
  transform: scaleY(-1);
  filter: blur(18px) saturate(1.5);
}
/* the light behind the glass, a white-gold burning brighter as it fills */
.scene-layer.hl-glow {
  left: 0; right: 0; bottom: 0;
  height: 100vh;
  mix-blend-mode: screen;
  background:
    radial-gradient(26% 34% at 50% 44%, rgba(255, 250, 235, .9), transparent 72%),
    radial-gradient(50% 44% at 50% 50%, color-mix(in srgb, var(--holy) 55%, transparent), transparent 74%);
  opacity: calc(.12 + var(--lit) * .5 + var(--fill) * .2);
  animation: hl-glow-breathe 7s ease-in-out infinite alternate;
}
/* Breathes on opacity, not on a filter: a filter animation repaints this
   whole full-screen gradient every frame, which is felt on a phone. */
@keyframes hl-glow-breathe {
  from { opacity: calc(.1 + var(--lit) * .4 + var(--fill) * .16); }
  to   { opacity: calc(.14 + var(--lit) * .55 + var(--fill) * .22); }
}
@media (max-width: 619px) {
  .scene-layer.hl-window,
  .scene-layer.hl-radiance { height: 64vh; bottom: 16vh; }
}

/* --- Bard: the tavern ----------------------------------------------------
 *
 * Seen from the back of the room, over the crowd's heads to the hearth on
 * the far wall. js/scenes/bard.js reads the scroll and sets three numbers:
 *   --lanterns  how many of the six lanterns are lit: how far down the
 *               list you are. Each lantern shows while its --rank is below
 *               it, the same clamp() as the Healer's glass.
 *   --vigor     how hard the tune is going, 0 to 1: raised by scrolling,
 *               falling away when you stop. Everything burns by it.
 *   --blaze     the fire: never quite out, roaring at full vigor.
 */
@property --lanterns {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;
}
@property --vigor {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}
.mood.tavern {
  --lanterns: 1;
  --vigor: 0;
  --blaze: calc(.3 + var(--vigor) * .7);
  transition: --lanterns .6s ease, --vigor .5s linear;
}
/* the far wall and the air, lit by the fire */
.scene-layer.bd-glow {
  inset: 0;
  mix-blend-mode: screen;
  background:
    radial-gradient(40% 36% at 50% 72%, rgba(255, 150, 60, .55), transparent 72%),
    radial-gradient(80% 60% at 50% 30%, rgba(255, 190, 110, .14), transparent 70%);
  opacity: calc(.25 + var(--blaze) * .75);
}
/* the hearth on the far wall, its edges lost in the dark of the room */
.scene-layer.bd-hearth {
  left: 50%;
  bottom: 16vh;
  height: min(50vh, 72vw);
  aspect-ratio: 1;
  translate: -50% 0;
  background: var(--plate) center / contain no-repeat;
  -webkit-mask-image: radial-gradient(closest-side, #000 62%, transparent 100%);
  mask-image: radial-gradient(closest-side, #000 62%, transparent 100%);
  filter: brightness(calc(.45 + var(--blaze) * .75)) saturate(calc(.8 + var(--blaze) * .4));
}
/* the fire itself, flaring with the vigor */
.bd-hearth > .fire {
  position: absolute;
  left: 22%; right: 22%; top: 34%; bottom: 20%;
  border-radius: 50% 50% 40% 40%;
  mix-blend-mode: screen;
  background: radial-gradient(closest-side at 50% 70%,
    rgba(255, 230, 150, .9), rgba(255, 130, 40, .6) 45%, transparent 80%);
  opacity: calc(var(--blaze) * .8);
  animation: bd-flicker .28s steps(2) infinite alternate;
}
@keyframes bd-flicker {
  from { transform: scale(.96, .94); }
  to   { transform: scale(1.03, 1.06); }
}
/* The bard, beside the hearth and facing the room, lit from the fire's
   side. Four plates of the same person, one per way of performing; the one
   shown is whichever bard.js has set in data-play, cross-faded. He keeps
   time with the tune: still when the room is quiet, bobbing when it roars. */
.scene-layer.bd-bard {
  --h: min(44vh, 64vw);
  left: min(calc(50% + min(50vh, 72vw) * .46), calc(100% - var(--h) * .3));
  bottom: 12vh;
  height: var(--h);
  aspect-ratio: 4 / 7;
  translate: -50% 0;
  transform-origin: 50% 100%;
  filter: brightness(calc(.5 + var(--blaze) * .55))
          drop-shadow(-4px 0 5px rgba(255, 150, 60, calc(.25 + var(--blaze) * .5)));
  animation: bd-beat .6s ease-in-out infinite alternate;
}
@keyframes bd-beat {
  from { transform: translateY(0) rotate(calc((.2 + var(--vigor)) * -1.4deg)); }
  to   { transform: translateY(calc((.2 + var(--vigor)) * -5px)) rotate(calc((.2 + var(--vigor)) * 1.4deg)); }
}
.bd-bard > div {
  position: absolute;
  inset: 0;
  background: center bottom / contain no-repeat;
  opacity: 0;
  transition: opacity .9s ease;
}
.bd-bard > .lute   { background-image: url("../art/bard-lute.webp"); }
.bd-bard > .flute  { background-image: url("../art/bard-flute.webp"); }
.bd-bard > .fiddle { background-image: url("../art/bard-fiddle.webp"); }
.bd-bard > .sing   { background-image: url("../art/bard-sing.webp"); }
.bd-bard[data-play="lute"]   > .lute,
.bd-bard[data-play="flute"]  > .flute,
.bd-bard[data-play="fiddle"] > .fiddle,
.bd-bard[data-play="sing"]   > .sing { opacity: 1; }
.scene-layer.bd-rafters {
  left: 0; right: 0; top: 0;
  height: 34vh;
  background: var(--plate) center top / cover no-repeat;
  filter: brightness(calc(.4 + var(--blaze) * .5));
  /* the beams go up into the dark, not end on a line */
  -webkit-mask-image: linear-gradient(to bottom, #000 55%, transparent);
  mask-image: linear-gradient(to bottom, #000 55%, transparent);
}
/* six lanterns strung across the room, hung lower toward the middle */
.scene-layer.bd-lantern {
  --alight: clamp(0, calc(var(--lanterns) - var(--rank)), 1);
  --sag: calc((2.5 - var(--i)) * (2.5 - var(--i)));
  left: calc(8% + var(--i) * 16.8%);
  top: calc(20vh - var(--sag) * 1.1vh);
  height: min(24vh, 30vw);
  aspect-ratio: 3 / 5;
  translate: -50% 0;
  transform-origin: 50% 0;
  animation: bd-swing 5s ease-in-out infinite alternate;
  animation-delay: calc(var(--i) * -.8s);
}
@keyframes bd-swing {
  from { rotate: -2deg; }
  to   { rotate: 2deg; }
}
.bd-lantern > .body {
  position: absolute;
  inset: 0;
  background: var(--plate) center / contain no-repeat;
  /* unlit it is iron and dark glass; lit, the candle takes it */
  filter: brightness(calc(.3 + var(--alight) * (.5 + var(--vigor) * .5)));
}
.bd-lantern > .halo {
  position: absolute;
  left: -140%; right: -140%; top: -40%; bottom: -60%;
  border-radius: 50%;
  mix-blend-mode: screen;
  background: radial-gradient(closest-side at 50% 58%,
    rgba(255, 210, 130, .55), rgba(255, 150, 60, .18) 45%, transparent 75%);
  opacity: calc(var(--alight) * (.4 + var(--vigor) * .6));
}
/* the crowd, their backs to us, lit from in front by the fire */
.scene-layer.bd-crowd {
  left: -4%; right: -4%; bottom: -1vh;
  height: min(42vh, 52vw);
  background: var(--plate) center bottom / auto 100% repeat-x;
  filter: drop-shadow(0 -2px 2px rgba(255, 170, 80, calc(.35 + var(--blaze) * .5)))
          drop-shadow(0 -6px 10px rgba(255, 120, 40, calc(.15 + var(--blaze) * .35)));
  transform-origin: 50% 100%;
  animation: bd-sway 2.4s ease-in-out infinite alternate;
}
/* how far they sway is the vigor: still when the room is quiet */
@keyframes bd-sway {
  from { transform: rotate(calc(var(--vigor) * -1.2deg)) translateY(calc(var(--vigor) * 3px)); }
  to   { transform: rotate(calc(var(--vigor) * 1.2deg)) translateY(0); }
}
/* notes lifting off the bottom of the screen */
.scene-layer.bd-notes {
  inset: 0;
  pointer-events: none;
  filter: drop-shadow(0 0 5px rgba(255, 190, 90, .9));
}
.bd-notes > i {
  position: absolute;
  width: var(--size);
  height: var(--size);
  translate: -50% -50%;
  background: #ffd98f;
  -webkit-mask: url("../art/bard-notes.webp") no-repeat;
  mask: url("../art/bard-notes.webp") no-repeat;
  -webkit-mask-size: 500% 100%;
  mask-size: 500% 100%;
  -webkit-mask-position: calc(var(--cell) / 4 * 100%) 0;
  mask-position: calc(var(--cell) / 4 * 100%) 0;
  opacity: 0;
  animation: bd-note 3.5s ease-out forwards;
}
@keyframes bd-note {
  0%   { opacity: 0; transform: scale(.6); }
  12%  { opacity: 1; }
  70%  { opacity: .85; }
  100% { opacity: 0; transform: translate(var(--dx), var(--dy)) rotate(var(--spin)) scale(1.1); }
}
/* the finale: a shimmer of applause through the room */
.scene-layer.bd-applause {
  inset: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  background: radial-gradient(70% 60% at 50% 70%, rgba(255, 210, 130, .5), transparent 70%);
  opacity: 0;
}
.mood.applause .scene-layer.bd-applause { animation: bd-applause 2.6s ease-out; }
@keyframes bd-applause {
  0%   { opacity: 0; }
  12%  { opacity: 1; }
  30%  { opacity: .5; }
  45%  { opacity: .85; }
  100% { opacity: 0; }
}

/* --- Druid / Ranger: the hunter's watch -----------------------------------
 *
 * A ridge above a misty valley at dusk: the painted vista behind, fog along
 * the valley, a strip of meadow in front, the spirit stag standing in it,
 * and the ranger's camp. js/scenes/ranger.js sets --near (0 to 1, how much
 * kit the list holds) and turns each piece of the camp on as it is bought.
 */
@property --near {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}
.scene-layer.rg-vista {
  inset: 0;
  background: var(--plate) 55% 60% / cover no-repeat;
}
.scene-layer.rg-mist {
  left: -20%; right: -20%;
  bottom: 22vh;
  height: 26vh;
  background: var(--plate) center / auto 100% repeat-x;
  mix-blend-mode: screen;
  opacity: .35;
  animation: rg-fog 90s linear infinite alternate;
}
@keyframes rg-fog {
  from { translate: -6% 0; }
  to   { translate: 6% 0; }
}
/* the ground everything stands in, dusk-dark */
.scene-layer.rg-meadow {
  left: 0; right: 0; bottom: 0;
  height: min(21vh, 19vw);
  background: var(--plate) center bottom / auto 100% repeat-x;
  filter: brightness(.4) saturate(.75);
}
/* The stag. Far off it stands at the back of the meadow on the right, its
   legs in the grass; with the kit it walks nearer and larger. It breathes,
   and shifts its weight now and then (ranger.js). */
.scene-layer.rg-stag {
  left: calc(86% - var(--near) * 16%);
  bottom: calc(11vh - var(--near) * 10vh);
  height: calc(16vh + var(--near) * 28vh);
  aspect-ratio: 598 / 674;
  translate: -50% 0;
  background: var(--plate) center bottom / contain no-repeat;
  mix-blend-mode: screen;
  opacity: calc(.6 + var(--near) * .35);
  filter: drop-shadow(0 0 calc(5px + var(--near) * 10px) rgba(150, 210, 255, .7));
  transform-origin: 50% 100%;
  transition: left 3s ease-in-out, bottom 3s ease-in-out, height 3s ease-in-out, opacity 3s ease;
  animation: rg-breathe 4.5s ease-in-out infinite alternate;
}
@keyframes rg-breathe {
  from { transform: scale(1, 1); }
  to   { transform: scale(1.006, 1.014); }
}
/* walking nearer: a stepping bob for as long as the move takes */
.scene-layer.rg-stag.walking { animation: rg-walk .55s ease-in-out 6 alternate; }
@keyframes rg-walk {
  from { transform: translateY(0) rotate(-.6deg); }
  to   { transform: translateY(-4px) rotate(.6deg); }
}
/* shifting its weight, from one hoof to another */
.scene-layer.rg-stag.shift { animation: rg-shift 1.4s ease-in-out; }
@keyframes rg-shift {
  0%, 100% { transform: translate(0, 0); }
  35%      { transform: translate(var(--step, 6px), -2px); }
  70%      { transform: translate(calc(var(--step, 6px) * .6), 0); }
}
/* when a piece of kit is set down, it lifts its head and looks */
.scene-layer.rg-stag.startle { animation: rg-startle .9s ease-out; }
@keyframes rg-startle {
  0%   { transform: translateY(0) scale(1); }
  25%  { transform: translateY(-8px) scale(1.02); }
  100% { transform: translateY(0) scale(1); }
}
/* The camp: exactly the shape of the hunter painting, so every place below
   is a place in it. In that painting the log's top runs from 50% down at its
   right end to 61% at its left, its cut end is at 88-93% across, and the
   ground is at 22% up behind the log and 5% up in front of the fire. */
.scene-layer.rg-camp {
  left: 27%;
  bottom: 1vh;
  height: min(50vh, 36vw);
  aspect-ratio: 742 / 716;
  translate: -50% 0;
}
.rg-camp > div {
  position: absolute;
  background: center bottom / contain no-repeat;
  translate: -50% 0;
  transform-origin: 50% 100%;
  opacity: 0;
  transition: opacity 1s ease;
  /* lit by the fire: a warm edge, so each piece catches the light */
  filter: drop-shadow(0 0 1.5px rgba(255, 180, 100, .75)) drop-shadow(0 0 6px rgba(255, 140, 60, .3));
}
.rg-camp > .on { opacity: 1; }
.rg-camp > .hunter {
  left: 50%; bottom: 0; width: 100%; height: 100%; translate: -50% 0;
  background-image: url("../art/ranger-hunter.webp");
  opacity: 1; filter: none;
}
/* The stump, the bow leaning on it, stands in front of the log's left end:
   the painting stops there with a straight cut, and the stump hides it. */
.rg-camp > .stump {
  left: 2%; bottom: 7%; height: 54%; aspect-ratio: 524 / 639;
  background-image: url("../art/ranger-stump.webp");
  opacity: 1;
}
/* The great axe: a great weapon is 48 inches or more. The ranger's hood is
   about 53 inches off the ground here, so the axe stands nearly that tall. */
.rg-camp > .greataxe {
  left: 96%; bottom: 3%; height: 86%; aspect-ratio: 256 / 846;
  background-image: url("../art/ranger-greataxe.webp");
  /* upside down: its head on the ground in front of the log, the haft up
     in the air, leaning back against the log's top */
  transform-origin: 50% 50%;
  scale: 1 -1;
  rotate: -19deg;
}
/* the sword: its point on the ground in front of the log, right of the
   fire, its hilt resting against the log's top edge */
.rg-camp > .sword {
  left: 86%; bottom: 3%; height: 52%; aspect-ratio: 279 / 842;
  background-image: url("../art/ranger-sword.webp");
  rotate: -7deg;
}
/* the hand axe: at the ranger's right side, hung at the hip below the
   shield, its handle down to the log */
.rg-camp > .handaxe {
  left: 69%; bottom: 38%; height: 28%; aspect-ratio: 246 / 565;
  background-image: url("../art/ranger-handaxe.webp");
  /* blade facing the other way: toward the log's far end */
  scale: -1 1;
  rotate: 12deg;
  transform-origin: 50% 50%;
}
/* the hunting knife, planted in the ground in front of the stump */
.rg-camp > .knife {
  left: 13%; bottom: -2%; height: 27%; aspect-ratio: 275 / 527;
  background-image: url("../art/ranger-knife.webp");
  rotate: -6deg;
}
/* the shield: slung on the ranger's back, across the shoulders over the
   cloak, its face to us */
.rg-camp > .shield {
  left: 47%; bottom: 52%; height: 30%; aspect-ratio: 350 / 447;
  background-image: url("../art/ranger-shield.webp");
  rotate: -8deg;
  transform-origin: 50% 50%;
}
/* the quiver: on the ground beside the stump, leaning on it */
.rg-camp > .quiver {
  left: 27%; bottom: 6%; height: 40%; aspect-ratio: 215 / 550;
  rotate: -12deg;
  opacity: 1;
}
.rg-camp > .quiver > .q-body {
  position: absolute; inset: 0;
  background: url("../art/ranger-quiver.webp") center bottom / contain no-repeat;
}
/* the arrows it holds: behind its body, fletching up */
.rg-camp > .quiver > .q-arrows {
  position: absolute;
  left: 22%; right: 22%;
  bottom: 30%; top: -24%;
}
.rg-camp > .quiver > .q-arrows > i {
  position: absolute;
  left: calc(50% + var(--x) * .9%);
  bottom: 0;
  height: 100%;
  aspect-ratio: 192 / 868;
  translate: -50% 0;
  /* the plate is stored fletching-up, as an arrow sits in a quiver */
  background: url("../art/ranger-arrow.webp") center / contain no-repeat;
  rotate: calc(var(--x) * .45deg);
  transform-origin: 50% 100%;
  animation: rg-arrow-in .8s ease-out;
}
@keyframes rg-arrow-in {
  from { translate: -50% -30%; opacity: 0; }
  to   { translate: -50% 0; opacity: 1; }
}
/* the fire's light: on the ground round the camp, and flickering over it */
.rg-camp > .glow {
  left: 55%; bottom: -10%;
  width: 110%; height: 56%;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(255, 150, 60, .42), transparent 75%);
  mix-blend-mode: screen;
  opacity: 1; filter: none;
  animation: rg-flicker 1.6s ease-in-out infinite alternate;
}
.rg-camp > .light {
  left: 55%; bottom: 4%;
  width: 30%; height: 34%;
  border-radius: 50%;
  background: radial-gradient(closest-side at 50% 70%, rgba(255, 220, 140, .6), rgba(255, 130, 40, .25) 50%, transparent 80%);
  mix-blend-mode: screen;
  opacity: 1; filter: none;
  animation: rg-flicker .3s steps(2) infinite alternate;
}
@keyframes rg-flicker {
  from { scale: .95; opacity: .8; }
  to   { scale: 1.05; opacity: 1; }
}
@media (max-width: 619px) {
  .scene-layer.rg-camp { left: 42%; height: 30vh; }
  .scene-layer.rg-meadow { height: 16vh; }
  /* a phone is narrow: the stag stays at the right edge and grows less */
  .scene-layer.rg-stag { left: calc(88% - var(--near) * 4%); height: calc(10vh + var(--near) * 12vh); }
}
