/* ============================================================
   PiggyCS motion system.

   The design rules in styles.css say what the site LOOKS like. These say how
   it MOVES, and they exist for the same reason: without a stated rule, motion
   drifts into the generic fade-up-everything that every dark template ships,
   which is exactly what reads as machine-made.

   1. MOTION MUST MEAN SOMETHING. Every animation here is tied to a real
      event: a figure changed, a place changed hands, the stream went live,
      money moved. Decorative motion with no referent is banned. If you can't
      name what it's telling the viewer, delete it.
   2. THE HAIRLINE IS THE BRAND. This design is built from 1px rules, so the
      signature entrance is a rule DRAWING itself left-to-right, not a box
      sliding up. Structure assembling itself.
   3. NUMBERS TICK, THEY DON'T FADE. It's a betting board. Figures roll like
      a sportsbook display, in tabular mono so nothing reflows while counting.
   4. ACCELERATE OUT, SETTLE IN. Things leave fast and arrive slow. Never a
      symmetric ease — that's what makes motion feel like a template.
   5. ONE THING MOVES AT A TIME. Staggers are short and capped. If ten
      elements animate at once the eye has nowhere to land.
   6. IT ALL SWITCHES OFF. Every rule here is inside a reduced-motion guard.
      Nothing below is load-bearing for reading the page.
   ============================================================ */

:root {
  /* Durations. Named for intent, not length, so call sites read as meaning. */
  --t-tap:    90ms;    /* immediate feedback — a press, a toggle */
  --t-quick:  180ms;   /* hover, small state change */
  --t-move:   340ms;   /* something travels */
  --t-reveal: 620ms;   /* an element arrives on screen */
  --t-draw:   760ms;   /* a rule draws itself */

  /* Easings. Asymmetric by design — see rule 4. */
  --e-out:   cubic-bezier(.16, 1, .3, 1);      /* settle: fast then glide */
  --e-in:    cubic-bezier(.7, 0, .84, 0);      /* exit: hesitate then go */
  --e-snap:  cubic-bezier(.34, 1.56, .64, 1);  /* overshoot, for wins only */
}

/* ============================================================
   1. Reveal on scroll — the hairline draw
   ============================================================ */

/* The element is present and readable before JS runs; motion.js only adds
   .is-in. If the script never loads, nothing is hidden. That's deliberate —
   content must not depend on an animation to become visible. */
@media (prefers-reduced-motion: no-preference) {

  .js-motion [data-reveal] {
    opacity: 0;
    transform: translate3d(0, 14px, 0);
    transition:
      opacity var(--t-reveal) var(--e-out),
      transform var(--t-reveal) var(--e-out);
    transition-delay: var(--reveal-delay, 0ms);
  }
  .js-motion [data-reveal].is-in {
    opacity: 1;
    transform: none;
  }

  /* The signature: a rule that draws itself across. Used on section heads and
     statlist borders, where the hairline IS the structure. */
  .js-motion [data-reveal="rule"] {
    opacity: 1;
    transform: none;
    position: relative;
  }
  .js-motion [data-reveal="rule"]::after {
    content: '';
    position: absolute;
    left: 0; bottom: -1px;
    height: 1px;
    width: 100%;
    background: var(--brand);
    transform: scaleX(0);
    transform-origin: 0 50%;
    transition: transform var(--t-draw) var(--e-out);
    transition-delay: var(--reveal-delay, 0ms);
  }
  .js-motion [data-reveal="rule"].is-in::after { transform: scaleX(1); }

  /* Text that arrives by weight rather than position — for headlines, where a
     slide would fight the type's own vertical rhythm. */
  .js-motion [data-reveal="type"] {
    opacity: 0;
    transform: translate3d(0, 0, 0) scale(.985);
    transition:
      opacity var(--t-reveal) var(--e-out),
      transform var(--t-reveal) var(--e-out);
    transition-delay: var(--reveal-delay, 0ms);
  }
  .js-motion [data-reveal="type"].is-in { opacity: 1; transform: none; }
}

/* ============================================================
   2. The odds board — figures that tick
   ============================================================ */

/* Mono + tabular is already the house rule for numbers; this makes the
   guarantee explicit because a counting figure is where proportional digits
   would visibly jitter. */
[data-count] {
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
}

@media (prefers-reduced-motion: no-preference) {
  /* A figure that just changed gets a brief accent lift, then falls back to
     its resting colour. Direction is set by motion.js. */
  [data-count].is-up   { animation: tick-up   var(--t-move) var(--e-out); }
  [data-count].is-down { animation: tick-down var(--t-move) var(--e-out); }

  @keyframes tick-up {
    0%   { color: var(--green); transform: translate3d(0, 3px, 0); }
    100% { color: inherit;      transform: none; }
  }
  @keyframes tick-down {
    0%   { color: var(--brand); transform: translate3d(0, -3px, 0); }
    100% { color: inherit;      transform: none; }
  }

  /* The last second of a countdown segment. Marks time actually passing
     rather than decorating the number. */
  .statrow__cd b.is-tick { animation: cd-tick var(--t-quick) var(--e-out); }
  @keyframes cd-tick {
    0%   { color: var(--bone); transform: translate3d(0, -2px, 0); }
    100% { color: inherit;     transform: none; }
  }
}

/* ============================================================
   3. Place changes — the leaderboard's one real event
   ============================================================ */

@media (prefers-reduced-motion: no-preference) {
  /* Someone overtook someone. This is the most meaningful thing that happens
     on this site, so it gets the most emphatic motion in the system. */
  .pod.is-climb, .row.is-climb {
    animation: climb var(--t-reveal) var(--e-out);
  }
  .pod.is-fall, .row.is-fall {
    animation: fall var(--t-reveal) var(--e-out);
  }

  @keyframes climb {
    0%   { transform: translate3d(0, 18px, 0); background: rgba(52, 199, 110, .16); }
    100% { transform: none;                    background: transparent; }
  }
  @keyframes fall {
    0%   { transform: translate3d(0, -18px, 0); background: rgba(255, 46, 77, .12); }
    100% { transform: none;                     background: transparent; }
  }

  /* A brand-new entrant slides in from the rail rather than dropping in. */
  .pod.is-new, .row.is-new { animation: enter-rail var(--t-reveal) var(--e-out); }
  @keyframes enter-rail {
    0%   { opacity: 0; transform: translate3d(-22px, 0, 0); }
    100% { opacity: 1; transform: none; }
  }
}

/* ============================================================
   4. Live state — the only thing allowed to loop
   ============================================================ */

/* A persistent loop is normally banned (rule 1: it has no event). This one
   earns it: "we are live right now" is a continuously true claim, and a
   static dot can't distinguish live from stale. */
@media (prefers-reduced-motion: no-preference) {
  .livechip__dot,
  .live__status.is-live i,
  .lv__status.is-on {
    animation: live-pulse 2.4s var(--e-out) infinite;
  }
  @keyframes live-pulse {
    0%, 100% { opacity: 1; }
    50%      { opacity: .35; }
  }
}

/* ============================================================
   5. Interface feedback
   ============================================================ */

/* Buttons are pills now, so the old underline wipe drew a straight bar across
   a rounded shape. A pill's natural hover is the fill itself moving — the
   brand colour sweeps in from the left edge under the label. */
.btn { position: relative; overflow: hidden; isolation: isolate; }
.btn::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: var(--brand-dk);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform var(--t-move) var(--e-out);
}
@media (prefers-reduced-motion: no-preference) {
  .btn:hover::before, .btn:focus-visible::before { transform: scaleX(1); }
  .btn:active { transform: scale(.98); transition: transform var(--t-tap); }

  /* Ghost buttons fill with the brand instead, and flip their label to the
     dark on-brand colour so contrast holds through the transition. */
  .btn--ghost::before { background: var(--brand); }
  .btn--ghost { transition: color var(--t-move) var(--e-out), border-color var(--t-move) var(--e-out); }
  .btn--ghost:hover { color: var(--on-brand); border-color: var(--brand); }
}

/* Nav links get the same underline logic, so the header shares the vocabulary
   rather than inventing a third hover style. */
.nav__links a { position: relative; }
.nav__links a::after {
  content: '';
  position: absolute;
  left: 10px; right: 10px; bottom: 12px;
  height: 1px;
  background: var(--brand);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform var(--t-quick) var(--e-out);
}
@media (prefers-reduced-motion: no-preference) {
  .nav__links a:hover::after { transform: scaleX(1); }
}

/* Cards lift by hairline, not by shadow — consistent with rule 2 of the
   visual system, which bans blurred depth. */
.gamecard, .pod, .prize {
  transition:
    border-color var(--t-quick) var(--e-out),
    background var(--t-quick) var(--e-out),
    transform var(--t-quick) var(--e-out);
}
@media (prefers-reduced-motion: no-preference) {
  .gamecard:hover { transform: translate3d(0, -3px, 0); }
}

/* Menu panel: opens as a wipe from the button edge, not a fade. */
@media (prefers-reduced-motion: no-preference) {
  .menu.is-open .menu__panel { animation: panel-open var(--t-move) var(--e-out); }
  @keyframes panel-open {
    0%   { opacity: 0; transform: translate3d(0, -6px, 0); clip-path: inset(0 0 100% 0); }
    100% { opacity: 1; transform: none;                    clip-path: inset(0 0 0 0); }
  }
}

/* ============================================================
   6. Copy-to-clipboard confirmation
   ============================================================ */

@media (prefers-reduced-motion: no-preference) {
  .copy.is-copied { animation: copied var(--t-move) var(--e-snap); }
  @keyframes copied {
    0%   { transform: scale(1); }
    40%  { transform: scale(1.04); }
    100% { transform: scale(1); }
  }
}

/* ============================================================
   7. Page-level: the shell settling in
   ============================================================ */

/* The nav draws its bottom hairline once on load. Small, but it frames the
   page as assembling rather than simply appearing. */
@media (prefers-reduced-motion: no-preference) {
  .nav { position: sticky; }
  .nav::after {
    content: '';
    position: absolute;
    left: 0; bottom: -1px;
    height: 1px; width: 100%;
    background: var(--brand);
    transform: scaleX(0);
    transform-origin: 0 50%;
    animation: draw-in var(--t-draw) var(--e-out) .1s forwards;
  }
  @keyframes draw-in {
    to { transform: scaleX(1); }
  }
}


/* ============================================================
   8. Brand mark — the one piece of pure character
   ============================================================ */

/* The wordmark is the site's only proprietary asset, so it gets to be
   playful. A pig-like waggle on hover, and only on hover — it never loops,
   so it can't become wallpaper. */
.nav__mark { transform-origin: 12% 60%; }
@media (prefers-reduced-motion: no-preference) {
  .nav__brand:hover .nav__mark { animation: mark-waggle 620ms var(--e-out); }
  @keyframes mark-waggle {
    0%   { transform: rotate(0deg)   scale(1); }
    28%  { transform: rotate(-3.5deg) scale(1.04); }
    58%  { transform: rotate(2.5deg)  scale(1.02); }
    100% { transform: rotate(0deg)   scale(1); }
  }
}

/* ============================================================
   9. Scroll progress — where you are in the race page
   ============================================================ */

/* Earns its place by reporting real state: how far down a long board you are.
   Sits under the nav's own hairline so it reads as part of the chrome. */
.scrollbarline {
  position: fixed;
  top: 0; left: 0;
  height: 2px;
  width: 100%;
  transform: scaleX(var(--scrolled, 0));
  transform-origin: 0 50%;
  background: var(--brand);
  z-index: 60;
  pointer-events: none;
}
@media (prefers-reduced-motion: reduce) { .scrollbarline { display: none; } }

/* ============================================================
   10. Board rows — hover reads as "this one"
   ============================================================ */

@media (prefers-reduced-motion: no-preference) {
  .pod, .row { position: relative; }
  .pod::before, .row::before {
    content: '';
    position: absolute;
    left: 0; top: 0; bottom: 0;
    width: 2px;
    background: var(--brand);
    transform: scaleY(0);
    transform-origin: 50% 50%;
    transition: transform var(--t-quick) var(--e-out);
  }
  .pod:hover::before, .row:hover::before { transform: scaleY(1); }
}

/* ============================================================
   11. Cards — lift on the radius, not on a shadow
   ============================================================ */

@media (prefers-reduced-motion: no-preference) {
  .gamecard, .prize {
    transition:
      transform var(--t-quick) var(--e-out),
      border-color var(--t-quick) var(--e-out),
      background var(--t-quick) var(--e-out);
  }
  .gamecard:hover { transform: translate3d(0, -4px, 0); border-color: var(--brand); }
  .prize:hover    { transform: translate3d(0, -2px, 0); }
}


/* ============================================================
   12. Hero mark — the brand as hero art

   Rule 1 says decorative loops are banned, and this is the one place the
   system bends. The mark is the site's identity sitting in an otherwise empty
   half of the hero; perfectly still it reads as a stuck image. So it gets a
   very slow, very low-amplitude drift — under 1% of its own size — which is
   below the threshold you consciously notice but above the one that says
   "this page is alive". Anything faster would be wallpaper.

   The real motion here is the pointer parallax in motion.js, which responds
   to actual input rather than to a timer.
   ============================================================ */

@media (prefers-reduced-motion: no-preference) {
  /* Arrival: the mark settles in behind the headline rather than appearing.
     `backwards` holds the start frame during the delay so it can't flash at
     full size for one frame before the animation takes over. */
  .heromark {
    animation:
      mark-arrive 1100ms var(--e-out) 120ms backwards,
      mark-drift 9s ease-in-out 1200ms infinite;
  }

  @keyframes mark-arrive {
    0% {
      opacity: 0;
      transform: translate3d(38px, -50%, 0) rotate(-8deg) scale(.86);
    }
    100% {
      opacity: .9;
      transform: translate3d(0, -50%, 0) rotate(0deg) scale(1);
    }
  }

  /* Kept deliberately tiny — see the note above. */
  @keyframes mark-drift {
    0%, 100% { transform: translate3d(var(--mark-x, 0px), calc(-50% + var(--mark-y, 0px)), 0) rotate(var(--mark-r, 0deg)); }
    50%      { transform: translate3d(var(--mark-x, 0px), calc(-50% + var(--mark-y, 0px) - 10px), 0) rotate(calc(var(--mark-r, 0deg) + 1.2deg)); }
  }

  /* Nudged by the pointer. motion.js writes --mark-x/y/r; the transition
     smooths them so the mark trails the cursor instead of snapping to it. */
  .heromark.is-tracking {
    animation: none;
    transition: transform 600ms var(--e-out);
  }
}

/* Reduced motion: present, still, and slightly calmer. */
@media (prefers-reduced-motion: reduce) {
  .heromark { opacity: .7; }
}
