/* ==========================================================================
   STAR BACKGROUND — the field the whole page sits on.

   ARTWORK: assets/hero/starfield-deep-4k.webp — 3840x2144, WebP q86, 124 KB.
   Generated at 4K from a FRAME OF THE HERO VIDEO — messengers-hero-video.mp4 at
   t = 7.79s, frame 187 of 210 — passed in as the reference so the composition,
   the diagonal nebula band and the star density all come from the footage
   already playing at the top of the page. Delivered at 5504x3072 and downscaled
   to 3840 for weight: 3840 is true UHD and covers a 2x 1440px viewport with room
   to spare, at half the bytes of the native output. starfield-deep.webp, the
   straight 1920x1080 video frame, is kept beside it.

   To go back to a raw frame instead: ffmpeg -ss <t> -i
   assets/hero/messengers-hero-video.mp4 -frames:v 1 out.png, encode at q86.

   MEASURED off the shipped file, with the source frame beside it:

                              source frame      shipped 4K
       median luminance           2.08              4.30
       mean luminance             9.16             11.32
       at or below #03040F       60.2%             53.3%
       saturation (L>12)          67%               60%
       field RGB              (0.1, 0.3, 5.5)   (2.0, 1.4, 10.3)

   Half the frame is still at or below the page's own black, which is the only
   reason a photograph can sit under body copy at all. But the regeneration DID
   lift the floor — the field is about twice as bright and twice as blue as the
   frame it came from, and under screen blending that is a blue cast added to the
   page's black everywhere the nebula is not. --star-dim and --star-sat both
   attenuate it. Worth knowing before either slider goes up.

   THE BLUE IS THE THING TO WATCH. css/spine-bg.css opens with a rule — the
   hero is essentially colourless, 2-7% saturation, do not re-warm it — and that
   rule was measured off the hero's SPINE CLOSE-UP, frame 3. This frame is from
   the same video and is nothing like colourless. --star-sat exists for exactly
   that reason: it is the dial between the site's neutral grade and the footage
   as shot. It is NOT a contradiction of the spine rule, but it is the one place
   on the page where the two greys disagree, so judge them together.
   ========================================================================== */

:root {
  /* Bumped on every change, same reason as --spine-build: a browser sitting on
     a cached stylesheet is indistinguishable from a change that did not work.
     28 = the FIRST non-empty page block in this file — html.page-home gets
     --star-desync 1 and --star-cloud-bright 2.1. Every page block before this
     one was a hook with nothing in it; this is the first time index.html has
     needed to differ from the baseline at all. The Copy CSS carried thirteen
     sky sliders and the other eleven matched :root exactly. Placed ABOVE the
     mobile block, which matters: that block sets --star-cloud-bright 1.27, and
     source order is the only thing keeping the phone value winning.
     26 and 27 = the cloud sky (js/clouds-sky.js) and the merged /?tune panel;
     neither changed a value in this file.
     25 = the sky reaches merch.html. Empty page block, class added to the
     mobile selector list at the bottom of this file. No sky variable changed.
     THIS ONE WAS NOT FOUND BY A SWEEP, unlike 23 and 24, and the difference
     matters: merch.html is being authored in the same session as this entry,
     so it was ANNOUNCED rather than discovered and there was nothing on disk
     to measure when the block was written. That is exactly why the block is
     empty — see it for what the page itself still has to carry (the <link>,
     and a <main>, neither of which a page block can supply).
     24 = the sky reaches archive/artwork/001.html, which had the identical
     defect and was the WORSE of the two: archive.html:76 links straight to
     it, so every archive card click landed on flat black. Two levels deep,
     so it spells the link ../../css/star-bg.css. Empty page block, class
     added to the mobile selector list at the bottom of this file.
     CORRECTION TO 23 BELOW: transmissions/001.html was not "the last black
     page" — that was written from the five pages the handoff names, and this
     file is not among them. A repo-wide sweep the same day found this one,
     plus music.html (orphaned, nothing links it) and hero-timeline-lab.html
     (carries page-home but never links this file). Do not trust a "last one"
     claim that was not made by a sweep of every .html in the repo.
     23 = the sky reaches transmissions/001.html, the first of the black pages
     to be found (it linked no star-bg.css and carried no page class, so both html
     pseudo-elements computed to content: none — measured in Chrome at
     1440x900 on 2026-08-11). Empty page block + the widened mobile selector
     list at the bottom of this file. No sky variable changed; the reading
     page is the :root baseline, and the reasoning for NOT calming it is in
     the block itself.
     22 = the mobile block re-derived against the build-21 desktop values —
     the stale set HANDOFF 14 records. Dim and the twinkle pair rescaled by
     their original ratios (play-button lift restored on phones); the cloud
     cut moved off --star-cloud (the headroom split, now untouched at every
     width) onto --star-cloud-bright, per the 2026-08-05 stale note it
     replaces. Calibrated at a real 390px viewport; not yet seen on a phone.
     21 = the sky reaches archive.html and transmissions.html (each links this
     file and carries a page class; empty page blocks + the widened mobile
     selector list live at the bottom of this file). No sky variable changed.
     music.html is skipped ON PURPOSE — it is an instant redirect to /#tracks,
     so a sky there would paint for no one; do not "complete the set".
     20 = patterns d/e/f, extracted from the owner's three reference frames
     (.star-bolt--d/e/f; scripts/bolt-extract.py reference-frame mode). Five
     patterns rotate, random, never the same twice in a row.
     19 = pattern b removed by the owner's eye; two patterns alternate
     (.star-bolt--a/c; the slot letter b is retired, not reused).
     18 = three strike patterns (.star-bolt--a/b/c; the snare machine rotates
     .is-struck between them, random, never twice in a row).
     17 = the lightning rewired from the kick to the snare envelope
     (--kick-bolt renamed --snare-bolt; --snare itself lives in spine-bg.css).
     16 = the lightning layer (.star-bolt, end of this file). 9 = --star-black,
     the sky's black point. 8 = four-band twinkle, plus the
     cloud layer's transform replaced by a negative inset (recorded at the time
     as worth 63 -> 88 fps; that figure was taken on a software renderer and is
     corrected under html::after below). 6 was the single lockstep cores
     layer. */
  --star-build: 29;

  /* Brightness of the whole field. 0 is off — the layer still exists but adds
     no light, and under screen blending (see below) that is exactly the same
     as not being there. */
  --star-dim: 1.4;

  /* 1 = the footage as shot, deep blue. 0 = colourless, matching the grade the
     spine artwork was generated to.
     SHIPPED AT 1 — the owner's call, 2026-08-04, after seeing both. The sky is
     the footage as filmed, at full saturation. Note what that means: the "2-7%
     saturation, do not re-warm it" rule in css/spine-bg.css governs the SPINE,
     and the sky is now deliberately not held to it. The two are allowed to
     disagree; they are different objects at different depths. Do not "fix" this
     to match the spine without asking. */
  --star-sat: 0.7;

  /* ---- BLACK POINT ------------------------------------------------------
     HOW MANY LEVELS (out of 255) TO SUBTRACT FROM THE SKY BEFORE IT SCREENS.
     0 is off and is an exact identity — see the filter note below — so this
     slider's zero renders build 8 pixel for pixel.

     WHY IT EXISTS. HANDOFF 8 records that the 4K regeneration lifted the sky's
     floor: field RGB went (0.1, 0.3, 5.5) in the source video frame to
     (2.0, 1.4, 10.3) in the delivered file. Under screen blending that lift can
     only ADD, so it sits on the page as a blue-grey wash. The obvious reading —
     "the empty gaps between stars aren't black" — is not actually where it
     lives. MEASURED off the shipped file:

       source band        share of frame   on-page BLUE at --star-dim 0.8
       empty  (<6)           0.23%              8.6
       FAINT DUST (6-20)    58.45%             14.4      <-- this is the wash
       dust   (20-60)       35.29%             31.0
       nebula (60-150)       5.79%             68.6
       cores  (>=150)        0.25%            159.1

     The true empty sky is a quarter of one percent of the frame. What reads as
     milky is the faint dust across FIFTY-EIGHT PERCENT of it, landing nine
     levels of blue above a (3,4,15) page. Dimming the whole layer to fix that
     also dims the stars; a black point does not.

     WHAT EACH VALUE COSTS — on-page blue for the faint-dust band, against what
     the star cores lose:

       k     faint dust    nebula    star cores    frame at exactly page black
       0        14.4        68.6        187.3               0.0%
       4        11.4        65.5        184.2               0.1%
       6         9.8        63.9        181.3               3.1%
       8         8.3        62.3        179.4              12.1%
      10         7.1        60.8        177.2              24.8%
      12         6.1        59.2        175.3              35.4%

     The cores barely move — 187.3 to 175.3 across the whole range — because a
     subtract of twelve levels is nothing to a pixel at 187 and everything to a
     pixel at 10. That asymmetry is the entire point, and it is why this is the
     right control for "make the blacks blacker" and --star-dim is the wrong one.

     MEASURED ON REAL HARDWARE, 2026-08-04, four frozen frames of the isolated
     sky screenshotted from the owner's own GPU and differenced. Darkest 60% of
     the frame, against a page black of 5.0:

                              R      G      B    blue-R | nebula | cores
       k=0  hue 1  (build 8)  8.02   8.24  17.04   9.02  |  53.9  | 115.4
       k=6  hue 1             5.55   5.37  11.73   6.19  |  49.2  | 110.5
       k=12 hue 1             5.24   5.20   7.18   1.94  |  44.4  | 105.7
       k=6  hue 0.55          5.69   5.34   8.96   3.27  |  43.2  | 105.4

     TWO FINDINGS, AND THE SECOND ONE SETTLED THE VALUE.

     First: by k=6 the RED AND GREEN CHANNELS ARE ALREADY AT PAGE BLACK — 5.55
     and 5.37 against 5.0. Everything left is a blue cast. "The blacks aren't
     black" is really "the blacks are blue", which follows from the artwork's
     field being (2.0, 1.4, 10.3): blue sits seven levels above the other two,
     so a subtract that clears red and green still leaves blue standing.

     Second: the obvious lever for a blue cast is --star-sat, and it is the
     WRONG ONE. Compare the last two rows. Dropping saturation to 0.55 costs the
     same nebula as k=12 (43.2 against 44.4 — slightly worse), blackens LESS
     (blue 8.96 against 7.18), and throws away a decision the owner made
     deliberately on 2026-08-04 that the sky is the footage as shot. The black
     point is strictly better at this job on every axis. Leave hue at 1.

     SHIPPED AT 13 as of build 15 (16 in builds 13-14, 12 when this table was
     written — the reasoning below still holds, only the number has drifted).
     The paragraph was written for 12. Not a taste call — it is the value the
     table supports for
     the stated goal, and it puts 28.1% of the frame at exactly page black
     against 1.1% at k=6. Come down toward 6 if the faint dust is missed; 6 is
     also the level HANDOFF 8 clamped the SPINE artwork by, so the two
     photographs would then be graded alike. Above ~14 the nebula starts to read
     as cut out rather than fading. */
  --star-black: 13;

  /* ---- TWINKLE ----------------------------------------------------------
     Amplitude of the twinkle layers, 0 = off (dead static sky).
     --star-twinkle is the idle state, always running. --star-twinkle-hi
     replaces it while a sample is playing, driven by the same is-spine-pulsing
     flag on <html> that makes the spine column breathe — so the sky and the
     column react to the play button together rather than as two separate ideas.

     NOTE FOR THE OTHER FOUR PAGES: that flag is set by js/spine-bg.js, which is
     index.html only. Everywhere else there is no player, the class never
     appears, and the sky simply twinkles at the idle amplitude forever. That is
     correct, not a bug — do not add spine-bg.js to a page just for this.

     TUNED 2026-08-04, AND THE TWO ARE NOW EQUAL. --star-twinkle and
     --star-twinkle-hi both sit at 1, which means THE PLAY BUTTON NO LONGER
     CHANGES THE STARS. That reverses the shape HANDOFF 8 shipped (0.04 idle
     against 0.32 playing, an eight-fold jump described there as "the play
     button is meant to be the thing that wakes it") and it is the owner's call
     after seeing the four-band version. The mechanism is untouched and costs
     nothing while inert — set the two to different values and the response
     comes straight back. The clouds DO still answer the player, at 1.8x, so
     the sky is not completely deaf to playback.

     Note why the amplitude could go from 0.04 to 1 without the page turning
     into a disco: at build 6 all the light moved on ONE clock, so a little went
     a long way. Spread across four independent bands, the same total light
     reads as scintillation rather than as the sky breathing, and it takes far
     more amplitude before it registers at all. */
  --star-twinkle: 0.46;

  /* THE PLAYING AMPLITUDE, AND IT CLIPS TWO OF THE WAVEFORM'S PEAKS.
     Build 15 ships 1.5 against an idle 0.46, so the play button now moves the
     stars a great deal — reversing the 2026-08-04 decision to hold the two
     equal. Note what happens above 1.0: the keyframe stops are
     0.10 / 0.66 / 0.28 / 1.00 / 0.38 / 0.80 / 0.20, so at amp 1.5 the 46% stop
     resolves to 1.50 and the 78% stop to 1.20, and BOTH clamp to opacity 1.0.
     The cycle's two brightest moments become the same value and the shape of the
     top of the waveform is lost — what should be a peak and a lesser peak reads
     as a plateau.
     That may be the intent, since a flattened top holds the sky bright for
     longer. If not, ~0.95 keeps nearly all of the liveliness with the waveform
     intact. Nothing clips at or below 1.0. */
  --star-twinkle-hi: 1.5;
  --star-twinkle-ms: 8200ms;

  /* ---- DESYNC -----------------------------------------------------------
     HOW FAR APART THE FOUR STAR BANDS RUN. This is the control the whole
     four-band rebuild exists to expose, and it is a true A/B:

       0 = every band on the same period with no delay. Because the four band
           images are an exact PARTITION of the old single cores layer (see
           "THE FOUR BANDS" below), desync 0 renders pixel-for-pixel what
           --star-build 6 rendered. Not approximately. Identically.
       1 = the full spread — periods 1x / 1.37x / 1.79x / 2.31x and four
           different start phases, so neighbouring stars are almost never doing
           the same thing at the same time.

     Anything between is a linear interpolation of both the periods and the
     phases. If the sky ever reads as too busy, come down this slider rather
     than reaching for the amplitude: amplitude changes how bright the twinkle
     is, desync changes whether it reads as a sky or as one object breathing.

     >>> SHIPPED AT 0 FROM BUILD 15, AND THAT IS A REVERSAL. Builds 7-14 shipped
     >>> 1 and the four-band rebuild exists specifically to make lockstep stars
     >>> impossible. Setting this to 0 puts all four bands back on one clock and
     >>> renders build 6 exactly — the "one object breathing" state the rebuild
     >>> was built to remove.
     >>>
     >>> It is the owner's call and there is a reading behind it: the drum
     >>> response moved off the stars entirely this session (--kick-stars is 0)
     >>> and onto the nebula (--kick-cloud), so the sky is now backdrop rather
     >>> than the thing carrying the rhythm, and a backdrop breathing as one
     >>> object is defensible. MEASURED while it was at 1: the four bands moved
     >>> light on 0.66% of the frame against the cloud layer's 50.6%, so the
     >>> desync was governing a fifteenth of the page's motion anyway.
     >>>
     >>> What this does NOT mean is that the four band images are dead weight.
     >>> They still render, still cost four layers, and setting this back to 1 is
     >>> a one-character change. Do not "tidy away" starfield-cores-1..4.webp on
     >>> the grounds that desync is 0. */
  --star-desync: 0;

  /* The soft glow on the nebula, independent of the star twinkle above so each
     slider does one thing: turning the stars off leaves the clouds breathing,
     and vice versa. It gets its own 1.8x while a sample plays.
     TUNED TO 0.36 on 2026-08-04, up from 0.02. HANDOFF 8 shipped it at 0.02 —
     effectively off — after trying 0.92 and finding the glow became the
     dominant light source on the page. 0.36 is the middle ground the owner
     settled on once --star-black existed to hold the floor down underneath it,
     and that pairing is the point: the clouds add light to the nebula while the
     black point removes it from everything that is not nebula, so the glow
     reads without the whole frame going milky.

     LEGIBILITY WAS RE-MEASURED AT THIS VALUE, not assumed. See the note under
     --star-black; worst case on the page is 5.45:1 against a 4.5:1
     requirement, and that worst case is text over a lit spine vertebra rather
     than over the sky.

     RETUNED 1.2 -> 0.27 on 2026-08-05, and the number went DOWN so the layer
     could do MORE. Read the next paragraph before touching it, because 0.27
     against the old 1.2 looks like the glow was turned off and it was not.

     THIS FEEDS A CSS OPACITY, WHICH CLAMPS AT 1.0. At 1.2 the layer was past
     that ceiling and three separate things were wrong with it, all measured:
       - 1.2 and 1.0 rendered IDENTICALLY at the peak of the breath.
       - 1.2 was actually WORSE than 1.0. The keyframe floor still scales with
         the value while the ceiling does not, so 1.0 swings 0.40 -> 1.00 and
         1.2 swings only 0.48 -> 1.00. A brighter floor and 13% less breath.
       - While a sample plays the 1.8x below took it to 2.16, so the kick had
         NOWHERE to push. Measured at the breath peak: a drum hit moved the
         nebula by +0.000. Not subtly, not a little. Zero.
     The brightness that used to live in this opacity now lives in the
     brightness() at the end of the cloud filter, which is not clamped. Same
     nebula, verified by bisecting for equal rendered luminance in the band, but
     with 0.51 of headroom left under the ceiling for the kick to work in.

     SO THE PAIRING IS LOAD-BEARING. Build 15 ships --star-cloud 0.2 against
     --star-cloud-bright 2.2; build 13-14 shipped 0.27 against 1.75, which was
     the calibrated match for the old 1.2 x brightness(0.85). Move one without
     the other and the nebula changes brightness. To make the nebula brighter,
     raise the brightness and leave this alone; raising this instead spends the
     headroom the kick needs — at 0.2 the ceiling for --kick-cloud is 0.64, and
     every 0.05 added here costs 0.09 of it. */
  --star-cloud: 0.2;

  /* HOW BRIGHT THE NEBULA IS, carried in the cloud filter rather than in the
     opacity above. THIS IS THE ONE TO REACH FOR when the glow is too weak or
     too strong; --star-cloud is the headroom split, not the brightness.

     WHY THE BRIGHTNESS AND THE KICK ARE ON DIFFERENT CHANNELS, because it looks
     like an inconsistency and it is not. A filter is not clamped, so brightness
     can go as high as it likes and there is no ceiling to run into. But a
     filter change REPAINTS the layer, and this layer carries a 22px blur across
     the whole viewport, so re-running the chain every frame would mean
     re-blurring every frame. The kick therefore drives the OPACITY, which the
     compositor handles without repainting anything, and pays for that with the
     1.0 ceiling that --star-cloud exists to stay under. Static brightness in
     the filter, per-frame movement in the opacity.
     DO NOT "simplify" this by moving the kick onto the brightness. It would
     remove the clamp and look correct in a still frame, and it would put a
     full-screen gaussian blur on the every-frame path. */
  --star-cloud-bright: 2.2;

  /* HOW HARD THE NEBULA ANSWERS THE KICK, per unit of envelope. Its own control
     rather than a share of --kick-stars, because one knob genuinely could not
     serve both layers: the cores clip at --star-twinkle-hi + --kick-stars, so
     filling the cloud headroom through the old `* 0.5` would have needed
     --kick-stars at 1.0, which puts the cores at 1.46 and hard against their
     own ceiling. Two layers, two ceilings, two knobs.
     0 restores a nebula that does not answer the drum at all.

     THE USEFUL RANGE ENDS BEFORE THE SLIDER DOES, AND IT MOVES WITH --star-cloud.
     Opacity while playing is --star-cloud * 1.8 + kick * this, and CSS clamps
     opacity at 1.0, so the ceiling arrives at 1 - 1.8 * --star-cloud. At the
     build-15 --star-cloud of 0.2 that is 0.64. MEASURED at the previous pairing
     (cloud 0.27, limit 0.514) by sweeping against the rendered frame: 0.50, 0.75
     and 1.00 all landed within 0.05/255 of each other.

     >>> SHIPPED AT 1, WHICH IS PAST THAT LIMIT, AND IT IS A CHOICE. Everything
     >>> above kick 0.64 lands on the same brightness, so the hardest drum hits
     >>> are compressed to a common peak — the nebula flash acts as a soft
     >>> limiter rather than tracking the envelope linearly. It only bites near
     >>> the top of the breath cycle: at the keyframe trough the opacity is
     >>> (0.36 + kick) * 0.40, which maxes at 0.544 and never clamps.
     >>> Set it to 0.64 if the loudest hits should stay proportional. */
  --kick-cloud: 1;

  /* ---- LIGHTNING (.star-bolt, end of this file) --------------------------
     STATIC BRIGHTNESS of the lightning filaments, carried in the layer's
     filter — unclamped, the same filter/opacity split the cloud layer uses:
     brightness in the filter (repaints, so it must be static), per-frame
     movement in the opacity (compositor-only). THIS is the one to raise if
     the strikes read too faint. Do NOT raise --snare-bolt for brightness —
     that is the envelope's RANGE, and spending range on brightness is how
     --star-cloud ended up parked in its dead zone.
     For scale (HANDOFF 14): at envelope 0.35 the asset adds 0.351/255 mean
     and moves light on ~3% of the frame, against the cloud layer's ~1.94/255
     on ~50%. The layer is cheap; there is room here. */
  --star-bolt-bright: 1;

  /* HOW HARD THE LIGHTNING ANSWERS THE SNARE, per unit of --snare — the
     lightning is side-chained to the SNARE, not to the kick. Two separate
     envelopes on two separate instruments: --kick moves the column, the
     shake and the nebula (--kick-cloud above); --snare strikes the
     lightning, and strikes it alone. See the snare block in css/spine-bg.css
     for how --snare is detected and the measurements behind it.
     Unlike --kick-cloud there is NO ceiling short of the slider: the layer
     idles at opacity 0 — lightning has no resting state — so nothing is
     spent on a baseline and the full 0..1 envelope range is usable. 0 is
     off; the layer stays in the tree and adds no light, which under screen
     is identical to not being there. */
  --snare-bolt: 1;

  /* The amplitude actually in force. Indirection rather than two copies of the
     animation: the keyframes below read this one property, so the playing state
     is a single re-binding and the animation never restarts. */
  --star-twinkle-amp: calc(var(--star-twinkle) * var(--df-sky, 1));
  --star-cloud-amp: calc(var(--star-cloud) * var(--df-sky, 1));

  /* --df-sky IS THE DEEP-FIELD BACKGROUND'S SUPPRESSION MULTIPLIER, and the
     fallback of 1 is what keeps every other page identical: css/deep-field-bg
     .css declares it and js/deep-field-bg.js drives it, and where neither is
     loaded var() falls back to 1 and these formulas are exactly what they were.

     IT BELONGS INSIDE THESE FORMULAS, NOT OVER THE TOP OF THEM. The lab first
     did this by re-declaring --star-*-amp in its own stylesheet, at the same
     specificity and later in the cascade, which won — and silently threw away
     everything the playing-state rules below put INTO those formulas: the 1.8
     swell, the --star-twinkle-hi lift, and the `+ var(--kick) * var(--kick-cloud)`
     term that IS the nebula answering the drum. The sky stayed suppressed
     correctly and simply stopped responding to music, which is a hard bug to
     see because the layer looks right in every still frame. Multiply, never
     replace. */

  /* HOW HARD THE SKY ANSWERS THE KICK DRUM, per unit of envelope.
     The envelope itself (--kick) is written by js/spine-bg.js and declared in
     css/spine-bg.css, which index.html alone loads — so this control lives
     here, with the layers it acts on, but the value it multiplies arrives from
     the other file. That is safe because the rule below is scoped to
     .is-spine-kicking, and only js/spine-bg.js ever sets that class, on the one
     page that loads both stylesheets. On the other four pages the rule can
     never match and --kick is never referenced.

     >>> SHIPPED AT 0 FROM BUILD 15. The star cores no longer answer the drum at
     >>> all; the whole kick response now lives in the nebula via --kick-cloud.
     >>> Everything below this line describes what the control DOES when it is
     >>> raised, and is kept because it is the map for turning it back on. It is
     >>> not describing the shipped page.
     >>>
     >>> Note also that the paragraph after next is now out of date in the other
     >>> direction: --star-twinkle and --star-twinkle-hi are NO LONGER equal.
     >>> They are 0.46 idle against 1.5 playing, so the play button moves the
     >>> stars a great deal — see the clipping note on --star-twinkle-hi.

     HISTORICAL — KEPT LOW ON PURPOSE. --star-twinkle and --star-twinkle-hi were
     set equal on 2026-08-04, which meant the play button deliberately did not
     move the stars. At that time the idle sky and the playing sky were identical
     and the only thing that moved the stars was an actual drum hit.

     IT SCALES THE WAVEFORM, IT DOES NOT ADD A FLASH ON TOP. The keyframes are
     amp x coefficient, so at 0.25 a hit multiplies the whole twinkle by 1.25 at
     every phase — MEASURED in Chrome with the animations paused and stepped
     (the getAnimations() method HANDOFF 9 documents): a trough band went
     0.376 -> 0.470 and a near-peak band 0.645 -> 0.807, both exactly x1.250.
     The consequence to know about: --star-twinkle-hi is 1 and opacity cannot
     exceed 1, so at the exact top of a band's cycle the hit is clipped and that
     band alone does not answer. With four bands on four clocks that is a small
     slice of the sky at any instant and it reads as scintillation rather than
     as a gap — but if this is ever raised a long way, that ceiling is what it
     will run into, and the fix is to lower --star-twinkle-hi, not to raise this
     further. */
  --kick-stars: 0;
}
:root.is-spine-pulsing {
  --star-twinkle-amp: calc(var(--star-twinkle-hi) * var(--df-sky, 1));
  --star-cloud-amp: calc(var(--star-cloud) * 1.8 * var(--df-sky, 1));
}
/* MUST STAY BELOW the pulsing rule — same specificity, and both classes sit on
   <html> at once while a sample plays, so source order is what decides. */
:root.is-spine-kicking {
  --star-twinkle-amp: calc((var(--star-twinkle-hi) + var(--kick) * var(--kick-stars)) * var(--df-sky, 1));
  /* The clouds get half as much. They are a 22px-blurred glow with no detail in
     them, so they read as a slow swell whatever is done to them; a transient
     there mostly just lifts the page's floor, which is the thing --star-black
     exists to hold down. The star CORES are what can actually flick. */
  --star-cloud-amp: calc((var(--star-cloud) * 1.8 + var(--kick) * var(--kick-cloud)) * var(--df-sky, 1));
}

/* A pseudo-element, so this file is the ENTIRE feature: one <link> and a page
   has stars, with no markup to add and no script to load. That matters because
   the spine is index.html-only and this is not meant to be — the other four
   pages are flat black and read as a different site.

   position: fixed rather than a document-height layer. Deep space does not
   scroll past you: one viewport-sized copy that stays put reads as distance,
   and it also sidesteps tiling a 1080px-tall frame down a 3400px document,
   where the seam would be a horizon line running across the page. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-image: url("../assets/hero/starfield-deep-4k.webp");
  background-position: 50% 50%;
  background-size: cover;
  background-repeat: no-repeat;

  /* THE BLACK POINT IS THE FIRST TWO FUNCTIONS, AND TOGETHER THEY ARE AN EXACT
     SUBTRACT-AND-CLIP. Not an approximation of one — the algebra:

         contrast(c):      out = (v - 0.5) * c + 0.5,  clamped at 0
         brightness(1/c):  out = out / c
         composed:         out = v - (0.5 - 0.5/c)     for v above that, else 0

     So with k = 0.5 - 0.5/c expressed in levels, this subtracts exactly k and
     clips at zero — the same linear subtract HANDOFF 8 used on the spine
     artwork, chosen there because "a linear subtract, not a threshold, so
     nothing bands at the clip point". Solving for c gives c = 1/(1 - 2k/255),
     and 1/c is simply (1 - 2k/255), so only one division is needed and Chrome
     resolves it (verified in a real browser: at k=6 the computed filter reads
     contrast(1.04938) brightness(0.952941), matching the arithmetic exactly).

     AT --star-black: 0 THIS IS contrast(1) brightness(1) — a true identity, not
     a near-identity. The slider's zero is build 8 pixel for pixel.

     ORDER IS LOAD-BEARING. The subtract runs BEFORE brightness(--star-dim), so
     k means "levels of the artwork" and keeps that meaning whatever the dim is
     set to. Put it after and the same k would cut a different amount every time
     --star-dim moved, which is how a control stops being tunable. */
  filter:
    contrast(calc(1 / (1 - 2 * var(--star-black) / 255)))
    brightness(calc(1 - 2 * var(--star-black) / 255))
    brightness(var(--star-dim))
    saturate(var(--star-sat));

  /* SAME REASONING AS THE SPINE LAYER, and load-bearing for the same reason.
     Half of this frame is DARKER than the page background, so painted normally
     it would drag the page's black down below #03040F everywhere the nebula is
     not — every text-shadow on the site is rgba(3,4,15,...) precisely because
     that is the floor, and moving the floor would put those shadows back above
     the background instead of invisible against it.
     screen(page, black) leaves the page untouched, so this layer can only ever
     ADD light. The frame's own black stops existing, #03040F stays the floor,
     and every contrast decision already made on this site still holds. */
  mix-blend-mode: screen;
}

/* z-index: -1 puts this behind everything, INCLUDING the spine layer, which is
   what makes the spine screen over the stars rather than the other way round.
   Do not give body a z-index or a transform: either would make body a stacking
   context, the -1 would then be trapped inside it, and the blend would resolve
   against body's own transparent backdrop instead of the page — the same trap
   documented under "NO z-index HERE" in css/spine-bg.css, one level up.

   THE SAME NOW APPLIES TO <main>, which carries two of the four band layers
   below. main is `position: static` with no transform, filter, opacity,
   isolation, contain or will-change on any page, and it must stay that way.
   Verified in a real browser on 2026-08-04 by rendering probe layers from
   html::before, main::before and main::after at once: all three composited
   against the page exactly as body::after does. If someone ever puts a
   transform on main for an entrance animation, bands 3 and 4 will silently
   stop screening against the page and start screening against main's own
   transparent box, which renders as a faint grey rectangle — the same failure
   HANDOFF 7 records for .spine-bg. */

/* ==========================================================================
   TWINKLE — four star bands, and a soft glow on the clouds.

   HISTORY, because both wrong turns are instructive and neither is obvious.

   WRONG TURN 1 (fixed 2026-08-04). The twinkle layer was MIRRORED and scaled,
   on the theory that offsetting it would stop every star pulsing in lockstep.
   It did — but at the cost of putting the winking points exactly where the sky
   has NO stars. It was not making the stars twinkle, it was inventing a second
   set of them in the gaps. Registered is the only correct answer: the light has
   to land back on the star that is already there.

   WRONG TURN 2 (fixed here, build 7). Registering it fixed the position and
   left the real complaint untouched: ONE layer means ONE opacity animation,
   so every star in the sky brightens and dims on the same clock. Worse, the
   crush is not evenly spread — MEASURED, 11 blobs larger than 1024px carry
   50.2% of the whole layer's light, and those eleven pulsing together is what
   reads as the sky breathing as a single object rather than as weather.

   THE FIX: FOUR BANDS ON FOUR CLOCKS.

   The cores are pre-split into four images, each holding roughly a quarter of
   the stars, each animated on its own period and phase. Neighbouring stars land
   in different bands, so nothing on screen shares a clock with the thing next
   to it.

   HOW THE BANDS WERE BUILT (scripts are not in the repo; this is the recipe):
     1. cores = contrast(9) applied to starfield-deep-4k.webp, per channel —
        i.e. exactly what build 6 did at runtime, now baked.
     2. Connected-component label the surviving pixels. 1143 blobs.
     3. Deal blobs largest-light-first into whichever band is currently dimmest.
        MEASURED RESULT: every band carries 25.00% of the total light, and a
        star's nearest neighbour is in the same band 27.8% of the time against
        25.0% for chance. Balanced and spatially decorrelated at once.
     4. Write each band as its own full-frame image, black everywhere else.

   NOTE ON STEP 3: HANDOFF 8 proposed splitting by BRIGHTNESS — "sample the
   cores into a handful of alpha bands". That was tried on paper and rejected:
   brightness bands put every bright star on the same clock, and the bright
   stars are exactly the ones you look at, so the lockstep would have survived
   where it is most visible. Balancing light across bands while randomising
   which band each star lands in decorrelates the thing the eye actually reads.

   LOSSLESS, NOT q90. The band images are WEBP LOSSLESS and that is
   load-bearing, not a quality preference. Lossy encoding rings around every
   star core, and ringing in a mostly-black frame is light in the EMPTY SKY —
   the precise failure mode the +0.00 rows in the table below certify against.
   Lossless also happens to be half the size here (13 KB vs 27 KB per band),
   because the frame is mostly exact black and lossy spends its bits on the
   ringing. Verified: round-trip error 0.00000, and zero lit pixels outside
   each band's own blobs. 56 KB for all four.

   WHY DESYNC 0 IS EXACTLY BUILD 6, and why that is a proof rather than a hope:
   the four images are DISJOINT and their union is byte-identical to build 6's
   crushed layer. A screen-blended element at opacity o composites to
   (1-o)*backdrop + o*screen(backdrop, source); where source is black,
   screen(backdrop, 0) = backdrop, so the whole expression collapses to
   backdrop. A band is therefore a perfect no-op everywhere it is black,
   whatever its opacity. So at any pixel exactly one band is doing anything and
   it does exactly what the single layer did. Set --star-desync: 0 and you are
   looking at build 6, not at an approximation of it.

   TWO LAYERS PER ELEMENT IS THE CEILING, AND WHY THERE ARE SIX SLOTS.
   With no markup to add, animatable full-screen layers come only from
   pseudo-elements: html::before, html::after, body::before, body::after,
   main::before, main::after. That is six, and they are all now spoken for —
   base sky, four bands, clouds. A fifth band needs either markup or the
   clouds' slot. Do not go looking for a seventh; there isn't one.

   MEASURED at 1440x900 on build 6, each layer frozen at opacity 1 against both
   off, with the delta sorted by how bright the BASE SKY already is at that
   pixel. Build 7 does not change these figures — the bands are a partition of
   the same pixels — but this is the test to repeat if either layer is touched:

     base sky band            stars      clouds
     empty field  (<p50)      + 0.00     + 2.83
     faint dust   (p50-90)    + 0.00     +11.83
     nebula       (p90-99)    + 0.00     +31.69
     star halos   (p99-99.9)  +30.18     +36.84
     STAR CORES   (>p99.98)  +113.66     + 5.53

   Read the stars column downward: the light is +113.66 on the cores, +30.18 on
   their halos, and EXACTLY ZERO on the nebula, the dust and the empty sky. It
   cannot light anything that was not already a star, which is the correction.
   The clouds column is the mirror image — broad through the dust and the
   nebula, almost nothing on the cores, which are already near clipping and
   blurred away besides.

   Concentration says the same thing another way: 70% of the star layers' light
   falls in 0.185% of the frame, against 29.868% for the clouds. Points versus
   glow. BOTH SLIDERS SHIP FAR BELOW THE AMPLITUDES THOSE FIGURES DESCRIBE:
   --star-cloud is 0.02 and --star-twinkle 0.04, so the real opacities are
   fractions of a percent and the page-black lift is nil. The table is the
   measurement of what each layer DOES at full strength — read it to understand
   which layer puts light where, not to predict the shipped page.

   Do not check any of this with getComputedStyle — as css/spine-bg.css already
   records for the playback pulse, it does not report interpolated animation
   values and reads flat while the pixels are plainly moving. Screenshot it.
   ========================================================================== */

/* The shared shape of a band. NO transform of any kind: registration with the
   base layer is wrong turn 1's correction and it applies to all four. */
body::after,
html::before,
main::before,
main::after {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-position: 50% 50%;
  background-size: cover;
  background-repeat: no-repeat;
  /* contrast(9) is BAKED into the band images, so only the two live controls
     remain. Build 6 ran saturate(sat) contrast(9) brightness(1.2) and the order
     mattered — the crush had to happen against the original levels. Baking it
     removes that trap entirely, and at the shipped --star-sat: 1 the result is
     identical because saturate(1) is the identity. Above or below 1 the slider
     now recolours the flare instead of changing which stars survive the crush,
     which is the more useful of the two behaviours anyway.

     NO --star-black HERE, deliberately. These four images are already exact
     black everywhere except the star cores — that is what makes them no-ops
     under screen — so there is no floor to subtract. Applying it would do
     nothing but dim the flares, and it would break the desync-0 identity with
     build 6. The black point is a property of the PHOTOGRAPH, and only the base
     sky and the clouds are the photograph. */
  filter: saturate(var(--star-sat)) brightness(1.2);
  mix-blend-mode: screen;
  opacity: 0;
  animation-name: star-twinkle;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
}

/* Per-band period and phase. Both are scaled by --star-desync so that 0 puts
   every band back on band 1's clock with no delay.

   The multipliers are 1 / 1.37 / 1.79 / 2.31 — no pair of them is close to a
   ratio of small integers, so the four never come back into alignment on any
   timescale anyone is going to watch. The negative delays start each band part
   way through its own cycle, so they are already spread apart on the first
   frame rather than converging into spread over the first half minute.

   ONE WAVEFORM FOR ALL FOUR, deliberately. A second set of keyframes for the
   even bands was tried on paper and dropped: it would have made --star-desync: 0
   merely similar to build 6 instead of identical to it, and that identity is
   the only reason the slider can be trusted as an A/B. */
body::after   { animation-duration: var(--star-twinkle-ms);
                animation-delay: 0ms;
                background-image: url("../assets/hero/starfield-cores-1.webp"); }
html::before  { animation-duration: calc(var(--star-twinkle-ms) * (1 + 0.37 * var(--star-desync)));
                animation-delay:    calc(var(--star-twinkle-ms) * -0.34 * var(--star-desync));
                background-image: url("../assets/hero/starfield-cores-2.webp"); }
main::before  { animation-duration: calc(var(--star-twinkle-ms) * (1 + 0.79 * var(--star-desync)));
                animation-delay:    calc(var(--star-twinkle-ms) * -0.66 * var(--star-desync));
                background-image: url("../assets/hero/starfield-cores-3.webp"); }
main::after   { animation-duration: calc(var(--star-twinkle-ms) * (1 + 1.31 * var(--star-desync)));
                animation-delay:    calc(var(--star-twinkle-ms) * -1.08 * var(--star-desync));
                background-image: url("../assets/hero/starfield-cores-4.webp"); }

/* THE CLOUDS. The same frame again, heavily blurred and held at low amplitude,
   so the nebula swells and settles as a soft glow rather than twinkling. Blur
   is what separates this from the bands: it destroys the point structure, so no
   star can wink here, and what is left is the broad shape of the dust. Scaled
   1.06 only because blur() pulls transparency in from outside the element and
   would otherwise leave the glow thinning at the viewport edges — at 22px of
   blur there is no point structure left to misregister, so this costs nothing
   visually — but see the frame-rate note below for what it used to cost.

   Its period is 2.7x the base, unrelated to the band multipliers, so it drifts
   against all four.

   THE OVERSCAN IS `inset: -3%`, NOT `transform: scale(1.06)`. Those two are
   algebraically identical — both give a fixed box 106% of the viewport in each
   axis, centred, and the measured geometry matches to 0.1px. Keep the inset:
   it is free and it is identical, so it costs nothing to keep whatever the
   numbers below turn out to mean.

   >>> CORRECTION 2026-08-05 — EVERY FRAME-TIME NUMBER BELOW WAS TAKEN ON A
   >>> SOFTWARE RENDERER. chrome://gpu on this machine read "Compositing:
   >>> Software only", GL_RENDERER "Microsoft Basic Render Driver", "Software
   >>> Rendering: Yes". The owner confirms hardware acceleration had been
   >>> DISABLED IN CHROME THE ENTIRE TIME. So "real GPU" below is wrong: these
   >>> are CPU compositing times through WARP, on a machine whose actual card is
   >>> an RTX 3090 Ti. An 88fps ceiling is plausible for WARP on 24 threads and
   >>> is a strange ceiling for a 3090 Ti — that ceiling was the tell and nobody
   >>> read it.
   >>>
   >>> RE-MEASURED 2026-08-05 with acceleration ON, three ways, all null:
   >>>   vsync-capped   all 11 conditions pinned at 4.2ms / 238fps, spread <0.1ms
   >>>   uncapped       all 8 ANIMATING conditions at 1.8ms, identical — removing
   >>>                  every blend mode, every filter, the blur, the clouds, or
   >>>                  four of the six layers changed nothing measurable
   >>>   traced         6 render surfaces from mix-blend-mode, 8 render passes per
   >>>                  frame vs 2 without the field (Chromium 141, structural)
   >>> The star field's cost on this GPU is below every floor that could be
   >>> built for it. See HANDOFF 11 and scripts/measure-stars.js.
   >>>
   >>> WHAT THIS DOES NOT SAY: it does not say the transform was free. It says
   >>> the transform-vs-inset gap has never been measured on a GPU. Treat the
   >>> table below as WARP data, not as hardware data.

   MEASURED ON WARP, 2026-08-04, 1920x855 at dpr 1, 220 frames per run, each
   configuration run twice:

       transform: scale(1.06)      63 fps    p95 frame 29.9ms
       inset: -3%, no transform    88 fps    p95 frame 14.1ms

   That was A THIRD OF THE PAGE'S FRAME RATE, and at the time it was found this
   layer was running at --star-cloud: 0.02 — close enough to off that HANDOFF 8
   called it "effectively off". The page had been paying a third of its frame
   budget for something invisible. (--star-cloud is 0.36 now, so the layer earns
   its keep; the fix below is what made that affordable.) The cost was isolated
   by elimination and it is specifically the transform:

       blur removed entirely       63 fps    no help
       blur reduced to 8px         63 fps    no help
       animation paused            63 fps    no help
       transform removed           89 fps    full recovery
       display: none               88 fps    same as removing the transform

   So — under CPU compositing — it is not the 22px blur, not the animation and
   not the layer's existence; a transform on this pseudo-element takes it off
   the cheap path and nothing else does. DO NOT PUT A TRANSFORM BACK HERE
   anyway: the inset is algebraically identical and provably free, so there is
   no upside to the transform even if the gap turns out to be a WARP artifact.
   If the glow ever needs more overscan, make the inset more negative.

   The same question is worth asking of any other always-on transformed layer on
   this page; this one was found only because the four-band work prompted a
   frame-time baseline, and it had been shipping since build 5. */
html::after {
  content: "";
  position: fixed;
  inset: -3%;
  z-index: -1;
  pointer-events: none;
  background-image: url("../assets/hero/starfield-deep-4k.webp");
  background-position: 50% 50%;
  background-size: cover;
  background-repeat: no-repeat;
  /* The clouds carry the same black point as the base sky, and since
     --star-cloud was tuned up to 0.36 this is doing visible work rather than
     sitting there for consistency. The two layers are the same photograph; a
     black point on one and not the other would show up as the glow having a
     different floor from the sky it sits on. */
  filter:
    contrast(calc(1 / (1 - 2 * var(--star-black) / 255)))
    brightness(calc(1 - 2 * var(--star-black) / 255))
    /* brightness 0.85 -> 1.75 on 2026-08-05. THIS IS WHERE THE NEBULA'S
       BRIGHTNESS LIVES NOW, and the move was the whole point rather than a
       taste change: a filter is not clamped, an opacity is. Carrying the
       brightness here instead of in --star-cloud leaves the opacity free to be
       the kick channel, which is the only way the drum can reach this layer at
       all. See the long note on --star-cloud.
       CALIBRATED, not picked: bisected for equal mean luminance over the top
       decile of cloud light, so 0.27 x brightness(1.75) renders the nebula the
       same as the old 1.2 x brightness(0.85) did at its playing peak.
       PAIRED WITH --star-cloud. Changing either alone changes the nebula.
       On a slider as `cloud b`, since hard-coding it took the nebula's
       brightness off the tuning panel entirely, which --star-cloud used to be
       the control for. */
    saturate(var(--star-sat)) blur(22px) brightness(var(--star-cloud-bright));

  /* ---- THE CLOUD MASK, hand-painted 2026-08-05 -------------------------------
     The owner's call: the glow spreading out into the empty sky read as haze
     rather than as nebula. This keeps the swell on the nebula's own diagonal
     band and removes it everywhere else. THE BASE SKY IS DELIBERATELY NOT
     MASKED — body::before still paints the full photograph, so what goes away
     is the glow and the breathing, not the structure. If the shape itself ever
     needs to go, the same file drops onto body::before with no repainting.

     ALPHA, NOT LUMINANCE. The file is white everywhere with the mask carried
     entirely in its alpha channel, so the default mask-mode is correct and no
     mask-mode declaration is needed. Do not "simplify" it to a greyscale image
     without adding mask-mode: luminance — an opaque greyscale file under the
     default alpha mode is a no-op, and a no-op mask looks exactly like a mask
     that failed to load.

     1920x1072 IS EXACTLY HALF THE SKY'S 3840x2144, and that is the whole reason
     for the number. Identical aspect means `cover` crops the mask and the
     photograph to the same rectangle at EVERY window shape, so registration is
     structural rather than lucky — VERIFIED at 820x1180 portrait and 2560x1080
     ultrawide, where cover crops width and height respectively. Any resize that
     changes the aspect, even by a rounding error, makes the mask slide against
     the nebula as the window is resized. Halve or double; do not eyeball it.

     THE PAINTED FILE WAS 3840x2144 AND IS MASSIVELY OVERSAMPLED FOR THIS JOB.
     MEASURED on the source: the feather is 361px wide at the median and 73px at
     its sharpest tenth. Downscaling to 1920 costs a maximum alpha error of
     5/255 and takes the file from 587 KB to 158 KB. Lossless webp, because the
     mask is one long soft gradient and that is exactly where lossy alpha bands.

     WHY THE FEATHER HAD TO BE THAT WIDE. CSS applies the mask AFTER the filter,
     so the 22px blur above is already baked in when the mask cuts. 22 CSS px is
     about 52 px in the source file at a 900px-tall viewport, so a mask edge
     tighter than roughly 3x that reads as a hole punched in a soft glow rather
     than as the nebula thinning out. The painted edges clear it everywhere.

     The kept region tops out at alpha 251, not 255 — a brush artifact, left as
     painted. It costs the surviving clouds about 1.6% of their strength, which
     is below anything --star-cloud can resolve. */
  -webkit-mask-image: url("../assets/hero/cloud-mask.webp");
          mask-image: url("../assets/hero/cloud-mask.webp");
  -webkit-mask-size: cover;      mask-size: cover;
  -webkit-mask-position: 50% 50%; mask-position: 50% 50%;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;

  mix-blend-mode: screen;
  opacity: 0;
  animation: cloud-glow calc(var(--star-twinkle-ms) * 2.7) ease-in-out infinite;
}

/* ==========================================================================
   LIGHTNING — filaments discharging in the nebula on the SNARE.

   ARTWORK: assets/hero/nebula-lightning-4k.webp — 3840x2144, 52 KB, built in
   HANDOFF 14 as a FILAMENT-ONLY DELTA, not a nebula+filaments composite: the
   owner's target image registered to this site's own sky (scale 0.60650,
   offset (28, 326), NCC 0.76-0.82), the sky subtracted out, and only the
   filaments kept. 96.72% of the frame is exact black, residual floor 0.000
   per channel, so no --star-black treatment is needed — there is no floor to
   subtract, same as the four band images. It was multiplied by
   cloud-mask.webp at build time, so it is PRE-CONTAINED: it cannot leak
   light outside the nebula whatever the opacity does. Filament hue
   211.6-213.5 against the cloud layer's post-filter 212.9 — the nebula's own
   cold blue. The ~70% coverage gap this note used to file as a KNOWN GAP is
   now half-closed: pattern c below puts the first filaments in the outer
   wing (HANDOFF 16).

   REAL ELEMENTS, NOT PSEUDO-ELEMENTS. All six full-viewport pseudo slots
   are taken — base sky, four bands, nebula (see "TWO LAYERS PER ELEMENT IS
   THE CEILING" above). The two <div class="star-bolt"> pattern layers are
   injected as body children by js/spine-bg.js, beside the .spine-bg build,
   so this file stays one <link> with no markup. PROVEN ON REAL HARDWARE (HANDOFF 14 §4): a
   fixed body child at z-index -1 with mix-blend-mode screen composites
   identically to body::before — no grey rectangle, blacks held. Do NOT
   borrow a section's pseudo-element instead: .track-experience, .newsletter,
   .footer and main > .section are real stacking contexts, and a screened
   pseudo inside one blends against that box's transparent backdrop and
   renders as a faint grey rectangle — the HANDOFF 7 trap.

   3840x2144 EXACTLY MATCHES starfield-deep-4k.webp, and that is structural,
   not a coincidence — identical aspect means `cover` crops both to the same
   rectangle at every window shape, so the filaments cannot slide against the
   nebula on resize. Same rule as cloud-mask.webp: halve or double, never
   resize to anything else. If overscan is ever needed, make the inset
   negative like the cloud layer's — never a transform.

   IDLE IS OPACITY 0, AND ONLY THE SNARE RULE BELOW EVER RAISES IT — and
   only on the div carrying .is-struck. The opacity is the snare channel
   (compositor-only, clamped at 1.0, which idle-0 leaves entirely free); the
   brightness is static in the filter (unclamped, but a change repaints).
   Same discipline as the cloud layer — this one carries no blur so the
   repaint would be cheaper, but keep the split. */
.star-bolt {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-position: 50% 50%;
  background-size: cover;
  background-repeat: no-repeat;
  filter: brightness(var(--star-bolt-bright));
  mix-blend-mode: screen;
  opacity: 0;
}

/* FIVE STRIKE PATTERNS since build 20 — js/spine-bg.js injects five divs
   sharing the .star-bolt class (see the long note there for why sharing the
   class is load-bearing: STAR_LAYERS, the view modes and the reduced-motion
   rule all address the lightning by it). Each carries one pattern; only the
   one with .is-struck lights, and the machine moves that class per strike —
   random, never the same twice in a row (the two-pattern builds' strict
   alternation was this rule's reduction; five patterns restore the random
   pick). THE SLOT LETTER b IS RETIRED, NOT REUSED: a second diagonal
   pattern shipped in build 18, was re-contained three times against the
   owner's reference, and was removed by his eye in the same session —
   HANDOFF 16 has the story. A future sixth pattern takes g.

   ALL ASSETS ARE THE SAME MACHINE (scripts/bolt-extract.py — c was built by
   its generated-candidate path in HANDOFF 16; a is the HANDOFF 14 original
   that path recreates; d/e/f came from its REFERENCE-FRAME path the session
   after, extracted directly from the owner's three target frames):
   filament-only deltas registered to this sky, pre-contained by
   cloud-mask.webp at build time AND by a visible-glow ramp. The two paths
   ramp differently ON PURPOSE — 45->85 g2 corrals generator-invented
   geometry (a/c), 25->60 g1 only de-noises owner-supplied geometry (d/e/f);
   the script's reference-mode header carries the measured reasons. Near-full
   black floor (min nonzero 1), no --star-black treatment needed. Same
   3840x2144 as starfield-deep-4k.webp — halve or double, never resize to
   anything else (the `cover` alignment argument in the header note above).

     a  the HANDOFF 14 original     96.72% black, 3.14% of frame lit
     c  the OUTER WING              97.94% black, 2.06% lit — the first
        filaments in the upper-right arm, a burst inside the arm's white
        cloud mass, closing part of HANDOFF 14's ~70% coverage gap. Its
        painted core came back warm cream and was recolored to the
        filament cold (0.72/0.82/1.0) by the extraction's size-aware hue
        gate; its containment was tightened three times against the
        owner's reference — scripts/bolt-extract.py steps 4 and 6b carry
        both stories with the measurements.
     d  SPARSE CRACKLE around the left core (target frame -2)
                                    98.45% black, 1.34% lit
     e  the HEAVIEST — veins threading the whole band (frame -3)
                                    97.82% black, 1.89% lit
     f  the UPPER-RIGHT ARM emphasis (frame -4)
                                    97.09% black, 2.56% lit
        d/e/f are the frames' own vein networks: screen-inverse delta,
        median-isolated veins, synthesized discharge halo, recolored to the
        same filament cold, cores normalized to ~245 after the 4K warp so
        no pattern begs at the shared `bolt b` slider. The pattern letters
        name the frames' states, and the STATES DIFFER IN COVERAGE ON
        PURPOSE — d is sparse because its reference is sparse; do not
        "even them out". */
.star-bolt--a { background-image: url("../assets/hero/nebula-lightning-4k.webp"); }
.star-bolt--c { background-image: url("../assets/hero/nebula-lightning-c-4k.webp"); }
.star-bolt--d { background-image: url("../assets/hero/nebula-lightning-d-4k.webp"); }
.star-bolt--e { background-image: url("../assets/hero/nebula-lightning-e-4k.webp"); }
.star-bolt--f { background-image: url("../assets/hero/nebula-lightning-f-4k.webp"); }

/* THE LIGHTNING ANSWERS THE SNARE — var(--snare), a SECOND envelope written
   by the same frame loop that writes --kick, with its own detector (2.5kHz
   noise + 200Hz body coincidence, ±45ms kick veto — the long note lives with
   the --snare declarations in css/spine-bg.css). Do not "unify" this with
   --kick: the kick moves the column, the shake and the nebula; the snare
   strikes the lightning. Two instruments, two channels, on purpose.

   SCOPED TO :root.is-spine-kicking ON PURPOSE — a stated decision, not an
   accident. The class means "the detector loop is running" (one loop runs
   both machines; there is no separate is-snare class, deliberately — the two
   envelopes start and stop together). It only ever appears on index.html:
   the detector attaches to .track-experience, which no other page has, so an
   unscoped rule would read an undefined --snare everywhere else. Scoped, the
   rule can never match there, --snare is never referenced, and with idle
   opacity 0 the layer is simply invisible on the other pages (about.html
   injects the div too — spine-bg.js runs wherever data-spine-from exists —
   and it stays black).
   No 1.8x pulsing baseline and no resting term here, unlike the cloud rule
   above: lightning has no resting state, which is exactly why the full
   envelope range is usable.

   .is-struck GATES WHICH PATTERN LIGHTS (build 18): the two divs without it
   stay at the base rule's opacity 0 while --snare drives the one that has
   it. The class moves BETWEEN strikes, not per frame — the per-frame work
   is still only this one compositor-cheap opacity. */
:root.is-spine-kicking .star-bolt.is-struck {
  opacity: calc(var(--snare) * var(--snare-bolt));
}

/* Uneven keyframe spacing, so one cycle does not read as a sine wave. Nothing
   here touches background-position: drifting stars look like a moving camera,
   not like atmosphere. Opacity only, which the compositor handles without
   repainting any layer. */
@keyframes star-twinkle {
  0%   { opacity: calc(var(--star-twinkle-amp) * 0.10); }
  17%  { opacity: calc(var(--star-twinkle-amp) * 0.66); }
  29%  { opacity: calc(var(--star-twinkle-amp) * 0.28); }
  46%  { opacity: calc(var(--star-twinkle-amp) * 1);    }
  61%  { opacity: calc(var(--star-twinkle-amp) * 0.38); }
  78%  { opacity: calc(var(--star-twinkle-amp) * 0.80); }
  90%  { opacity: calc(var(--star-twinkle-amp) * 0.20); }
  100% { opacity: calc(var(--star-twinkle-amp) * 0.10); }
}

/* The clouds only swell and settle — a plain two-stop breath, never down to
   nothing, because a nebula that switches off is not a glow. */
@keyframes cloud-glow {
  0%, 100% { opacity: calc(var(--star-cloud-amp) * 0.40); }
  50%      { opacity: calc(var(--star-cloud-amp) * 1); }
}

/* --------------------------------------------------------------------------
   PER-PAGE OVERRIDES FOR THE SKY. These are the first page blocks this file
   has ever carried — until build 21 the only page blocks lived in
   css/spine-bg.css, whose long note above html.page-home explains the whole
   architecture (why the class sits on <html> and never <body>, why inline
   tuner writes still win, and the specificity trap the media block below
   exists to defuse). ALL FIVE BLOCKS SHIP EMPTY, same convention as
   html.page-home when it first landed: a page appears here only when it
   should actually differ from the :root baseline, and an empty block is the
   hook that lets it be tuned later without touching every other page.
   archive.html and transmissions.html link this stylesheet as of build 21,
   transmissions/001.html as of build 23, archive/artwork/001.html as of
   build 24, merch.html as of build 25 — the sky is the same four bands +
   nebula everywhere, one <link>, no markup.
   THESE BLOCKS MUST STAY ABOVE THE @media (max-width: 600px) BLOCK BELOW.
   The first value that ever lands in one of them is (0,2,0) against the
   media block's need to win at equal specificity by source order — page
   blocks below the media block would beat the phone values silently.
   -------------------------------------------------------------------------- */
html.page-archive {
  /* archive.html. Sky only — no spine, no lightning: that page does not link
     css/spine-bg.css or js/spine-bg.js, the owner's call, 2026-08-07. */
}

html.page-transmissions {
  /* transmissions.html. Same deal as archive above. */
}

html.page-transmission-detail {
  /* transmissions/001.html — a single transmission, the site's only LONG-FORM
     READING page. Sky only, same as its index above: no spine, no lightning,
     no player. It does not link css/spine-bg.css or js/spine-bg.js, so
     .is-spine-pulsing and .is-spine-kicking can never appear here and the
     sky twinkles at the idle amplitude forever — correct, not a bug, per the
     note beside --star-twinkle.

     THIS BLOCK IS EMPTY ON PURPOSE, and that IS the "same visual family as
     its index" instruction carried out literally: html.page-transmissions
     above is itself empty, so transmissions.html renders the :root baseline,
     and the only way for the detail page to match it exactly is to render the
     same baseline. Copying numbers down from that block was considered and is
     not possible — there are no numbers in it.

     ON THE LEGIBILITY QUESTION, since this is the first page here that is
     body copy rather than cards. The obvious move is a calmer sky — a lower
     --star-dim, or a lower --star-cloud-bright. It was NOT made, and the
     reason is that nobody has measured this page:
       - The 4.5:1 check recorded under --star-cloud was run on 2026-08-04 at
         the OLD cloud pairing (1.2 x brightness(0.85)), not the shipped
         0.2 x 2.2, and it was run against index.html's layout. Its worst case
         — 5.45:1 — was text over a lit spine vertebra, and this page has no
         spine at all, so its floor is strictly better than that number. That
         is an argument for the baseline being safe here, not a measurement of
         it.
       - A guessed number reads as a decision. HANDOFF 15 records that rule
         for exactly this situation; an unmeasured calm-down would be
         indistinguishable from a tuned one to whoever reads this next.
     SO: if the sky does turn out to sit too loud under the body copy, the
     lever is --star-dim (baseline 1.4) for the whole field, or
     --star-cloud-bright (baseline 2.2) for the nebula alone — NOT
     --star-cloud, which is the kick-headroom split and means nothing on a
     page with no kick. Put the value here, in this block, and leave
     transmissions.html on the baseline: the two pages are allowed to differ
     if a reading page needs it, but that difference has to be a decision
     someone made with their eyes on it.

     FIRST PAGE IN A SUBDIRECTORY TO LINK THIS FILE, and nothing about that
     breaks: every url() here is relative to the STYLESHEET, not to the
     document, so ../assets/hero/... resolves to the same files from
     /transmissions/ as it does from /. The document's own link is
     ../css/star-bg.css and that part is 001.html's business.
     001.html does carry a <main>, which matters — bands 3 and 4 are
     main::before / main::after, so a page without one would silently render
     half the stars. Any future detail page needs it too. */
}

html.page-artwork-detail {
  /* archive/artwork/001.html, added build 24. Empty, same as the three above:
     nothing here should differ from the :root baseline yet, and this is the
     hook that lets it be tuned when someone has actually looked at it.

     THE CHECK THAT FOUND THIS PAGE is worth keeping. It was NOT found by
     reading the handoff, which names five production pages and does not
     include this one, and it was NOT found by measuring the pages that were
     already known. It came out of a sweep of every .html in the repo asking
     one question: does this file link css/star-bg.css? That is the only
     condition that makes a page black — the page class is a tuning hook and
     an absent block changes nothing (index.html has no block in this file at
     all and renders the sky fine). Two sibling pages are still black by that
     test and are deliberately left alone for now: music.html, which nothing
     in the repo links to, and hero-timeline-lab.html, which declares
     class="page-home" and then never links this file — a half-wire, not a
     decision.

     It carries <main id="main">, so bands 3 and 4 resolve. Its link is
     ../../css/star-bg.css: TWO hops, unlike transmissions/001.html's one. */
}

html.page-merch {
  /* merch.html, added build 25. Empty for the same reason as the four above:
     nothing here should differ from the :root baseline yet, and this is the
     hook that lets it be tuned once someone has actually looked at the page.

     THIS ONE ARRIVED BY A DIFFERENT ROUTE, and that is worth stating because
     it changes how much this block is allowed to claim. Builds 23 and 24 came
     out of a repo-wide sweep asking one question — does this file link
     css/star-bg.css? — so both pages existed, rendered black, and were
     measured. merch.html was ANNOUNCED instead: it is being authored in the
     same session as this block, so at the time of writing there is nothing on
     disk to look at and no rendered frame to judge. Any number put here would
     therefore be invented, which is the one move this file's history rules
     out — see the legibility note in html.page-transmission-detail above, and
     HANDOFF 15's rule that a guessed number reads as a decision.

     TWO THINGS THE PAGE ITSELF MUST CARRY, neither of which this block can
     supply, and both of which fail silently:
       - a <link> to css/star-bg.css. THAT is what makes a page not-black, not
         the page class: index.html has no block in this file at all and
         renders the sky fine, while the pages found in builds 23 and 24 had
         classes' worth of intent and no link. merch.html is root-level, so it
         spells a plain css/star-bg.css — no ../ hops, unlike
         transmissions/001.html's one or archive/artwork/001.html's two.
       - a <main>. Bands 3 and 4 are main::before / main::after, so a page
         without one renders half the stars with balanced braces and nothing
         in the console.

     SKY ONLY IS THE EXPECTED SHAPE, same as archive.html and
     transmissions.html: a shop page has no player, so .is-spine-pulsing and
     .is-spine-kicking can never appear, --kick and --snare are never
     referenced, and the sky twinkles at the idle amplitude forever. That is
     correct, not a bug — the note beside --star-twinkle says so at length. If
     merch.html turns out to link css/spine-bg.css as well, that file wants its
     own page-merch block; this one governs the sky and nothing else.

     WHEN SOMEONE DOES PUT EYES ON IT: this page will carry product
     photography, which none of the pages above do, and a photograph sitting on
     a photograph is where the sky is likeliest to intrude. The levers are
     --star-dim (baseline 1.4) for the whole field and --star-cloud-bright
     (baseline 2.2) for the nebula alone — NOT --star-cloud, which is the
     kick-headroom split and means nothing on a page with no kick. Put the
     value here and leave the other pages on the baseline. */
}

html.page-home {
  /* index.html, tuned Aug 17 2026 at /?tune on the real page. Until now this
     file had NO page-home block at all — the note above html.page-archive says
     a page appears here only when it actually needs to differ, and index.html
     had been riding the baseline since the file was written. These are the two
     values that finally made it differ; the Copy CSS printed thirteen and the
     other eleven were byte-identical to :root and were dropped on the way in.

     THIS BLOCK MUST STAY ABOVE THE @media (max-width: 600px) BLOCK BELOW, which
     is not a style preference. `html.page-home` is (0,2,0) and a media query
     adds nothing to specificity, so the only thing keeping the phone values
     winning is that the mobile block names this same class and comes later in
     source order. That matters here concretely: the mobile block sets
     --star-cloud-bright to 1.27, and if this block were moved below it, 2.1
     would ride all the way down to 390px with no error and nothing in the
     console. */

  /* THE FOUR BANDS DESYNC AGAIN ON THE HOME PAGE. The baseline sits at 0 with a
     long banner explaining that the union of the four disjoint images is
     byte-identical to the single crushed layer, so 0 costs nothing visually —
     and, per that same banner, 1 is a one-character change back. This is that
     change, scoped to one page. Everything the banner says still holds: the
     four starfield-cores-*.webp files are load-bearing at BOTH settings and
     must not be tidied away. */
  --star-desync: 1;

  /* Nebula brightness, a hair under the 2.2 baseline. Note it is the FILTER's
     brightness and not the opacity — the note beside the baseline explains why
     the kick must not be moved onto it, and that reasoning is unaffected by
     the value. */
  --star-cloud-bright: 2.1;
}

@media (max-width: 600px) {
  /* THE SELECTOR LIST IS LOAD-BEARING — do not shorten it back to `:root`.
     index.html and about.html carry a page class on <html> (page-home /
     page-about) selecting per-page override blocks; see the long note beside
     those blocks in css/spine-bg.css for why the class sits on html and not on
     body. `:root` IS html, so `html.page-home` selects the same element one
     class higher — (0,2,0) against (0,1,0) — and a media query adds NOTHING to
     specificity. With a bare `:root` here, the first star variable anyone put
     in a page block would outrank its phone value and the desktop number would
     apply at 390px, with balanced braces and nothing in the console to say so.
     That is the trap the re-derivation note below records, arriving by a new
     route. The /?tune panel's Copy CSS emits page-scoped blocks for this file
     too, so star variables WILL land in them. Naming the page classes here puts
     them at equal specificity and later in source order, so the phone values
     win. Add any new page class to this list in the same commit. */
  :root, html.page-home, html.page-about, html.page-archive, html.page-transmissions,
  html.page-transmission-detail, html.page-artwork-detail, html.page-merch,
  html.page-connect {
    /* RE-DERIVED 2026-08-07, the whole block at once, against the desktop
       values shipping in build 22 — every number below is a documented ratio
       times TODAY'S desktop value, not an edit of the previous phone number.
       The previous block was the stale set HANDOFF 14 records (three values
       inverted); each ratio's origin is noted at its line. Verified at a real
       390x844 viewport this time (headless Chromium, computed values + pixel
       diff) — but still not seen on the owner's actual phone, so these are
       calibrated, not approved. */
    /* cover on a 390px-wide viewport crops to the middle 20% of the frame,
       which is the densest part of the nebula band — so the phone gets MORE
       nebula per screen than the desktop does, not less. Down, not up.
       0.575 x desktop 1.4 = 0.805 -> 0.8. The 0.575 is the ratio the first
       pass chose (0.46 against the then-desktop 0.8). */
    --star-dim: 0.8;
    /* The phone crop lands on the densest part of the nebula band, so the same
       amplitude reads as more movement on a smaller screen.
       Ratios 0.75 / 0.78, from the first pass (0.03/0.04 and 0.25/0.32):
       0.75 x 0.46 = 0.345 -> 0.35, and 0.78 x 1.5 = 1.17. The previous pair
       (0.75 / 0.75) was re-derived 2026-08-04 when the desktop pair was held
       equal at 1 / 1; build 15 split the desktop pair again (0.46 idle vs 1.5
       playing), which made the old phone pair BOTH wrong at once — idle 1.6x
       brighter than desktop, and the play button a no-op because hi equalled
       idle. Restoring the ratios restores both: idle drops below desktop, and
       the play lift is back (3.3x, against the desktop's 3.26x). At 1.17 the
       46% keyframe stop clips to 1.0 — one peak flattened, against the
       desktop's two; see the clipping note beside --star-twinkle-hi above. */
    --star-twinkle: 0.35;
    --star-twinkle-hi: 1.17;
    /* --star-desync is NOT overridden. The crop shows fewer stars, which makes
       lockstep MORE obvious rather than less, so the phone wants at least as
       much spread as the desktop. */
    /* --star-cloud is NOT overridden any more — the cut moved channels.
       The previous block held --star-cloud: 0.21, scaled off a desktop 0.36
       that had since fallen to 0.2, so the phone had quietly gone BRIGHTER
       than the desktop; and the 2026-08-05 stale note here pointed out that
       the real lift had meanwhile moved into the cloud filter's brightness(),
       which no opacity number can undo. Since build 15 the pairing note at
       --star-cloud is explicit: the opacity is the KICK-HEADROOM SPLIT, the
       brightness is the brightness. The phone wants less nebula LIGHT, not a
       different headroom split — so the override now lives one line down, on
       the channel that means brightness, and the split (and with it the
       --kick-cloud ceiling math on index.html) is identical at every width.
       0.575 x desktop 2.2 = 1.265 -> 1.27, the same ratio the first pass
       applied when the cut lived on the opacity. */
    --star-cloud-bright: 1.27;
    /* --star-black is deliberately NOT overridden. It is a property of the
       photograph, not of the viewport, and the crop does not change where the
       artwork's floor sits. */
  }
}

@media (prefers-reduced-motion: reduce) {
  /* No twinkle at all. It is decorative movement with no information in it —
     the same call css/spine-bg.css makes for the playback pulse. The layers are
     left in the tree at opacity 0 rather than display:none so that turning
     reduced motion off does not require a reload. */
  /* .star-bolt carries no animation — its opacity is driven by the snare
     envelope, whose detector loop js/spine-bg.js already refuses to attach
     under reduced motion and stops live if the preference flips mid-session.
     It is named
     here anyway, with !important against the kick rule above, so the CSS
     states the decision on its own: a view mode or a future rule that lit the
     bolt would otherwise leave it as the one moving layer on a page where
     every other motion feature has shut itself off. */
  body::after, html::before, main::before, main::after,
  html::after { animation: none; opacity: 0; }
  .star-bolt { opacity: 0 !important; }
}
