/* ═══ mu-src/00-core.css ═══ */
/* Aimlink | Magic UI ports (styles for assets/mu.js, built by assets/build_mu.py from assets/mu-src/*.css). */
.mu-sw { display: inline-block; white-space: nowrap; }
.mu-ch { display: inline-block; }

/* ═══ mu-src/fx.css ═══ */
/* Aimlink | Magic UI ports, group "fx": BorderBeam, ShineBorder, Backlight, Ripple (styles for assets/mu-src/fx.js). */

/* ───────── BorderBeam: a lime light riding the border of a dark panel ───────── */
.mu-fx-beam {
  position: absolute; inset: 0; z-index: 3; pointer-events: none; border-radius: inherit; box-sizing: border-box;
  padding: var(--mu-fx-beam-w, 1px);
  /* only the border band shows: the whole box minus its content box */
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  contain: layout paint;   /* the moving light cannot invalidate anything outside its ring */
  --mu-fx-beam-a: var(--color-lime, #A0E020);
  --mu-fx-beam-b: rgb(160 224 32 / 0.55); /* lime at 55% */
}
.mu-fx-beam-dot {
  position: absolute; top: 0; left: 0; width: var(--mu-fx-beam-size, 90px); aspect-ratio: 1;
  background: linear-gradient(to left, var(--mu-fx-beam-a), var(--mu-fx-beam-b) 35%, transparent);
  offset-path: rect(0 auto auto 0 round var(--mu-fx-beam-r, var(--radius-panel, 26px)));
  offset-distance: 0%;
  animation: mu-fx-beam-run var(--mu-fx-beam-dur, 8s) linear var(--mu-fx-beam-delay, 0s) infinite;
}
@keyframes mu-fx-beam-run { to { offset-distance: 100%; } }
/* the ring is a border made with mask-composite; where that is missing the layers stay hidden instead of filling the box */
.mu-fx-beam, .mu-fx-shine { display: none; }
@supports ((mask-composite: exclude) or (-webkit-mask-composite: xor)) { .mu-fx-beam, .mu-fx-shine { display: block; } }

/* the sheet: a zero-height strip pinned to the top of the scrolling dialog; the ring is as tall as the dialog is
   (one pixel less, so the bottom edge is not lost to sub-pixel rounding) */
.mu-fx-beam-strip { position: sticky; top: 0; z-index: 4; display: block; height: 0; pointer-events: none; }
.mu-fx-beam-strip .mu-fx-beam {
  inset: auto; top: 0; left: 0; width: 100%; height: calc(var(--mu-fx-sheet-h, 100%) - 1px);
  border-radius: var(--radius-panel, 26px); --mu-fx-beam-r: var(--radius-panel, 26px);
}
@media (max-width: 599.98px) {
  /* on phones the sheet is attached to the bottom edge: square lower corners */
  .mu-fx-beam-strip .mu-fx-beam { border-radius: var(--radius-panel, 26px) var(--radius-panel, 26px) 0 0; --mu-fx-beam-r: var(--radius-panel, 26px) var(--radius-panel, 26px) 0 0; }
}

/* the QR box in the footer: the beam only runs while the box is on screen */
.store-qr { position: relative; }
.store-qr .mu-fx-beam { --mu-fx-beam-r: var(--radius-icon, 14px); }
/* a gated beam (QR box, Explainer film) waits for its host to be on screen; the first frame is paused too, so nothing runs before it is seen */
.mu-fx-beam.is-gated:not(.is-live) .mu-fx-beam-dot { animation-play-state: paused; }

/* ───────── ShineBorder: a slow fern shimmer along the border of a light card ─────────
   The ring is the border band only (mask), and the light is a 3x layer inside it that slides on a diagonal and back: the same picture the old
   background-position loop drew (a radial band, 300 % size, from the top left to the bottom right and back, 14 s, linear), but it is a transform,
   so the compositor moves it and the main thread has nothing to do. It is paused until the card is on screen. */
.mu-fx-shine {
  position: absolute; inset: 0; z-index: 2; pointer-events: none; border-radius: inherit; box-sizing: border-box; overflow: hidden;
  padding: 1.5px;
  -webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
}
.mu-fx-shine-glow {
  position: absolute; left: 0; top: 0; width: 300%; height: 300%; pointer-events: none;
  background-image: radial-gradient(transparent, transparent, var(--color-fern, #547820), transparent, transparent);
  animation: mu-fx-shine 14s linear infinite paused;
}
.mu-fx-shine.is-live .mu-fx-shine-glow { animation-play-state: running; will-change: transform; }
@keyframes mu-fx-shine { 0%, 100% { transform: translate3d(0, 0, 0); } 50% { transform: translate3d(-66.6667%, -66.6667%, 0); } }

/* ───────── Backlight: the logo's own colour, blurred, behind it ───────── */
.mu-fx-defs { position: absolute; width: 0; height: 0; overflow: hidden; pointer-events: none; }
.mu-fx-backlit { filter: url(#mu-fx-backlight); }
/* the hero logo's glow is a blurred copy under it (same chain as the filter: blur 9, saturate 4, 90 % alpha); the host is positioned from the first paint */
.go-hero .product { position: relative; isolation: isolate; }
.mu-fx-bl { position: absolute; z-index: -1; opacity: 0; pointer-events: none; filter: blur(9px) saturate(4); }

/* ───────── Ripple: target rings round the closing call (light ground: fern lines) ───────── */
.app-ask { overflow: clip; }
/* the container is the stacking context, so the rings sit behind everything in it (the headline, the badges and the list below) */
.app-ask > .container { isolation: isolate; }
.app-ask .unit--center { position: relative; }
.mu-fx-ripple {
  position: absolute; left: 50%; top: 50%; z-index: -1; width: 1600px; height: 1600px; margin: -800px 0 0 -800px; pointer-events: none;
  -webkit-mask-image: radial-gradient(closest-side, #000 22%, transparent 100%);
  mask-image: radial-gradient(closest-side, #000 22%, transparent 100%);
  --mu-fx-rp-line: rgb(84 120 32 / 0.34); --mu-fx-rp-fill: rgb(84 120 32 / 0.022);
}
.mu-fx-ripple-ring {
  position: absolute; left: 50%; top: 50%; box-sizing: border-box; border-radius: 50%;
  width: calc(340px + var(--mu-fx-i) * 150px); aspect-ratio: 1;
  border: 1px solid var(--mu-fx-rp-line); background: var(--mu-fx-rp-fill);
  opacity: calc(1 - var(--mu-fx-i) * 0.13);
  transform: translate(-50%, -50%) scale(1);
  animation: mu-fx-ripple 6s ease-in-out calc(var(--mu-fx-i) * 0.14s) infinite paused;
}
.mu-fx-ripple.is-live .mu-fx-ripple-ring { animation-play-state: running; }
@keyframes mu-fx-ripple { 50% { transform: translate(-50%, -50%) scale(0.93); } }

/* reduced motion: no beams, no shimmer; the rings stand still (the finished state) */
@media (prefers-reduced-motion: reduce) {
  .mu-fx-beam, .mu-fx-beam-strip, .mu-fx-shine { display: none; }
  .mu-fx-ripple-ring { animation: none; }
}

/* ── AnimatedBeam: a light runs along the line between two apps ── */
.mu-ab { position: absolute; left: 0; top: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
.mu-ab path { fill: none; stroke-linecap: round; }
.mu-ab-line { stroke: rgb(160 224 32 / 0.28); stroke-width: 1.5; }
.mu-ab-pulse { stroke: var(--color-lime, #A0E020); stroke-width: 2.5; stroke-dasharray: 18 82; stroke-dashoffset: 18; opacity: 0; filter: drop-shadow(0 0 5px rgb(160 224 32 / 0.8)); }
.has-beam.is-live .mu-ab-pulse { opacity: 1; animation: mu-ab-run 2.6s linear infinite; }
.has-beam.is-live .xp-link:nth-of-type(2) .mu-ab-pulse { animation-delay: 1.1s; }
@keyframes mu-ab-run { to { stroke-dashoffset: -82; } }
@media (prefers-reduced-motion: reduce) { .has-beam.is-live .mu-ab-pulse { animation: none; opacity: 0; } }

/* ═══ mu-src/orbit.css ═══ */
/* ── OrbitingCircles (GO: "GO works with"): GO in the middle, TARGET and CLUB on two slow rings. The list beside it is the real, keyboard-reachable one. ── */
.works-body { display: grid; gap: var(--space-6); margin-top: var(--space-3); }
.works-body .works-list { margin-top: 0; }
@media (min-width: 900px) {
  .works-orbit { width: min(100%, 21rem); }
  .works-body { grid-template-columns: minmax(0, 21rem) minmax(0, 1fr); align-items: center; column-gap: clamp(var(--space-7), 6vw, var(--space-9)); }
  .works-body .works-list { grid-template-columns: minmax(0, 1fr); }
}
.works-orbit {
  --orbit-line: color-mix(in srgb, var(--color-ink) 13%, transparent);
  position: relative; width: min(100%, 16.5rem); aspect-ratio: 1; margin-inline: auto; container-type: size;
  overflow: visible;
}
.orbit-ring {
  position: absolute; left: 50%; top: 50%; width: calc(var(--r) * 2); height: calc(var(--r) * 2); margin: calc(var(--r) * -1) 0 0 calc(var(--r) * -1);
  border-radius: 50%; border: 1px solid var(--orbit-line); pointer-events: none;
}
.orbit-ring--a { --r: 29cqw; }
.orbit-ring--b { --r: 45cqw; }
.orbit-core {
  position: absolute; left: 50%; top: 50%; width: 24cqw; height: 24cqw; margin: -12cqw 0 0 -12cqw; border-radius: 22%; pointer-events: none;
  filter: drop-shadow(0 10px 22px rgb(12 20 28 / 0.18));
}
.orbit-sat {
  --s: clamp(2.5rem, 16cqw, 3.5rem);
  position: absolute; left: 50%; top: 50%; width: var(--s); height: var(--s); margin: calc(var(--s) / -2) 0 0 calc(var(--s) / -2);
  transform: rotate(calc(var(--a) * 1deg)) translateY(calc(var(--r) * -1)) rotate(calc(var(--a) * -1deg));
}
.orbit-sat img { display: block; width: 100%; height: 100%; border-radius: 22%; filter: drop-shadow(0 6px 14px rgb(12 20 28 / 0.16)); transition: transform var(--duration-hover, 200ms) var(--ease-out, ease-out); }
.orbit-sat--a { --r: 29cqw; --a: 300; --d: 30; }
.orbit-sat--b { --r: 45cqw; --a: 120; --d: 46; }
/* the motion is added by JS (so without it, and in reduced motion, the two sit still); it runs only while the figure is on screen */
.works-orbit.is-orbiting .orbit-sat { animation: mu-orbit calc(var(--d) * 1s) linear infinite; animation-play-state: paused; }
.works-orbit.is-orbiting.is-live .orbit-sat { animation-play-state: running; }
.works-orbit.is-orbiting .orbit-sat--b { animation-direction: reverse; }
@keyframes mu-orbit {
  from { transform: rotate(calc(var(--a) * 1deg)) translateY(calc(var(--r) * -1)) rotate(calc(var(--a) * -1deg)); }
  to   { transform: rotate(calc(var(--a) * 1deg + 360deg)) translateY(calc(var(--r) * -1)) rotate(calc(var(--a) * -1deg - 360deg)); }
}
@media (hover: hover) and (pointer: fine) {
  /* the planets keep turning under the pointer (they are slow enough to click); the matching row answers and the other way round */
  .orbit-sat:hover img { transform: scale(1.12); }
  .works:has(.works-item[href$="target/"]:hover) .orbit-sat[data-for="target"] img,
  .works:has(.works-item[href$="club/"]:hover) .orbit-sat[data-for="club"] img { transform: scale(1.12); }
  .works:has(.orbit-sat[data-for="target"]:hover) .works-item[href$="target/"] .title,
  .works:has(.orbit-sat[data-for="club"]:hover) .works-item[href$="club/"] .title { text-decoration: underline; text-decoration-thickness: var(--line-underline, 1px); text-underline-offset: 0.2em; }
}
@media (prefers-reduced-motion: reduce) { .orbit-sat img { transition: none; } }

/* ═══ mu-src/text.css ═══ */
/* Aimlink | Magic UI ports, group "text" (styles for assets/mu-src/text.js).
   BlurFade · TextReveal · HyperText · TextAnimate · Text3DFlip · DiaTextReveal · AnimatedShinyText.
   Rules kept: lime only as light on dark and as a fill on light, no will-change, no animated width/height/top/left,
   transform / opacity / filter only (and only on small text), reduced motion gets the finished state. */

/* ───────────── shared: a word, split out of a line (the text itself is never changed) ───────────── */
.mu-wd { }

/* ───────────── BlurFade: the site's own reveal (opacity + 16 px rise) also resolves from a soft blur, on text only ─────────────
   DESIGNDIRECTION 8.3/8.5: blur is allowed in reveals on small elements. Only headings, ledes and text blocks get it (text.js picks them);
   pictures, forms, figures and the 3D set-pieces keep the plain reveal. assets/motion.css switches the transitions off while GSAP drives
   opacity and transform, so there the filter keeps its own (the only thing CSS animates on these nodes). */
@media (prefers-reduced-motion: no-preference) {
  html.reveal-ready .mu-bf:not(.is-in):not(:focus-within) { filter: blur(var(--mu-bf-blur, 7px)); }
  html.reveal-ready .mu-bf {
    transition: opacity var(--duration-reveal) var(--ease-out), transform var(--duration-reveal) var(--ease-out), filter 700ms var(--ease-out);
    transition-delay: calc(var(--i, 0) * var(--motion-stagger));
  }
  html.reveal-ready.gsap-reveals .mu-bf { transition: filter 700ms var(--ease-out) calc(min(var(--i, 0), 3) * 60ms) !important; }
}
html.motion-static .mu-bf { filter: none !important; }

/* ───────────── TextReveal: a statement lights up word by word while it climbs the screen ─────────────
   Scroll-driven CSS (no scroll listener, runs on the compositor, scrubs both ways). The Manifest pattern of DESIGNDIRECTION 8.3: the words
   start at --mu-floor (0.6 keeps the moss accent words at 3.4:1 and ink at 5:1) and fill to 1. Browsers without scroll timelines keep the words at full strength. */
@keyframes mu-word-fill { from { opacity: var(--mu-floor, 0.5); } to { opacity: 1; } }
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .mu-tr { view-timeline: --mu-tr block; }
    .mu-tr .mu-wd {
      animation: mu-word-fill linear both;
      animation-timeline: --mu-tr;
      animation-range:
        cover calc(var(--mu-from, 8vh) + var(--mu-p, 0) * var(--mu-span, 30vh))
        cover calc(var(--mu-from, 8vh) + var(--mu-p, 0) * var(--mu-span, 30vh) + var(--mu-win, 9vh));
    }
  }
}

/* ───────────── HyperText: a small label decodes into its words when it arrives ─────────────
   The real text stays in the page (and stays the accessible name); while it decodes, a copy drawn by ::after stands in for it. */
@supports (content: "a" / "") {
  .mu-hx { position: relative; }
  .mu-hx.is-scr { -webkit-text-fill-color: transparent; }
  .mu-hx.is-scr::after {
    content: attr(data-mu-s) / ""; position: absolute; left: 0; top: 0; white-space: nowrap; pointer-events: none;
    -webkit-text-fill-color: currentcolor;
  }
}

/* ───────────── TextAnimate: a heading that nothing else animates comes in word by word, from a blur ───────────── */
.mu-ta .mu-wd { display: inline-block; }
@media (prefers-reduced-motion: no-preference) {
  .mu-ta.mu-armed .mu-wd {
    transition: opacity 640ms var(--ease-out), filter 640ms var(--ease-out), transform 640ms var(--ease-out);
    transition-delay: calc(var(--mu-i, 0) * 60ms);
  }
  .mu-ta.mu-armed:not(.mu-in) .mu-wd { opacity: 0; filter: blur(9px); transform: translateY(0.28em); }
  .mu-ta.mu-armed em { transition: background-color 640ms var(--ease-out); transition-delay: calc(var(--mu-i, 0) * 60ms); }
  .mu-ta.mu-armed:not(.mu-in) em { background-color: transparent; }
}

/* About's hero is the one hero without an intro: the same recipe as the others (word by word, once), with a blur, in pure CSS so
   nothing flashes before a script runs. The label, headline, line and link arrive one after the other. */
@keyframes mu-blur-up { from { opacity: 0; filter: blur(10px); transform: translateY(18px); } to { opacity: 1; filter: blur(0); transform: none; } }
@media (prefers-reduced-motion: no-preference) {
  .about-hero-copy > .label { animation: mu-blur-up 640ms var(--ease-out) 40ms both; }
  .about-hero-copy .h1 { animation: mu-blur-up 760ms var(--ease-out) 100ms both; }
  .about-hero-copy .h1 em { animation: mu-blur-up 760ms var(--ease-out) 260ms both; }
  .about-hero-copy .lede { animation: mu-blur-up 700ms var(--ease-out) 460ms both; }
  .about-hero-copy .link-arrow { animation: mu-blur-up 700ms var(--ease-out) 560ms both; }
}

/* ───────────── Text3DFlip: a footer link rolls over, letter by letter, on hover ─────────────
   Built over the link on the first pointer-enter only; the underline stays where it is (the real text just turns transparent for the roll). */
.mu-fl-host { position: relative; }
.mu-fl-host.is-rolling { color: transparent !important; }
.mu-fl { position: absolute; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; perspective: 360px; }
.mu-fc { position: absolute; transform-style: preserve-3d; }
.mu-ff { position: absolute; left: 0; top: 0; width: 100%; height: 100%; text-align: center; white-space: pre; -webkit-backface-visibility: hidden; backface-visibility: hidden; }

/* ───────────── DiaTextReveal and AnimatedShinyText: light travels along an accent word (the gradient position is animated from text.js) ─────────────
   Dia (the footer's closing word): a band of light writes the word out, lime behind it, nothing ahead. Shine (hero accents): the word is
   lime throughout and one white glint crosses it. Both only exist while they run; at rest the word is plain text. */
@property --mu-g { syntax: '<percentage>'; inherits: false; initial-value: -20%; }
.mu-dia {
  background-image: linear-gradient(90deg, currentcolor 0%, currentcolor calc(var(--mu-g) - 15%), var(--color-mint) calc(var(--mu-g) - 6%), var(--color-white) var(--mu-g), var(--color-mint) calc(var(--mu-g) + 6%), transparent calc(var(--mu-g) + 15%), transparent 100%);
  background-repeat: no-repeat; background-size: 100% 100%; -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
}
.mu-shine {
  background-image: linear-gradient(105deg, currentcolor calc(var(--mu-g) - 14%), var(--color-white) var(--mu-g), currentcolor calc(var(--mu-g) + 14%));
  background-repeat: no-repeat; background-size: 100% 100%; -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;
}

/* the hero glint (text.js glint): white copies of the accent word, cut into slices, over the word. The heading is positioned from the first paint
   (changing the word's own box later would repaint it), the copies are generated content and carry no opacity until they run. */
.go-hero .h1, .np-hero .h1, .cp-hero .h1, .pv-hero .h1, .fq-hero .h1, .xp-hero .h1, .pr-hero .h1 { position: relative; }
.mu-gl { position: absolute; box-sizing: border-box; pointer-events: none; }
.mu-gl-s {
  position: absolute; inset: 0; padding: inherit; white-space: nowrap; opacity: 0; color: var(--color-white); -webkit-text-fill-color: var(--color-white);
  -webkit-user-select: none; user-select: none; pointer-events: none;
}
.mu-gl-s::before { content: attr(data-t); }

/* ───────────── "Fresh off the range": TextAnimate by character (the news heading, text.js textFresh) ─────────────
   Each letter lands from below out of a blur, a hair too big, and settles with a little overshoot; the letters follow each other
   34 ms apart. The classes are removed once it has played, so the heading at rest is plain text. */
@media (prefers-reduced-motion: no-preference) {
  .mu-fresh .mu-ch {
    opacity: 0; filter: blur(12px); transform: translateY(0.62em) scale(1.14); transform-origin: 50% 100%;
    transition: opacity 520ms var(--ease-out), filter 640ms var(--ease-out), transform 820ms cubic-bezier(0.2, 1.4, 0.4, 1);
    transition-delay: calc(var(--mu-n, 0) * 34ms);
  }
  .mu-fresh.mu-in .mu-ch { opacity: 1; filter: blur(0); transform: none; }
}

/* ═══ mu-src/ui.css ═══ */
/* Aimlink | Magic UI ports, group "ui" (styles for assets/mu-src/ui.js). Transform, opacity, clip-path and background-position only;
   no will-change, nothing that moves the layout; reduced motion gets the finished state. Lime only on dark. */

/* ───────────── Glint (ShimmerButton + ShinyButton): one sheen across a filled button ───────────── */
/* the glint's colour follows the button's fill: lime light across the ink pill (light pages), white light across the lime pill (dark) */
:root, [data-theme="light"], [data-theme="mist"] { --mu-glint: rgb(208 252 100 / 0.5); }
[data-theme="dark"] { --mu-glint: rgb(255 255 255 / 0.5); }
[data-mu-glint] { position: relative; }
[data-mu-glint]::after {
  content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; opacity: 0;
  background: linear-gradient(105deg, transparent 37%, var(--mu-glint) 50%, transparent 63%) 112% 0 / 250% 100% no-repeat;
}
@media (hover: hover) and (pointer: fine) {
  [data-mu-glint="btn"]:not(:disabled):not([aria-disabled="true"]):hover::after,
  [data-mu-glint="btn"]:not(:disabled):not([aria-disabled="true"]):focus-visible::after,
  .site-header.is-scrolled [data-mu-glint="nav"]:hover::after, .site-header.is-open [data-mu-glint="nav"]:hover::after {
    opacity: 1; background-position: -12% 0; transition: background-position 760ms cubic-bezier(0.3, 0.55, 0.3, 1);
  }
}
.mu-glint-go::after { opacity: 1; animation: mu-glint 900ms cubic-bezier(0.3, 0.55, 0.3, 1) 360ms both; }
@keyframes mu-glint { from { background-position: 112% 0; } to { background-position: -12% 0; } }

/* ───────────── FillHover (InteractiveHoverButton): the outlined button fills from where the pointer came in ───────────── */
[data-mu-fill] { position: relative; }
.mu-fill {
  position: absolute; inset: 0; z-index: 1; display: inline-flex; align-items: center; justify-content: center; gap: inherit; padding: inherit;
  border-radius: inherit; background: var(--action-bg); color: var(--action-fg); pointer-events: none; user-select: none; -webkit-user-select: none;
  clip-path: circle(0px at var(--mu-x, 28px) var(--mu-y, 50%));
  transition: clip-path 300ms var(--ease-out);
}
[data-mu-fill].mu-in .mu-fill { clip-path: circle(165% at var(--mu-x, 28px) var(--mu-y, 50%)); transition-duration: 520ms; }
[data-mu-still] .mu-fill { transition: none; }

/* ───────────── ScrollProgress: a hairline at the very top of long pages ───────────── */
.mu-sp {
  position: fixed; inset: 0 0 auto 0; z-index: calc(var(--z-nav) + 1); height: 2px; pointer-events: none; opacity: 0;
  transition: opacity var(--duration-exit) var(--ease-out);
}
.mu-sp.is-on { opacity: 1; }
.mu-sp i {
  position: absolute; inset: 0; background: var(--color-lime); transform: scaleX(0); transform-origin: 0 50%;
  transition: background-color var(--duration-nav) var(--ease-hover);
}
.mu-sp[data-on]:not([data-on="dark"]) i { background: var(--color-ink); }

@media (prefers-reduced-motion: reduce) {
  .mu-glint-go::after { animation: none; opacity: 0; }
  .mu-fill { transition: none; }
  .mu-sp { display: none; }
}
