/* The site's background: black, four diffuse bands, one bright arc over
   them, and a hex field.

   Linked by BOTH base templates - templates/base.html and
   templates/admin/base_site.html - because the storefront and the portal
   share it. One definition, two surfaces; a second copy would drift.

   THREE LAYERS, and they do different jobs. The bands are the weather -
   diffuse, slow, and the reason there is any light behind the content at all,
   which is what a card's window has to show. The arc is a light source IN
   that weather: one defined stroke, bright enough to read as a source rather
   than a glow. The hex field is texture under both.

   The arc replaced the bands for one build on 2026-08-03. That was wrong
   twice: it took the light out from behind the content, so every card window
   went dark again, and a stroke on a flat black field is not the same
   picture as a stroke in weather. Black is still doing real work - the arc
   only reads as light because most of the page is not - but black is the
   FLOOR, not the whole field.

   Two things are load-bearing and are not style choices:

   1. NO filter: blur(), anywhere. Blurring a viewport-wide element costs a
      full-surface repaint every frame. The arc's bloom is five stacked
      copies of one path, widest and faintest first - free, and
      indistinguishable at this scale.

   2. Motion is TRANSFORM and OPACITY only, for the same reason.

   STYLE-GUIDE S10 permits background motion for this layer only, and S10 is
   currently marked UNDER DEVELOPMENT - the look is still being found. No
   parallax, no scroll animation, no skeleton loaders; the rest of S10 is
   unchanged. */

.aurora {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  background: var(--bg-base);
  pointer-events: none;
  /* The arc now overflows its own box by design, and at full stretch it
     reaches a long way past this one too. `contain: paint` promises the
     browser that nothing inside paints outside these bounds, so the work is
     bounded by the viewport instead of by how far the geometry happens to
     swing. Same visible result as the overflow: hidden above - this tells the
     compositor about it up front rather than after the fact. */
  contain: paint;
}

/* --- The arc ------------------------------------------------------------- */

/* One bright two-tone sweep on a black field, sitting behind the mark.

   This replaced three overlapping diffuse ellipses on 2026-08-03. They lit
   the whole upper half evenly and never resolved into anything - the
   reference is a defined stroke of light with dark around it, and a wash is
   the opposite of that. Black is the point: the arc only reads as light
   because most of the page is not.

   Sized and placed to sit BEHIND the mark rather than anywhere in
   particular. The height is a fraction of the viewport so the arc keeps its
   proportions on a laptop and a phone, and the SVG stretches to the width
   rather than cropping, so its ends always reach past both edges. */

/* THE BOX IS THE WHOLE VIEWPORT, and that is a requirement of the motion
   rather than a layout preference.

   It was a strip - top: 8%, height: 40% - back when the arc was a fixed bow
   and only needed to be where the light was. The anchors now travel the box's
   left and right EDGES, so the box has to be the thing they are meant to
   travel: the page. In a 40% strip the same anchor_travel value is a wobble.

   Every fraction in ArcSettings is measured against this box, which is why
   the background and mark arcs need separate stored values even though they
   take the same parameters. */
.aurora-arc {
  position: absolute;
  /* Full height, and pulled OUT sideways. The spine already overshoots both
     side edges inside the viewBox (theming/arc.py, OVERSHOOT) and the viewBox
     clips it; pulling the element out means that clip happens 6% off-screen
     rather than at the glass, so the ends leave the frame instead of ending
     in mid-air. */
  inset: 0 -6%;
}

/* arc-breathe is gone. It drifted and swelled the whole element on a 26s
   loop, which was the only motion the arc had when its geometry was static.
   Everything it did is now a parameter - and it FOUGHT the new motion, since
   scaling the box vertically stretches anchors that are already moving. */

/* On a page that anchors its own arc to the mark, the background one would
   be a second arc at a different height. :has() rather than a body class:
   the anchoring is a property of the MARKUP being there, so the stylesheet
   can see it without a template having to remember to announce it. */
body:has(.mark-arc) .aurora-arc {
  display: none;
}

.aurora-arc-svg {
  width: 100%;
  height: 100%;
  display: block;
  /* An outermost <svg> clips to its viewBox by default, and the spline goes
     WELL outside its box: the anchors travel a fifth of the box height and a
     76-degree exit angle with a half-width handle throws the curve far past
     the top and bottom edges. The old bowed spine stayed inside, so this
     never mattered; the first animated build sliced the threads off against a
     hard straight edge.

     Letting it overflow is right rather than expedient. The box positions the
     arc - it is not meant to be a frame the arc lives inside - and something
     outside still clips: .aurora for the background arc, .page for the mark
     arc. Growing the box instead would move the arc, since every fraction is
     measured against it. */
  overflow: visible;
}

@media (prefers-reduced-motion: reduce) {
  .aurora-band {
    animation: none;
  }
  /* The arc needs no rule here. Its motion is JS, and static/js/arc.js checks
     the same media query before it starts - leaving the server's flat pose,
     which is already a valid arc, exactly as rendered. */
}

/* A vignette, and nothing else. The grid that used to be here read as a
   second texture fighting the hex field. */
.aurora::after {
  content: "";
  position: absolute;
  inset: 0;
  background: radial-gradient(
    140% 115% at 50% 26%,
    transparent 36%,
    rgba(0, 0, 0, 0.6) 100%
  );
}

/* --- The bands ----------------------------------------------------------- */

/* The diffuse layer, UNDERNEATH the arc. Four soft ellipses on their own
   periods: three across the top and one low, so there is light where the
   content is and a card's window has something behind it to show.

   These were removed on 2026-08-03 and put straight back. The arc replaced
   them for one build, which was wrong twice over - it lost the light behind
   the content, so every card window went dark again, and the two do
   different jobs. The bands are the weather; the arc is a light source in
   it. A page with only the arc is a stroke on a flat black field. */

.aurora-band {
  position: absolute;
  left: -30%;
  width: 160%;
  height: 70%;
  border-radius: 50%;
  transform-origin: 50% 50%;
  will-change: transform;
}

/* Peak alphas are LOW on purpose. The first build ran these cores at full
   opacity and the result was a magenta slab across the upper half of every
   page - it read as a broken gradient rather than as light, and it washed
   every glass panel out. The reference is a near-black page with one bright
   arc across it; the light is meant to be a suggestion of a source
   off-frame, not the source itself. */
.aurora-band-1 {
  top: -40%;
  height: 62%;
  background: radial-gradient(
    closest-side ellipse,
    rgba(255, 42, 42, 0.34) 0%,
    rgba(255, 42, 42, 0.13) 40%,
    transparent 72%
  );
  animation: aurora-drift-1 19s ease-in-out infinite alternate;
}

.aurora-band-2 {
  top: -30%;
  height: 58%;
  background: radial-gradient(
    closest-side ellipse,
    rgba(255, 0, 127, 0.26) 0%,
    rgba(255, 0, 127, 0.10) 42%,
    transparent 74%
  );
  animation: aurora-drift-2 27s ease-in-out infinite alternate;
}

.aurora-band-3 {
  top: -14%;
  height: 54%;
  background: radial-gradient(
    closest-side ellipse,
    rgba(155, 77, 255, 0.20) 0%,
    rgba(155, 77, 255, 0.08) 44%,
    transparent 76%
  );
  animation: aurora-drift-3 37s ease-in-out infinite alternate;
}

/* A fourth band, low and wide, and it is not decoration.
   The first three all sit in the top third, and with the vignette under them
   the middle and bottom of a page were effectively black - so a card's
   window had nothing behind it to show and every border came out grey. Light
   has to exist where the CONTENT is, or the window is a hole onto nothing. */
.aurora-band-4 {
  top: 34%;
  height: 76%;
  background: radial-gradient(
    closest-side ellipse,
    rgba(255, 42, 42, 0.2) 0%,
    rgba(180, 40, 140, 0.12) 42%,
    transparent 78%
  );
  animation: aurora-drift-4 31s ease-in-out infinite alternate;
}

@keyframes aurora-drift-1 {
  from {
    transform: translate3d(-18%, -4%, 0) rotate(-7deg) scale(1);
  }
  to {
    transform: translate3d(20%, 9%, 0) rotate(9deg) scale(1.3);
  }
}

@keyframes aurora-drift-2 {
  from {
    transform: translate3d(24%, 6%, 0) rotate(8deg) scale(1.25);
  }
  to {
    transform: translate3d(-20%, -7%, 0) rotate(-11deg) scale(0.95);
  }
}

@keyframes aurora-drift-4 {
  from {
    transform: translate3d(-22%, 4%, 0) rotate(4deg) scale(1.05);
  }
  to {
    transform: translate3d(18%, -5%, 0) rotate(-6deg) scale(1.32);
  }
}

@keyframes aurora-drift-3 {
  from {
    transform: translate3d(-14%, 8%, 0) rotate(5deg) scale(0.98);
  }
  to {
    transform: translate3d(26%, -6%, 0) rotate(-8deg) scale(1.28);
  }
}

/* The hex field: multi-tone greys, tiling, and covering the RIGHT TWO THIRDS
   of the viewport.

   Replaced the RHOFL word-molecule watermark on 2026-08-03. That was one
   fixed 1600x900 SVG stretched over whatever the viewport happened to be, so
   it ran out at the bottom of a long page and its letters read as content
   rather than texture. This is a tile - theming/hexfield.py emits it, and a
   flat-top honeycomb repeats every 2 columns and every row, so the tile butts
   against its own copies with no seam.

   The left third stays clear. A field that reaches the left edge crowds the
   content that sits there, and the asymmetry is what stops the texture
   reading as wallpaper. */
.aurora-hex {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 33.33%;
  background-image: url("../img/hex-field.svg");
  background-repeat: repeat;
  background-position: top right;
  opacity: var(--hex-opacity);
  /* Fade in from the clear third, so the field starts rather than begins
     abruptly at a vertical line. */
  -webkit-mask-image: linear-gradient(
    90deg,
    transparent 0%,
    #000 22%,
    #000 100%
  );
  mask-image: linear-gradient(90deg, transparent 0%, #000 22%, #000 100%);
}

@media (max-width: 760px) {
  /* A third of a phone is not much room to keep clear, and the field is
     texture rather than layout - it can have more of the screen. */
  .aurora-hex {
    left: 15%;
  }
}

/* --- The window --------------------------------------------------------- */

/* A panel's border is a HOLE through to the aurora. The colour on a card's
   edge is the light actually behind that card, at that moment - so a card
   under the red band has a red edge, one under the magenta band has a
   magenta edge, and every edge on the page changes as the bands drift.
   Nothing is synchronised because nothing is copied.

   HOW IT WORKS: background-clip: padding-box stops the panel's own fill at
   the inside of the border, and the border colour is transparent, so the
   ring is a gap. .aurora is a fixed, full-viewport sibling behind all
   content, and what shows through the gap is it.

   WHY IT IS 2px AND NOT 1px. It was 1px first and read as flat grey on the
   real page. The diagnosis at the time was that the aurora is too
   low-frequency for a ring to sample - which was wrong, or at least not the
   whole story. The aurora being low-frequency is fine: the point is not that
   one card's edge varies along its own length, it is that DIFFERENT cards
   differ and that all of them change over time. The actual problems were
   that 1px is below the width at which a colour registers at all, and that
   a solid hairline was painted on top of the gap.

   The floor is an OUTLINE, not the border. A card over a dark stretch of
   aurora still needs an edge, but painting that into the border colour fills
   the very gap the aurora is meant to come through. It sits just outside
   instead, so the two add rather than one hiding the other.

   backdrop-filter is FORBIDDEN on a windowed panel: its region is clipped to
   the BORDER box, so the aurora seen through the ring would be the blurred
   aurora, and the crispness this technique exists for is spent. */

/* TRANSMISSION AND EMISSION, and they are different things.

   The window below is transmission: the ring is a gap, and what shows in it
   is the aurora actually behind that card. It is genuinely driven by the
   background, and it is why a card near a bright band differs from one in
   the dark.

   But transmission has a ceiling that no amount of tuning lifts: a hole can
   never be BRIGHTER than what is behind it. The reference's borders glow
   brighter than the page around them, which is emission - light coming out
   of the edge, not through it. Brightening the aurora raises the ring and
   its surroundings equally and the edge stays exactly as invisible.

   So there are two layers here and the split is deliberate:

     the border  - transmission, real, position-dependent, dim by nature
     the shadow  - emission, NOT sampled from anything, on the aurora's
                   palette and drifting on band-4's period so the two move
                   together instead of being unrelated

   Calling the second one a reflection would be a lie. It is a light source
   that has been tuned to agree with the weather. */

.glass,
.card,
body #content .module,
body #changelist {
  position: relative;
  background: var(--surface);
  background-clip: padding-box;
  border: 2px solid transparent;
  border-radius: 14px;
  outline: 1px solid var(--hairline);
  outline-offset: 0;
  /* The top highlight stays on the panel itself. An INSET shadow paints
     inside the padding box; from the pseudo-layers below - which sit on the
     border box - the same shadow would land 2px high, across the window's
     gap. Static, so it is painted once and never again. (The old animation
     nudged it 0.06 to 0.07 at mid-sweep; that hundredth was never a visible
     event.) */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06);
}

/* HOW THE EMISSION MOVES - second build, 2026-08-04.

   The first build animated box-shadow itself: one keyframes rule on the
   panel, three colours, 31s. Box-shadow is a PAINT-phase property, so every
   panel repainted its whole shadow region every frame, for the life of the
   page. The catalog runs 66 of these panels, and at devicePixelRatio 2 that
   bill arrived as the recurring frame bursts the arc governor kept having
   to be taught about (~31 frames over 34ms in 40s of an otherwise
   16.7ms-median page, measured 2026-08-04 - the bursts in the amendments to
   docs/superpowers/specs/2026-08-04-filament-strike-ownership-design.md
   were THIS animation). It also broke rule 2 at the top of this file -
   motion is transform and opacity ONLY - which it predates.

   Now the shadows are painted ONCE, onto the panel's two pseudo-elements,
   and only their opacity animates - which the compositor does without
   repainting a pixel. ::before wears band-1's red, ::after band-3's violet,
   each over its own copy of the constant ambient drop, and the two
   crossfade on the same 31s alternate loop, still band-4's period.

   THERE IS NO MAGENTA LAYER, and that is arithmetic, not a loss. A panel
   has exactly two pseudo-elements and the sweep had three colours - but the
   old 50% keyframe sat BETWEEN the endpoints, and the crossfade holds both
   layers at 0.6 there, where red stacked on violet lands within a hair of
   it: ring 0.257 against the old 0.26, glow 0.183 against 0.18, doubled
   ambient 0.51 against 0.5. The middle of a sweep is a mix of its ends.

   A side effect worth knowing about: a RUNNING animation's keyframed value
   outranks every normal box-shadow declaration on the panel, so for as long
   as the first build ran, no panel's own shadow ever rendered - not
   .expando's softer lift, not .product-card's hover. Those rules all come
   back into force with this build, which is the cascade working again, not
   a regression. */

.glass::before,
.glass::after,
.card::before,
.card::after,
body #content .module::before,
body #content .module::after,
body #changelist::before,
body #changelist::after {
  content: "";
  position: absolute;
  /* -2px out from the padding box is the border box: the border is the 2px
     window gap, and these shadows must originate exactly where the
     element's own used to. */
  inset: -2px;
  border-radius: inherit;
  /* An outer box-shadow never paints inside its own border box, so neither
     layer can sit over the panel's content - but ::after is ABOVE that
     content for hit-testing, and two of these panels are links. */
  pointer-events: none;
}

.glass::before,
.card::before,
body #content .module::before,
body #changelist::before {
  box-shadow:
    0 0 0 1px rgba(255, 42, 42, 0.22),
    0 0 22px rgba(255, 42, 42, 0.16),
    0 1px 24px rgba(0, 0, 0, 0.5);
  animation: panel-emit-red 31s ease-in-out infinite alternate;
}

.glass::after,
.card::after,
body #content .module::after,
body #changelist::after {
  box-shadow:
    0 0 0 1px rgba(155, 77, 255, 0.24),
    0 0 24px rgba(155, 77, 255, 0.16),
    0 1px 24px rgba(0, 0, 0, 0.5);
  animation: panel-emit-violet 31s ease-in-out infinite alternate;
}

/* Complementary by construction: same duration, easing and 50% stop, so the
   pair sums to 1 at the ends and 1.2 at the middle - the middle is where
   the old animation ran its brightest keyframe. NO will-change: a running
   animation already promotes its layer, and these layers' contents never
   change, which is the entire point of the build. */
@keyframes panel-emit-red {
  0% {
    opacity: 1;
  }
  50% {
    opacity: 0.6;
  }
  100% {
    opacity: 0;
  }
}

@keyframes panel-emit-violet {
  0% {
    opacity: 0;
  }
  50% {
    opacity: 0.6;
  }
  100% {
    opacity: 1;
  }
}

/* Cards answer on approach. The accent, not the aurora: a hover is the site
   responding, and the weather does not care that a cursor arrived. */
.product-card,
.product-row {
  transition: outline-color 140ms ease, box-shadow 140ms ease,
    transform 140ms ease;
}

.product-card:hover,
.product-card:focus-visible,
.product-row:hover,
.product-row:focus-visible {
  outline-color: var(--accent);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.09),
    0 6px 34px rgba(255, 42, 42, 0.26);
}

.product-card:hover {
  transform: translateY(-3px);
}

@media (prefers-reduced-motion: reduce) {
  /* The still: the mid-sweep pose, painted the old single-shadow way. The
     moving layers are REMOVED rather than paused - a paused animation still
     holds its compositor layer, and a page that asked for no motion should
     not be billed for motion's plumbing. */
  .glass,
  .card,
  body #content .module,
  body #changelist {
    box-shadow:
      inset 0 1px 0 rgba(255, 255, 255, 0.06),
      0 0 0 1px rgba(255, 0, 127, 0.2),
      0 1px 24px rgba(0, 0, 0, 0.5);
  }

  .glass::before,
  .glass::after,
  .card::before,
  .card::after,
  body #content .module::before,
  body #content .module::after,
  body #changelist::before,
  body #changelist::after {
    content: none;
  }

  .product-card,
  .product-row {
    transition: none;
  }

  .product-card:hover {
    transform: none;
  }
}

/* Only ONE level may be a window. An opaque ancestor between a panel and the
   aurora means a nested ring shows its parent's fill, not the sky - which is
   a grey line pretending to be light. */
.glass .glass,
.card .card,
.glass .card,
.card .glass {
  background: transparent;
  border: 1px solid var(--rule);
  outline: 0;
  box-shadow: none;
}

/* The emission goes with the window. Under the first build it did not:
   the running animation's keyframed box-shadow beat this rule's `none` in
   the cascade, so a nested card kept the full glow the rule above says it
   must not have. Layered shadows lose that fight honestly - no layers, no
   glow. */
:is(.glass, .card) :is(.glass, .card)::before,
:is(.glass, .card) :is(.glass, .card)::after {
  content: none;
}

/* --- The filaments ------------------------------------------------------- */

/* Each thread re-strikes on its own phase and duration, both set inline by
   theming/arc.py. A bundle flickering in unison reads as one object blinking;
   threads on separate clocks read as a discharge.

   THIS RULE IS FOR STILL ARCS ONLY - a moving arc opts out via .arc-live
   below, and the distinction is the whole 2026-08-04 bug. */
/* NO will-change HERE, and its removal was a bug fix rather than a tidy-up.

   `will-change: opacity` was correct while the geometry was static: the only
   thing changing was opacity, which the compositor can animate on a promoted
   layer without touching pixels. It promoted eighteen threads and cost
   nothing.

   Once static/js/arc.js began rewriting `d` every frame that inverted. A
   promoted layer whose CONTENTS change has to be re-rasterised every frame,
   and eighteen of them re-rasterising at once flickered the whole page.
   Promotion buys nothing when the layer is invalidated anyway - it only adds
   memory and a raster step.

   REMOVING IT WAS HALF THE FIX, found 2026-08-04 on a laptop iGPU that
   glitched the whole page at four threads: a RUNNING animation promotes its
   element exactly as will-change did, so this very animation was rebuilding
   the same eighteen-layer problem the removal was meant to end. Hence
   .arc-live. Measured on that laptop, nine threads with this animation ran a
   33ms p95 frame; without it, a flat 16.7 - and thirty threads held 60Hz. */
.arc-filament {
  animation-name: filament-strike;
  animation-timing-function: ease-in-out;
  animation-iteration-count: infinite;
  animation-direction: alternate;
}

/* A moving arc's fade belongs to static/js/arc.js, which writes opacity in
   the same pass that writes `d` - free on an element re-rasterising anyway,
   and no promotion trigger. The class comes from the server (theming/arc.py
   emits it on any arc whose payload says animate) rather than from arc.js,
   so the animation never runs at all on a moving arc - a script-added class
   would let it start at first paint and hand over mid-flicker. */
.arc-live .arc-filament {
  animation: none;
}

@keyframes filament-strike {
  0% {
    opacity: 0.12;
  }
  35% {
    opacity: 0.85;
  }
  55% {
    opacity: 0.4;
  }
  100% {
    opacity: 1;
  }
}

/* The halo does NOT flicker. A shared glow that pulses with the threads
   turns the whole arc on and off; keeping it steady is what makes the
   threads look like they are moving THROUGH something. */

@media (prefers-reduced-motion: reduce) {
  .arc-filament {
    animation: none;
  }
}
