/* ============================================================
   BORDER LIGHTS
   A couple of soft pools of colour drift around an element's edge. The border
   reads dark until a pool reaches it; where one sits, the edge lights up and
   spills a little glow into the air around it.

   HOW IT IS BUILT
   Four layers, two pools, one clock:

     .beam-ring::before   pool one, on the border
     .beam-ring::after    pool two, counter-drifting, screened over the first
     .beam-host::after    pool one's spill  — behind the element, blurred wide
     .beam-host::before   pool two's spill

   The spill is what makes this read as light rather than as a painted border.
   It has to move WITH its pool: a static outer glow — which is all a box-shadow
   can be — sits still while the border brightens and dims underneath it, and
   the eye reads that as two unrelated effects rather than one lamp.

   TWO WAYS OF MOVING THE SAME LIGHT, AND WHY
   The ring layers are oversized squares, spun with `rotate`. That is a
   compositor transform: the gradient is rasterised once and the GPU just turns
   the texture. It matters here because those squares are 170% of the host and
   the host can be 1200px wide — re-rasterising a 2000px conic gradient every
   frame, forever, on every admin page, is not a thing to do for decoration.

   The spill layers cannot be spun, because they have to hug the host's own
   rounded rectangle and a spinning rounded rectangle is obviously wrong. They
   animate the gradient's start angle instead, which does re-rasterise — but
   they are host-sized rather than 170%-square, so it is a fraction of the work.

   Rotating a layer by an angle and offsetting a conic gradient's `from` by the
   same angle move the light identically, so the two techniques stay locked
   together with no extra bookkeeping.

   --beam-angle is registered below so it can be interpolated at all: an
   unregistered custom property animates as a discrete jump, which would make
   the light teleport. inherits:false is load-bearing too — it gives every
   element animating it its own copy, which is what lets both spill layers run
   on independent clocks off one keyframe.

   Shared by the public form and the admin dashboard — both link this file, so
   the effect cannot drift between them.

   Usage:
     <div class="beam-host">            (position: relative comes from here)
       <span class="beam-ring" aria-hidden="true"></span>
       ...content...
     </div>

   Tunables, set on .beam-host:
     --beam-from      first pool's colour   (default brand blue)
     --beam-to        second pool's colour  (default brand magenta)
     --beam-duration  one full drift        (default 14s)
     --beam-width     lit band thickness    (default 2px)
     --beam-blur      softness on the band  (default 3px)
     --beam-glow      how far light spills  (default 26px; 0 turns it off)
     --beam-spill     spill strength        (default 0.9)
     --beam-opacity   overall strength      (default 1)
   ============================================================ */

@property --beam-angle {
  syntax: '<angle>';
  initial-value: 0turn;
  inherits: false;
}

.beam-host {
  position: relative;
}

.beam-ring {
  position: absolute;
  inset: 0;
  /* inherit means the ring always traces the host's own corners. */
  border-radius: inherit;
  padding: var(--beam-width, 2px);
  pointer-events: none;
  overflow: hidden;
  opacity: var(--beam-opacity, 1);
  /* Contain the screen blending so it cannot reach the page behind. */
  isolation: isolate;

  /* Punch the middle out so only the border ring paints. Two layers, the
     inner one clipped to the content box, composited as a difference. */
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite: exclude;
}

/* Oversized and square so the gradient's centre stays put and its corners stay
   covered however wide the host is, at any rotation. */
.beam-ring::before,
.beam-ring::after {
  content: '';
  position: absolute;
  top: 50%;
  left: 50%;
  width: 170%;
  aspect-ratio: 1;
  translate: -50% -50%;
  /* Light, deliberately. The lit band is only a couple of pixels wide, so a
     heavy blur here is mostly thrown away by the mask and all it achieves is a
     dimmer line. The softness people actually see comes from the spill. */
  filter: blur(var(--beam-blur, 3px));
  animation: beam-drift var(--beam-duration, 14s) linear infinite;
}

/* A pool, not a lit ring.
 *
 * The long transparent run is the point. The gradient this replaced never
 * dropped below about a tenth of full colour, so every edge was always tinted
 * and nothing ever looked like it was arriving. Holding at transparent for most
 * of the turn means the border is genuinely dark until the light reaches it.
 *
 * The short plateau at full strength gives the pool a bright core instead of a
 * long even smear — on a wide element a degree of arc covers a lot of edge, so
 * both the plateau and the falloff stay narrow. */
.beam-ring::before {
  background: conic-gradient(
    from 0turn,
    transparent               0deg,
    transparent              26deg,
    var(--beam-from, #2ea6ff) 47deg,
    var(--beam-from, #2ea6ff) 55deg,
    transparent              76deg,
    transparent             360deg
  );
}

/* The second pool runs the other way and slower, so the two slide through each
   other and the colour never repeats on a fixed beat. Screened, so where they
   cross the light adds up instead of one hiding the other. */
.beam-ring::after {
  background: conic-gradient(
    from 0turn,
    transparent             182deg,
    var(--beam-to, #c637ff) 203deg,
    var(--beam-to, #c637ff) 211deg,
    transparent             232deg,
    transparent             360deg
  );
  mix-blend-mode: screen;
  animation-duration: calc(var(--beam-duration, 14s) * 1.7);
  animation-direction: reverse;
}

/* ---- The spill ----
 * The same two pools on the host's own shape, blurred hard enough to carry past
 * the edge. z-index: -1 puts them under the host's background: outside the
 * element you see the light at full strength, inside it is filtered through
 * whatever the card is made of. That difference is what makes the edge itself
 * read as the bright part. */
.beam-host::before,
.beam-host::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: -1;
  opacity: calc(var(--beam-spill, 0.9) * var(--beam-opacity, 1));
  /* Blurring a filled rounded rectangle pushes colour out past its edge by
     roughly the blur radius — that spread IS the glow, so --beam-glow is a blur
     and needs no separate spread. */
  filter: blur(var(--beam-glow, 26px));
  animation: beam-sweep var(--beam-duration, 14s) linear infinite;
}

.beam-host::after {
  background: conic-gradient(
    from var(--beam-angle),
    transparent               0deg,
    transparent              26deg,
    var(--beam-from, #2ea6ff) 47deg,
    var(--beam-from, #2ea6ff) 55deg,
    transparent              76deg,
    transparent             360deg
  );
}

.beam-host::before {
  background: conic-gradient(
    from var(--beam-angle),
    transparent             182deg,
    var(--beam-to, #c637ff) 203deg,
    var(--beam-to, #c637ff) 211deg,
    transparent             232deg,
    transparent             360deg
  );
  mix-blend-mode: screen;
  animation-duration: calc(var(--beam-duration, 14s) * 1.7);
  animation-direction: reverse;
}

/* Spun for the ring's squares. */
@keyframes beam-drift {
  to { rotate: 1turn; }
}

/* Swept for the spill's rounded rectangles. Same movement, different mechanism
   — see the note at the top of this file. */
@keyframes beam-sweep {
  to { --beam-angle: 1turn; }
}

/* Still lit, just no longer moving. */
@media (prefers-reduced-motion: reduce) {
  .beam-ring::before,
  .beam-ring::after,
  .beam-host::before,
  .beam-host::after {
    animation: none;
  }
}

/* mask-composite is what makes the ring a ring rather than a filled block.
   Without it the gradient would cover the content, so the border pools are
   dropped — the spill is unmasked and survives on its own, so the element stays
   lit rather than going flat. */
@supports not ((mask-composite: exclude) or (-webkit-mask-composite: xor)) {
  .beam-ring { display: none; }
}
