/* ==========================================================================
   Star horizon 1.0 — animated space background
   --------------------------------------------------------------------------
   Plain CSS. No framework, no build step, no external request.

   Eight stacked layers, painted back to front: a base haze, an aurora that
   sways above the horizon, three star fields drifting at three speeds, the
   black disc of a planet with a lit edge, a vignette, and film grain.

   Everything moves with CSS animations alone. The script is an enhancement —
   it maps two numeric knobs onto the curves of the original, can replace the
   star texture with a denser generated one, and stops the animation when the
   block leaves the screen. Without it the background looks and moves exactly
   the same.

   PROPORTIONS
   The component has no text, so the module is a length of its own:
   --sh-scale, 16px by default. Every fixed measurement of the original is
   expressed as a multiple of it, so a single variable rescales the whole
   effect — the drawing included, not just one part of it:

     module (--sh-scale)      1                 = 16px
     aurora blur              1.25              = 20px
     aurora sway              1.875             = 30px (up and down)
     star tile, far field    25                 = 400px
     star tile, mid field    31.25              = 500px
     star tile, near field   37.5               = 600px
     horizon shadow 1         0.125 / 0.9375    = -2px offset, 15px blur
     horizon shadow 2         0.5   / 2.5       = -8px offset, 40px blur
     horizon shadow 3         1.25  / 6.25      = -20px offset, 100px blur
     horizon lit edge         0.0625            = 1px

   Everything else in the original is already a ratio and stays one: the
   aurora spreads 25% beyond each edge, the star fields 10%, the planet is
   160% wide and 120% tall, pinned 85% of the height below the bottom edge,
   which puts its lit edge at exactly 65% of the height — where the aurora is
   centred. Those hold at any size.

   Order of this file:
     1. Textures
     2. Design tokens
     3. Block, sky, content
     4. Layer 0 — base haze
     5. Layer 1 — aurora
     6. Layers 2 to 4 — star fields
     7. Layer 5 — planet horizon
     8. Layers 6 and 7 — vignette and grain
     9. Motion
    10. Loop modes: seamless and raw
    11. Variants
    12. Fallbacks: reduced motion, forced colours, print

   Naming follows BEM: .star-horizon, .star-horizon__part,
   .star-horizon--variant.
   ========================================================================== */

/* ==========================================================================
   1. Textures
   --------------------------------------------------------------------------
   Two SVG data URIs, kept in base64 exactly as the original ships them —
   no request leaves the page. Decoded, they read:

   Stars (one tile, eight dots on a 512 grid):
     <svg width='512' height='512' viewBox='0 0 512 512'
          xmlns='http://www.w3.org/2000/svg'><g fill='#FFF'>
       <circle cx='56'  cy='67'  r='1.5' opacity='0.6'/>
       <circle cx='230' cy='45'  r='1'   opacity='0.4'/>
       <circle cx='450' cy='120' r='1.5' opacity='0.7'/>
       <circle cx='120' cy='280' r='1'   opacity='0.5'/>
       <circle cx='340' cy='320' r='1.5' opacity='0.6'/>
       <circle cx='80'  cy='450' r='1'   opacity='0.4'/>
       <circle cx='480' cy='420' r='1.5' opacity='0.7'/>
       <circle cx='260' cy='180' r='1'   opacity='0.5'/>
     </g></svg>

   Grain (fractal turbulence, tiled):
     <svg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'>
       <filter id='noiseFilter'>
         <feTurbulence type='fractalNoise' baseFrequency='0.8'
                       numOctaves='3' stitchTiles='stitch'/>
       </filter>
       <rect width='100%' height='100%' filter='url(#noiseFilter)'/>
     </svg>

   The star tile is a variable, so a page can swap it — the script does
   exactly that when data-sh-stars="seeded" asks for a denser field.
   ========================================================================== */

.star-horizon {
  --sh-star-texture: url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nNTEyJyBoZWlnaHQ9JzUxMicgdmlld0JveD0nMCAwIDUxMiA1MTInIHhtbG5zPSdodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2Zyc+PGcgZmlsbD0nI0ZGRic+PGNpcmNsZSBjeD0nNTYnIGN5PSc2Nycgcj0nMS41JyBvcGFjaXR5PScwLjYnLz48Y2lyY2xlIGN4PScyMzAnIGN5PSc0NScgcj0nMScgb3BhY2l0eT0nMC40Jy8+PGNpcmNsZSBjeD0nNDUwJyBjeT0nMTIwJyByPScxLjUnIG9wYWNpdHk9JzAuNycvPjxjaXJjbGUgY3g9JzEyMCcgY3k9JzI4MCcgcj0nMScgb3BhY2l0eT0nMC41Jy8+PGNpcmNsZSBjeD0nMzQwJyBjeT0nMzIwJyByPScxLjUnIG9wYWNpdHk9JzAuNicvPjxjaXJjbGUgY3g9JzgwJyBjeT0nNDUwJyByPScxJyBvcGFjaXR5PScwLjQnLz48Y2lyY2xlIGN4PSc0ODAnIGN5PSc0MjAnIHI9JzEuNScgb3BhY2l0eT0nMC43Jy8+PGNpcmNsZSBjeD0nMjYwJyBjeT0nMTgwJyByPScxJyBvcGFjaXR5PScwLjUnLz48L2c+PC9zdmc+");

  --sh-grain-texture: url("data:image/svg+xml;base64,PHN2ZyB2aWV3Qm94PScwIDAgMjAwIDIwMCcgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJz48ZmlsdGVyIGlkPSdub2lzZUZpbHRlcic+PGZlVHVyYnVsZW5jZSB0eXBlPSdmcmFjdGFsTm9pc2UnIGJhc2VGcmVxdWVuY3k9JzAuOCcgbnVtT2N0YXZlcz0nMycgc3RpdGNoVGlsZXM9J3N0aXRjaCcvPjwvZmlsdGVyPjxyZWN0IHdpZHRoPScxMDAlJyBoZWlnaHQ9JzEwMCUnIGZpbHRlcj0ndXJsKCNub2lzZUZpbHRlciknLz48L3N2Zz4=");
}

/* ==========================================================================
   2. Design tokens
   --------------------------------------------------------------------------
   Colours are channel triplets rather than colours, because every one of them
   is used at several alphas. Browsers that understand relative colours derive
   the two inner stops of the aurora from the main hue on their own — see the
   @supports block right after, which is what makes --sh-glow-rgb enough to
   recolour the whole effect.

   Speeds are two multipliers, not one: the original slows the drift down with
   speed^0.7 but widens the aurora sway with speed^0.4. Keeping them apart is
   what lets the CSS stay faithful without a script. The script computes both
   from a single data-sh-speed when it runs.
   ========================================================================== */

.star-horizon {
  /* Module — one variable rescales the whole drawing. */
  --sh-scale: 16px;

  /* Height of the block. `auto` lets the content decide. */
  --sh-height: 100svh;

  /* Colours, as R G B triplets. */
  --sh-glow-rgb: 80 250 250;
  --sh-space-rgb: 0 0 0;
  --sh-haze-rgb: 10 40 40;
  --sh-haze-deep-rgb: 3 15 20;

  /* Aurora stops. The two inner ones are the original's recipe applied to the
     hue above — red halved, green and blue at 72%, then 12.5% and 20% — with
     the result written out. data-sh-tint="auto" recomputes them from
     --sh-glow-rgb alone; see the @supports block below for why that is opt-in
     and not the default. */
  --sh-glow-near: rgb(var(--sh-glow-rgb) / 0.4);
  --sh-glow-mid: rgb(40 180 180 / 0.3);
  --sh-glow-far: rgb(10 50 50 / 0.15);

  /* Light of the horizon and of the aurora. 1 is the original. */
  --sh-intensity: 1;

  /* Drift multiplier (divides the three drift durations) and sway multiplier
     (widens the aurora travel). The script derives both from data-sh-speed. */
  --sh-star-speed: 1;
  --sh-glow-speed: 1;

  /* Derived lengths — absolute, never em, so they mean the same thing
     wherever they are read. */
  --sh-tile-far: calc(25 * var(--sh-scale));
  --sh-tile-mid: calc(31.25 * var(--sh-scale));
  --sh-tile-near: calc(37.5 * var(--sh-scale));
  --sh-glow-blur: calc(1.25 * var(--sh-scale));
  --sh-glow-travel: calc(1.875 * var(--sh-scale) * var(--sh-glow-speed));
  --sh-horizon-edge: calc(0.0625 * var(--sh-scale));

  /* Grain tile. `auto` follows the original: the SVG has a ratio but no size,
     so the browser fits one square tile to the shorter side of the block.
     A length (200px is the native grid of the texture) freezes the grain. */
  --sh-grain-size: auto;
  --sh-grain-opacity: 0.07;

  /* Star field brightness, per layer. */
  --sh-star-opacity-far: 0.6;
  --sh-star-opacity-mid: 0.7;
  --sh-star-opacity-near: 0.85;

  /* Durations. Drift first — one tile per cycle, timed so the stars cross the
     screen at the speed of the original (see README, "What differs"). */
  --sh-drift-far: 33.31s;
  --sh-drift-mid: 34.71s;
  --sh-drift-near: 37.48s;

  /* Same durations, raw mode: the original percentages need its own clock. */
  --sh-drift-far-raw: 60s;
  --sh-drift-mid-raw: 50s;
  --sh-drift-near-raw: 45s;

  /* Twinkle and zoom keep a fixed clock in the original: speed does not
     touch them. */
  --sh-twinkle-far: 5.3s;
  --sh-twinkle-mid: 3.7s;
  --sh-twinkle-near: 2.2s;
  --sh-zoom-far: 7s;
  --sh-zoom-mid: 8.5s;
  --sh-zoom-near: 6s;
  --sh-sway: 20s;

  /* Easing. The original animates with framer-motion's "easeInOut", which is
     cubic-bezier(0.42, 0, 0.58, 1) — the very definition of CSS ease-in-out. */
  --sh-ease: ease-in-out;
}

/* data-sh-tint="auto" : one hue is enough, the aurora derives its own two
   inner stops. Opt-in and not the default, for a measured reason — a stop
   written with relative colours computes to color(srgb …) instead of rgba(),
   and Chromium then interpolates the gradient slightly differently. Measured
   against the original: 0.05/255 of average difference with the values
   written out, 0.12/255 with the derivation. Invisible either way, but the
   default is the one that matches. */
@supports (color: rgb(from white r g b)) {
  .star-horizon[data-sh-tint="auto"] {
    --sh-glow-mid: rgb(from rgb(var(--sh-glow-rgb)) calc(r * 0.5) calc(g * 0.72) calc(b * 0.72) / 0.3);
    --sh-glow-far: rgb(from rgb(var(--sh-glow-rgb)) calc(r * 0.125) calc(g * 0.2) calc(b * 0.2) / 0.15);
  }
}

/* ==========================================================================
   3. Block, sky, content
   --------------------------------------------------------------------------
   The block declares its own box model for itself and for the decoration, and
   leaves the content slot alone: what a page puts inside keeps its own rules.

   The sky is isolated. Three of its layers blend in `screen` mode and one in
   `overlay`; without isolation they would reach for whatever the host page
   painted underneath. Isolated, the result is the same drawing everywhere —
   screen over the sky's own black is the identity, which is exactly what the
   original gets from the black behind it.
   ========================================================================== */

.star-horizon,
.star-horizon__sky,
.star-horizon__sky > * {
  box-sizing: border-box;
}

.star-horizon {
  position: relative;
  display: flow-root;
  min-block-size: var(--sh-height);
  background-color: rgb(var(--sh-space-rgb));
}

.star-horizon__sky {
  position: absolute;
  inset: 0;
  z-index: 0;
  /* clip, not hidden : `hidden` would turn the sky into a scroll container,
     and the layers reach 25% past its edges. It would then report an overflow
     it cannot show, and one Tab key inside the block could scroll it. */
  overflow: hidden;
  overflow: clip;
  isolation: isolate;
  pointer-events: none;
  /* The sky's background is not decorative: it is the backdrop of the blending
     group. Three layers blend in `screen`, one in `overlay`, and a blend only
     means something over an opaque base — over transparency the compositing
     formula renders a paler colour, by two values out of 255 at the centre of
     the image. In the original it is the black of the page that plays that
     part; here it is declared, so the result no longer depends on whatever is
     behind the block. */
  background-color: rgb(var(--sh-space-rgb));
}

.star-horizon__sky > * {
  display: block;
  position: absolute;
}

.star-horizon__content {
  position: relative;
  z-index: 1;
}

/* ==========================================================================
   4. Layer 0 — base haze
   --------------------------------------------------------------------------
   A cold halo in the middle, black at the edges. It is what the aurora and
   the stars are screened onto.
   ========================================================================== */

.star-horizon__base {
  inset: 0;
  z-index: 0;
  background-image: radial-gradient(
    circle at 50% 50%,
    rgb(var(--sh-haze-rgb) / 0.2) 0%,
    rgb(var(--sh-haze-deep-rgb) / 0.6) 40%,
    rgb(var(--sh-space-rgb) / 0.95) 80%,
    rgb(var(--sh-space-rgb)) 100%
  );
}

/* ==========================================================================
   5. Layer 1 — aurora
   --------------------------------------------------------------------------
   An ellipse of light centred on the horizon line (65% of the height), blurred
   and screened. It spreads 25% beyond every edge so the blur never shows an
   edge of its own, and it sways up and down forever.
   ========================================================================== */

.star-horizon__glow {
  inset: -25%;
  z-index: 1;
  background-image: radial-gradient(
    ellipse at 50% 65%,
    var(--sh-glow-near) 0%,
    var(--sh-glow-mid) 30%,
    var(--sh-glow-far) 60%,
    transparent 85%
  );
  filter: blur(var(--sh-glow-blur));
  mix-blend-mode: screen;
  opacity: calc(0.9 * var(--sh-intensity));
  animation: sh-sway var(--sh-sway) var(--sh-ease) infinite alternate;
}

/* ==========================================================================
   6. Layers 2 to 4 — star fields
   --------------------------------------------------------------------------
   The same texture three times, at three tile sizes. The small tile repeats
   most often, so it reads as the densest and most distant field; it also
   drifts the slowest. Each field breathes on its own clock — that is what
   makes the sky twinkle without a single star being animated on its own.

   Default loop: the tile itself travels, exactly one tile per cycle, so the
   image at the end of a cycle is the image at the start and nothing jumps.
   See section 10 for the raw mode, which reproduces the original's move.
   ========================================================================== */

.star-horizon__stars {
  inset: 0;
  background-image: var(--sh-star-texture);
  background-repeat: repeat;
  background-position: 0 0;
  mix-blend-mode: screen;
}

.star-horizon__stars--far {
  z-index: 2;
  background-size: var(--sh-tile-far) var(--sh-tile-far);
  opacity: var(--sh-star-opacity-far);
  animation:
    sh-drift-far calc(var(--sh-drift-far) / var(--sh-star-speed)) linear infinite,
    sh-twinkle-far var(--sh-twinkle-far) var(--sh-ease) infinite alternate,
    sh-zoom-far var(--sh-zoom-far) var(--sh-ease) infinite alternate;
}

.star-horizon__stars--mid {
  z-index: 3;
  background-size: var(--sh-tile-mid) var(--sh-tile-mid);
  opacity: var(--sh-star-opacity-mid);
  animation:
    sh-drift-mid calc(var(--sh-drift-mid) / var(--sh-star-speed)) linear infinite,
    sh-twinkle-mid var(--sh-twinkle-mid) var(--sh-ease) infinite alternate,
    sh-zoom-mid var(--sh-zoom-mid) var(--sh-ease) infinite alternate;
}

.star-horizon__stars--near {
  z-index: 4;
  background-size: var(--sh-tile-near) var(--sh-tile-near);
  opacity: var(--sh-star-opacity-near);
  animation:
    sh-drift-near calc(var(--sh-drift-near) / var(--sh-star-speed)) linear infinite,
    sh-twinkle-near var(--sh-twinkle-near) var(--sh-ease) infinite alternate,
    sh-zoom-near var(--sh-zoom-near) var(--sh-ease) infinite alternate;
}

/* ==========================================================================
   7. Layer 5 — planet horizon
   --------------------------------------------------------------------------
   A black ellipse, 160% wide, pushed 85% of the height below the bottom edge.
   What stays visible is the top of the curve, at 65% of the height, and its
   lit edge: a one-pixel border and three shadows thrown upwards, tight then
   wide, which is what reads as an atmosphere.
   ========================================================================== */

.star-horizon__horizon {
  inset-inline-start: -30%;
  inset-block-end: -85%;
  inline-size: 160%;
  block-size: 120%;
  z-index: 5;
  border-radius: 50%;
  border-block-start: var(--sh-horizon-edge) solid
    rgb(var(--sh-glow-rgb) / calc(0.3 * var(--sh-intensity)));
  background-color: rgb(var(--sh-space-rgb));
  box-shadow:
    0 calc(-0.125 * var(--sh-scale)) calc(0.9375 * var(--sh-scale))
      rgb(var(--sh-glow-rgb) / calc(0.5 * var(--sh-intensity))),
    0 calc(-0.5 * var(--sh-scale)) calc(2.5 * var(--sh-scale))
      rgb(var(--sh-glow-rgb) / calc(0.4 * var(--sh-intensity))),
    0 calc(-1.25 * var(--sh-scale)) calc(6.25 * var(--sh-scale))
      rgb(var(--sh-glow-rgb) / calc(0.3 * var(--sh-intensity)));
}

/* ==========================================================================
   8. Layers 6 and 7 — vignette and grain
   --------------------------------------------------------------------------
   The vignette darkens the corners so the eye stays on the horizon; the grain
   breaks the banding of two large gradients laid on top of each other.
   ========================================================================== */

.star-horizon__vignette {
  inset: 0;
  z-index: 6;
  background-image: radial-gradient(
    circle at 50% 50%,
    rgb(var(--sh-space-rgb) / 0) 0%,
    rgb(var(--sh-space-rgb) / 0.55) 55%,
    rgb(var(--sh-space-rgb) / 0.92) 100%
  );
}

.star-horizon__grain {
  inset: 0;
  z-index: 7;
  background-image: var(--sh-grain-texture);
  background-repeat: repeat;
  background-size: var(--sh-grain-size);
  opacity: var(--sh-grain-opacity);
  mix-blend-mode: overlay;
}

/* ==========================================================================
   9. Motion
   --------------------------------------------------------------------------
   Ten animations, all endless. `alternate` is what the original calls a
   mirrored repeat: the second half plays the first backwards, easing
   included. The drift alone runs one way and starts over.

   Each rule animates one property — translate, scale, opacity or
   background-position — and never `transform`, so three of them can share an
   element without overwriting each other.

   Keyframes that only describe their end let the start come from the rule
   above: that is how the zoom can start at whatever scale the element has.
   ========================================================================== */

@keyframes sh-sway {
  from {
    translate: 0 var(--sh-glow-travel);
    scale: 1;
    opacity: calc(0.7 * var(--sh-intensity));
  }
  to {
    translate: 0 calc(-1 * var(--sh-glow-travel));
    scale: calc(1 + 0.1 * var(--sh-glow-speed));
    opacity: calc(1 * var(--sh-intensity));
  }
}

@keyframes sh-drift-far {
  to { background-position: var(--sh-tile-far) var(--sh-tile-far); }
}
@keyframes sh-drift-mid {
  to { background-position: calc(-1 * var(--sh-tile-mid)) var(--sh-tile-mid); }
}
@keyframes sh-drift-near {
  to { background-position: calc(-1 * var(--sh-tile-near)) calc(-1 * var(--sh-tile-near)); }
}

@keyframes sh-twinkle-far {
  from { opacity: 0.2; }
  to { opacity: 0.6; }
}
@keyframes sh-twinkle-mid {
  from { opacity: 0.3; }
  to { opacity: 0.7; }
}
@keyframes sh-twinkle-near {
  from { opacity: 0.4; }
  to { opacity: 0.9; }
}

@keyframes sh-zoom-far {
  to { scale: 1.1; }
}
@keyframes sh-zoom-mid {
  to { scale: 1.15; }
}
@keyframes sh-zoom-near {
  to { scale: 1.2; }
}

/* Raw mode moves the layer instead of the texture — see section 10. */
@keyframes sh-raw-far {
  to { translate: 50% 50%; }
}
@keyframes sh-raw-mid {
  to { translate: -50% 50%; }
}
@keyframes sh-raw-near {
  to { translate: -50% -50%; }
}

/* ==========================================================================
   10. Loop modes: seamless and raw
   --------------------------------------------------------------------------
   The original translates each star layer by half of its own width and half
   of its own height, then snaps back. Two consequences, both visible:

     — the layer is only 10% wider than the block, so once it has travelled
       half its width it no longer covers half of the screen: that field
       simply vanishes from the left (or the right) of the page;
     — the return to zero is a jump of several hundred pixels.

   The delivered default moves the texture, not the layer, by exactly one
   tile: same direction, same speed, but the drawing at the end of a cycle is
   the drawing at the start, and every field covers the block at all times.

   data-sh-loop="raw" restores the original move, defect included. That is
   what the bench pins to compare the two frame by frame.
   ========================================================================== */

.star-horizon[data-sh-loop="raw"] .star-horizon__stars {
  inset: -10%;
  background-position: 0 0;
}

.star-horizon[data-sh-loop="raw"] .star-horizon__stars--far {
  animation-name: sh-raw-far, sh-twinkle-far, sh-zoom-far;
  animation-duration: calc(var(--sh-drift-far-raw) / var(--sh-star-speed)),
    var(--sh-twinkle-far), var(--sh-zoom-far);
}
.star-horizon[data-sh-loop="raw"] .star-horizon__stars--mid {
  animation-name: sh-raw-mid, sh-twinkle-mid, sh-zoom-mid;
  animation-duration: calc(var(--sh-drift-mid-raw) / var(--sh-star-speed)),
    var(--sh-twinkle-mid), var(--sh-zoom-mid);
}
.star-horizon[data-sh-loop="raw"] .star-horizon__stars--near {
  animation-name: sh-raw-near, sh-twinkle-near, sh-zoom-near;
  animation-duration: calc(var(--sh-drift-near-raw) / var(--sh-star-speed)),
    var(--sh-twinkle-near), var(--sh-zoom-near);
}

/* Off-screen, in a hidden tab, or asked to stand still. The paused state is
   set by the script; the "off" state is a plain attribute anyone can write. */
.star-horizon[data-sh-paused] .star-horizon__sky > * {
  animation-play-state: paused;
}

/* data-sh-motion="off" is written by hand; data-sh-still is written by the
   script when data-sh-speed reaches 0 — the original freezes everything, the
   aurora included, as soon as its speed hits zero. */
.star-horizon[data-sh-motion="off"] .star-horizon__sky > *,
.star-horizon[data-sh-still] .star-horizon__sky > * {
  animation: none;
}

/* ==========================================================================
   11. Variants
   --------------------------------------------------------------------------
   One hue away from the original. Both keep the same drawing: only the light
   changes, which is the point of a single colour variable.
   ========================================================================== */

.star-horizon--ember {
  --sh-glow-rgb: 255 138 76;
  --sh-haze-rgb: 40 22 14;
  --sh-haze-deep-rgb: 18 8 6;
  --sh-glow-mid: rgb(127 99 54 / 0.3);
  --sh-glow-far: rgb(31 27 15 / 0.15);
}

.star-horizon--violet {
  --sh-glow-rgb: 168 130 255;
  --sh-haze-rgb: 24 18 48;
  --sh-haze-deep-rgb: 10 6 24;
  --sh-glow-mid: rgb(84 93 183 / 0.3);
  --sh-glow-far: rgb(21 26 51 / 0.15);
}

/* ==========================================================================
   12. Fallbacks
   --------------------------------------------------------------------------
   Reduced motion stops every clock, delays included — a delay is movement
   spread over time. What stays is an image: the gradients, the star fields at
   their resting brightness, the horizon. That is exactly what the original
   renders when its motion is switched off.

   Forced colours drop the decoration entirely. Eight blended layers cannot be
   remapped to a two-colour palette, and the point of that mode is that the
   content reads — so the content is what is left.
   ========================================================================== */

@media (prefers-reduced-motion: reduce) {
  .star-horizon .star-horizon__sky > * {
    animation: none;
  }
}

@media (forced-colors: active) {
  .star-horizon {
    background-color: Canvas;
  }
  .star-horizon__sky {
    display: none;
  }
}

@media print {
  .star-horizon__sky {
    display: none;
  }
}
