/* Salty Web Co — the border chaser.
 *
 * Named .chase, not .chase: the marketing site already uses .chase for the
 * big ambient blur behind the hero, and two rules called the same thing
 * fighting over position and background is nobody’s idea of a good time.
 *
 * The trick, from the reel:
 *
 *   @property --a { syntax:"<angle>"; initial-value:0deg }
 *   @keyframes spin { to { --a: 360deg } }
 *   .card { background: conic-gradient(from var(--a), …);
 *           animation: spin 3s linear infinite }
 *
 * A conic-gradient cannot be animated on its own — the browser has no idea
 * how to move between two of them, so it jumps. Registering --a through
 * @property gives it a type, and a typed custom property CAN be animated,
 * so the gradient sweeps round instead.
 *
 * Here that gradient goes on two pseudo-elements sitting BEHIND the box and
 * slightly larger than it, so the ring shows around the outside of the
 * element rather than inside its edge. One is sharp — that is the ring. The
 * other is blurred and spreads outward — that is the glow.
 *
 * The element needs its own solid background, because that background is
 * what covers the middle of the gradient. --chase-face exists so the variants
 * can state which surface they sit on rather than guessing.
 *
 * This only works on a DARK face. The ring is a thin bright edge, and a
 * bright edge against a bright button is just a smudge — it needs something
 * dark to sit against, which is why every variant below names a dark
 * surface. A glowing dark button beside a flat orange one reads as the more
 * important of the two anyway.
 *
 * Colour carries meaning: orange is "press this", red is "this is broken",
 * green is "this is settled". A page with three glowing things has none.
 *
 *   <a class="btn chase on-raised">Primary action</a>
 *   <div class="alert chase bad always">Payment failed</div>
 *   <input class="chase field cool" placeholder="Search anything…">
 *
 * Three named uses so far:
 *   the chaser glow button  — a dark CTA, orange, on hover
 *   the payment failed chaser — a red alert, running on its own
 *   the search field chaser — runs while the field has focus
 *
 * Load this AFTER the page's own styles so its background wins.
 */

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

@keyframes swc-chase {
  to { --angle: 360deg }
}

.chase {
  --chase-dim:  rgb(255 122 26 / .18);  /* the ring between passes   */
  --chase-mid:  #FF7A1A;                /* the head building         */
  --chase-hot:  #FFFFFF;                /* the hot point itself      */
  --chase-tail: #FFD9B0;                /* what it leaves behind     */
  --chase-face: #121417;   /* the middle — match the surface it sits on */
  --chase-ring: 2px;       /* how far the ring reaches past the edge */
  --chase-spread: 9px;     /* how far the glow carries — close, not a floodlight */
  --chase-strength: .55;
  --chase-speed: 3s;       /* one full turn, as in the reel */
  --chase-radius: 12px;    /* the element's own corner radius — see below */

  position: relative;
  background: var(--chase-face);
  -webkit-tap-highlight-color: transparent;   /* iOS paints a dark square on tap */
}

/* The gradient is written on the PSEUDO-ELEMENT, not routed through a
   --chase-sweep variable declared on the parent.

   This is the whole trick and the easiest thing to get wrong. A custom
   property's var()s are resolved on the element that DECLARES it. Put the
   conic-gradient in --chase-sweep on .chase and its var(--angle) resolves
   against .chase's own --angle — which is 0deg forever, because @property
   says inherits:false and nothing animates the parent. The pseudo-element
   then inherits a dead string with "from 0deg" baked in, and the ring sits
   perfectly still while --angle animates away on the pseudo, unused. */
.chase::before,
.chase::after {
  content: '';
  position: absolute;
  inset: calc(var(--chase-ring) * -1);
  /* A box grown outward by the ring needs a radius grown by the ring, or its
     corners come out squarer than the element's. With border-radius:inherit
     the ring was 2px wide along the edges and noticeably thicker at the four
     corners — invisible for the moment it flashes past on a desktop hover,
     but on a phone, where the dim ring runs all the time, it reads as a dark
     square-cornered edge around a rounded button. */
  border-radius: calc(var(--chase-radius) + var(--chase-ring));
  /* a dim ring with one bright head running round it */
  background: conic-gradient(from var(--angle),
    var(--chase-dim) 0deg, var(--chase-dim) 215deg,
    var(--chase-mid) 300deg, var(--chase-hot) 338deg,
    var(--chase-tail) 350deg, var(--chase-dim) 360deg);
  animation: swc-chase var(--chase-speed) linear infinite;
  z-index: -1;
  pointer-events: none;
}

/* the glow: the same ring, blurred, sitting just outside the edge */
.chase::before {
  inset: calc(var(--chase-ring) * -1.5);
  border-radius: calc(var(--chase-radius) + var(--chase-ring) * 1.5);
  filter: blur(var(--chase-spread));
  opacity: var(--chase-strength);
}

/* On a touch screen the chaser runs the whole time, so it has to be cheap
   and it has to survive WebKit. The blurred glow is neither: it repaints a
   filtered layer every frame, and Safari will sometimes composite a blurred,
   animated, rounded layer as a plain rectangle. The ring stays; the halo
   becomes a still, soft shadow that costs nothing. */
@media not ((hover: hover) and (pointer: fine)) {
  .chase::before { display: none }
  .chase { box-shadow: 0 0 18px -6px var(--chase-mid) }
}

/* ---------- when it runs ----------------------------------------------
   Where there is a real pointer the chaser is a reward for reaching for the
   button: it is gone until then, and the button carries a quiet inset edge
   so it still reads as a button. Where there is no pointer there is no hover
   either, so it simply runs — otherwise every phone would get a button that
   never lights up, which is most of a contractor's traffic.

   At rest the chaser is fully transparent rather than paused-and-visible: a
   stopped comet parks its white head at one fixed spot on the border and
   reads as a blown pixel.
   --------------------------------------------------------------------- */
@media (hover: hover) and (pointer: fine) {
  .chase {
    box-shadow: inset 0 0 0 1px var(--chase-edge, rgb(255 122 26 / .22));
    transition: box-shadow .28s ease;
  }
  .chase::before,
  .chase::after {
    animation-play-state: paused;     /* costs nothing while invisible */
    opacity: 0;
    transition: opacity .3s ease;
  }

  /* :focus-within is here for fields. A search box is not hovered, it is
     entered: click into it and the chaser runs for as long as you are
     typing, then stands down when you leave. */
  .chase:hover,
  .chase:focus-visible,
  .chase:focus-within { box-shadow: inset 0 0 0 1px transparent }

  .chase:hover::after,
  .chase:focus-visible::after,
  .chase:focus-within::after { opacity: 1 }
  .chase:hover::before,
  .chase:focus-visible::before,
  .chase:focus-within::before { opacity: var(--chase-strength) }

  .chase:hover::before, .chase:hover::after,
  .chase:focus-visible::before, .chase:focus-visible::after,
  .chase:focus-within::before, .chase:focus-within::after {
    animation-play-state: running;
  }
}

/* A field carries the chaser more quietly than a button does: it is a large
   target and a full halo round a search box is shouting.

   PUT IT ON A WRAPPER, NEVER ON THE <input> ITSELF. Replaced elements do not
   generate ::before or ::after, so an input with .chase on it renders no ring
   at all — and getComputedStyle still answers as though the pseudo-element
   exists, so it reads as working right up until you look at it.

     <label class="chase field cool">
       <input class="bare" placeholder="Search anything…">
     </label>

   The wrapper carries the background and radius; the input inside is
   transparent with no border and no outline. */
.chase.field {
  --chase-ring: 1px;
  --chase-spread: 7px;
  --chase-strength: .4;
  --chase-edge: rgb(255 255 255 / .1);
}

/* The ring lives on pseudo-elements behind the box, which means a negative
   z-index. Those do not stay put: they rise to the nearest ancestor that
   makes a stacking context, and if nothing does, they end up behind the
   panel the glow is sitting in and disappear entirely. Anything holding a
   glow therefore makes its own context, which pins them in place.

   Without :has the glow is simply hidden in that one case — the element
   keeps its background and reads as an ordinary card, which is a fine thing
   to fall back to. */
:has(> .chase) { isolation: isolate }

/* ---------- what the colours mean ---------- */

/* something is broken and wants you now */
.chase.bad  { --chase-dim:rgb(226 61 61 / .2); --chase-mid:#E23D3D; --chase-hot:#FFFFFF;
              --chase-tail:#FFC9C9; --chase-edge:rgb(226 61 61 / .3) }

/* settled, signed, paid */
.chase.good { --chase-dim:rgb(16 185 129 / .2); --chase-mid:#10B981; --chase-hot:#FFFFFF;
              --chase-tail:#C7FFE9; --chase-edge:rgb(16 185 129 / .3) }

/* interesting, not urgent */
.chase.cool { --chase-dim:rgb(47 123 255 / .2); --chase-mid:#2F7BFF; --chase-hot:#FFFFFF;
              --chase-tail:#B45CFF; --chase-edge:rgb(47 123 255 / .3) }

/* the counter-spin: the direction flip alone changes the personality */
.chase.luxe { --chase-dim:rgb(201 138 24 / .2); --chase-mid:#E3B23C; --chase-hot:#FFF8E1; --chase-tail:#C98A18 }
.chase.luxe::before,
.chase.luxe::after { animation-direction: reverse }

/* Not everything with a chaser is hoverable. An alert saying a payment
   failed should run on its own — nobody hovers a warning to find out it is
   there. */
.chase.always,
.chase.always::before,
.chase.always::after { animation-play-state: running !important }
.chase.always { box-shadow: none }
.chase.always::after { opacity: 1 }
.chase.always::before { opacity: var(--chase-strength) }

/* a thin outline rather than a lit edge — for inputs and large panels,
   where a full halo would be shouting */
.chase.quiet { --chase-ring:1px; --chase-spread:6px; --chase-strength:.4; --chase-speed:6s }

/* ---------- which dark surface the middle has to match ---------- */
.chase.on-panel  { --chase-face:#121417 }
.chase.on-ground { --chase-face:#0A0B0D }
.chase.on-raised { --chase-face:#171A1E }

/* ---------- when the trick is not available ---------- */

/* Without @property the angle cannot animate, so the ring would sit frozen
   at one position and read as a mistake. Draw a plain lit edge instead. */
@supports not (background: conic-gradient(from var(--angle), red, blue)) {
  .chase::before,
  .chase::after { display: none }
  .chase {
    box-shadow: 0 0 0 var(--chase-ring) var(--chase-mid), 0 0 26px -6px var(--chase-mid);
  }
}

/* Anyone who has asked their machine to stop moving things gets the ring
   held still rather than taken away — the emphasis survives, the spin does
   not. */
@media (prefers-reduced-motion: reduce) {
  .chase::before,
  .chase::after { animation: none }
}
