/* ============================================================================
   JT BACKGROUND MODULE — v1.1.0
   FROZEN. Link this file; do not copy it into a page and do not edit it to
   suit one page. Per-page adjustment happens through the custom properties
   listed in MODULE.md, nothing else.

   Everything the background needs is in here: its own custom properties, its
   own @media blocks, its own stacking rules. It deliberately does NOT rely on
   the host page defining anything — that's what makes it drop-in across
   en/pl/ru and every page depth.

   NOTE — this reverses a rule from the prototype, on purpose. While this was
   a single page, every @media rule in the project lived in one file
   (base.css) so there was one place to look. That rule doesn't survive
   modularisation: a module whose breakpoints live in the host page's
   stylesheet isn't a module. The rule still holds INSIDE each file — all of
   the background's media queries are at the bottom of this one — it just no
   longer holds across the module boundary.

   Contents
     1. Module custom properties (+ per-breakpoint values)
     2. Parallax stage and layers
     3. Ambient glows
     4. Media queries (breakpoints, retina, reduced motion)
   ========================================================================== */


/* ==========================================================================
   1. MODULE CUSTOM PROPERTIES
   Scoped to :root so the host page can override any of them, at page or
   section level, without touching this file. Defaults are the shipped look.
   ========================================================================== */
:root {
  /* Painted behind everything, and the flash colour before images decode.
     Match this to the page background if you change either. */
  --jt-bg-fallback: #05070f;

  /* Edge length of the seamless deep-space tile. */
  --jt-deepspace-tile: 512px;

  /* Planet visibility. Phones get `none` — see the media queries below.
     This is visibility ONLY; whether the 892 KB file is DOWNLOADED is
     decided in background.config.js, because CSS can't gate that reliably
     (Chrome/Edge/Opera fetch background-images on display:none elements;
     Firefox and Safari don't). */
  --jt-planet-display: none;

  /* THE DESKTOP SKY AND THE PHONE SKY ARE TWO DIFFERENT PICTURES, and these
     two properties are the switch between them. Written mobile-first — the
     phone's values are the defaults here, and the ≥768 block flips both —
     because that is the direction that degrades safely: a browser that never
     applies the media query shows the light scene, not the heavy one.

       --jt-deep-sky-display    01-stars and all three nebulas. `none` on a
                                phone: they are replaced wholesale, not scaled.
       --jt-mobile-sky-display  the single nebula-mobile export that stands in
                                for them. `none` from 768 up.

     Visibility only, again. The 2.57 MB those four layers weigh is kept off a
     phone by `load` in background.config.js — a media query cannot stop
     Chrome fetching a background-image for a hidden element. */
  --jt-deep-sky-display:   none;
  --jt-mobile-sky-display: block;

  /* Size of every layer that sits on transparency (nebulas + planet),
     relative to viewport cover. 1 = exactly covers the viewport, <1 pulls
     back and reveals more starfield, >1 zooms in. Does NOT affect the
     starfield itself — that's the backdrop and must always cover. */
  --jt-sprite-scale: 0.6;

  /* Ambient glow strength — THE MAIN PER-PAGE DIAL.
     0 = off visually, 1 = the raw Figma alphas (0.20-0.30 peak).
     Set it per page on the container; see MODULE.md. */
  --jt-glow-opacity: 0.45;

  /* Glow visibility. Phones get `none`. */
  --jt-glow-display: none;
}


/* ==========================================================================
   2. PARALLAX STAGE AND LAYERS
   ========================================================================== */

/* Fixed, non-interactive, contained box. Layers live inside it and are the
   only things that move. It also carries the seamless deep-space tile as its
   own background — painted on an already-fixed element rather than with
   background-attachment: fixed, which has a known iOS Safari scroll-repaint
   cost. z-index: -1 puts it behind normal-flow content with no cooperation
   needed from the page. */
#parallax-stage {
  position: fixed;
  inset: 0;
  /* -2, with the glows at -1. Both are NEGATIVE, which is what makes them
     paint behind normal-flow content no matter where they sit in the DOM —
     see the stacking note above #ambient-glows. */
  z-index: -2;
  overflow: hidden;
  contain: strict;
  pointer-events: none;

  background-color: var(--jt-bg-fallback);
  background-image: url("images/00-deepspace.png");
  background-repeat: repeat;
  background-position: left top;
  background-size: var(--jt-deepspace-tile) var(--jt-deepspace-tile);
}

/* A layer's box is offset by a negative inset ("overscan") equal to its own
   travel distance, written per layer by background.js as --jt-ov-*.

   Why: layers move as rigid boxes via translate3d. background-size only fills
   WITHIN the box — it says nothing about the box still covering the viewport
   once translated. A viewport-sized box that slides left exposes a strip at
   its trailing edge. Padding the box by its own travel range closes that,
   per layer, per direction; a layer that can't move in some direction gets
   0px there. */
#parallax-stage .layer {
  position: absolute;
  top:    calc(-1 * var(--jt-ov-top, 0px));
  right:  calc(-1 * var(--jt-ov-right, 0px));
  bottom: calc(-1 * var(--jt-ov-bottom, 0px));
  left:   calc(-1 * var(--jt-ov-left, 0px));

  background-repeat: no-repeat;

  opacity: 0;
  transition: opacity 320ms ease;

}

#parallax-stage .layer.is-loaded {
  opacity: var(--jt-layer-opacity, 1);
}

/* --- sizing rule A: the backdrop (01-stars only) ---------------------------
   The only layer with nothing but the deep-space tile behind it, so it must
   always cover: background-size: cover against its own overscanned box. That
   IS its responsive rule — cover re-derives from the live box at every
   viewport size, no breakpoints involved. Its overscan is ~4%, imperceptible.
   Deliberately NOT scaled by --jt-sprite-scale: a backdrop that can shrink
   below cover is a backdrop that can leak the tile through. */
#parallax-stage .layer[data-full-bleed="true"] {
  background-size: cover;
}

/* --- sizing rule B: art on transparency (nebulas + planet) -----------------
   Sized against the VIEWPORT, never against the box. --jt-sprite-cover is
   published by background.js as max(100vw, ratio*100vh) from the configured
   asset ratio, and evaluated by the browser — so it tracks viewport changes
   with no JS on resize. These get zero overscan: box == viewport, and a gap
   at any edge reveals the starfield behind, which is the intended effect.

   Sizing these against their box instead is what once rendered the planet
   ~66% oversized: its travel is large, so its overscan was large, so cover
   scaled the art to a hugely inflated box. */
#parallax-stage .layer[data-full-bleed="false"] {
  background-size: calc(var(--jt-sprite-cover, 100vw) * var(--jt-sprite-scale, 1)) auto;
}

/* The planet is the one layer whose presence is breakpoint-dependent. */
#parallax-stage .layer[data-layer="05-planet"] {
  display: var(--jt-planet-display);
}

/* The four desktop sky layers, hidden together below 768. */
#parallax-stage .layer[data-layer="01-stars"],
#parallax-stage .layer[data-layer="02-nebula"],
#parallax-stage .layer[data-layer="03-nebula"],
#parallax-stage .layer[data-layer="04-nebula"] {
  display: var(--jt-deep-sky-display);
}

/* The phone's stand-in. Pinned to the TOP-LEFT corner of the stage — the
   stage is the whole scrollable page, so this is the top-left of the document,
   not of the viewport, and it does not follow the reader down.

   `background-size: 100% auto`, overriding the shared sprite rule below:
   that rule scales against `--jt-sprite-cover * --jt-sprite-scale`, which is
   the right maths for art floating over a starfield and the wrong maths here.
   The export is 767 wide — the widest a phone can be before the desktop scene
   takes over — so spanning the viewport width reproduces the drawing at, or
   just under, the size it was drawn for, at every phone width. */
#parallax-stage .layer[data-layer="nebula-mobile"] {
  display: var(--jt-mobile-sky-display);
  background-position: left top;
  background-size: 100% auto;
}


/* ==========================================================================
   3. AMBIENT GLOWS
   Unlike the parallax stage these are NOT fixed. In the Figma page frames the
   glows are children of the page at fixed document positions, so they scroll
   with the content.

   STACKING — why both layers use negative z-index
   -----------------------------------------------
   The paint order inside a stacking context is: the element's own background,
   then NEGATIVE z-index descendants, then normal-flow content, then
   positioned/z-index:0 descendants. So anything with a negative z-index sits
   behind all ordinary page content automatically.

   Both layers are therefore negative — stage at -2, glows at -1 — which buys
   two things:

     1. DOM ORDER DOESN'T MATTER. An earlier version put the glows at
        z-index: 0, which puts them in the same band as positioned content, so
        paint order fell back to document order and the two <div>s HAD to come
        before the page's content or they'd cover the text. That was a real
        invariant a page could silently break. It's gone.
     2. THE PAGE NEEDS NO COOPERATION. It no longer has to give <main> a
        `position: relative` to climb above the glows. Ordinary content is
        already above them.

   The one thing still required of the page is `isolation: isolate` on <body>,
   which keeps these negative layers inside the page's own stacking context
   instead of disappearing behind the document background.
   ========================================================================== */

/* Zero-height anchor; the glows inside are absolutely positioned, so this
   never affects layout or document height. */
#ambient-glows {
  display: var(--jt-glow-display, block);
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 0;
  z-index: -1;
  pointer-events: none;

  /* Applied as ONE opacity on the container rather than folded into each
     glow's gradient: it stays a pure CSS knob (no rebuild, live-draggable in
     DevTools), it composites the group once instead of per glow, and it can
     never reshuffle the seeded layout. */
  opacity: var(--jt-glow-opacity, 1);
}

/* Size, position and the radial gradient are written inline per glow by
   background.js; everything shared is here. translate(-50%,-50%) lets the JS
   write the glow's CENTRE as left/top, which is what its placement maths
   produces and what Figma's ellipse centres correspond to.

   No mix-blend-mode: Figma blends these with SCREEN, but over a near-black
   backdrop screen(a,b) collapses to roughly `a`, so normal compositing looks
   the same — and blend modes force every glow onto its own non-squashable
   compositing layer. Revisit only if a glow must sit over something bright. */
#ambient-glows .glow {
  position: absolute;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  will-change: auto;   /* static — never animated, nothing to promote */
}


/* ==========================================================================
   4. MEDIA QUERIES
   Every breakpoint the module has, in one place. These numbers must stay in
   sync with `mobileBreakpoint` in background.config.js — they're independent
   constants (CSS can't read JS, and @custom-media still isn't shipped outside
   Firefox Nightly as of 2026), kept aligned by convention and documented in
   both files.
   ========================================================================== */

/* ---- tablet and up: >=768px ----------------------------------------------
   The phone/not-phone line. Below it: no parallax at all, no planet, no
   glows. Above it: everything on. */
@media (min-width: 768px) {
  :root {
    --jt-planet-display: block;
    --jt-glow-display: block;
    --jt-sprite-scale: 0.6;

    /* Swap the phone's single nebula for the four-layer desktop sky. */
    --jt-deep-sky-display:   block;
    --jt-mobile-sky-display: none;
  }

  /* The engine adds this class only while a frame loop is active. Keeping
     will-change permanent pins every large layer in compositor memory. */
  #parallax-stage.is-animating .layer {
    will-change: transform;
  }

  #parallax-stage .layer {
    transform: translate3d(0, 0, 0);
    backface-visibility: hidden;
  }
}

/* ---- desktop: >=1024px --------------------------------------------------- */
@media (min-width: 1024px) {
  :root {
    --jt-sprite-scale: 0.8;
  }
}

/* ---- retina: swap the deep-space tile for the @2x export ------------------
   Threshold matches `retinaThreshold` in background.config.js. */
@media (min-resolution: 1.5dppx) {
  #parallax-stage {
    background-image: url("images/00-deepspace@2x.png");
  }
}

/* ---- reduced motion -------------------------------------------------------
   background.js stops the render loop entirely on this signal; this is the
   CSS half — drop the compositor hint and the load fade so nothing is
   promoted or animated. Glows are static and stay visible: they're ambience,
   not motion. */
@media (prefers-reduced-motion: reduce) {
  #parallax-stage .layer {
    will-change: auto;
    transition: none;
  }
}
