/* ── xol motion layer ─────────────────────────────────────────────────────────
   Shared by launcher.xol.dev and xol.dev (xol.dev serves a copy of /assets). A port of hwic.dev's motion.css:
   the same intro keyframes and timings, the same vocabulary, written against the xol tokens. Linked AFTER the
   site's own stylesheet so a tie in specificity goes to this file. Rules it keeps:
     · every colour is a token; a fade ends in the SAME colour at alpha 0, never the `transparent` keyword
       (WebKit interpolates that through grey on light presets);
     · only transform / opacity animate; no box-shadow animates on a backdrop-filter box (the sticky nav);
     · everything fails VISIBLE: the curtain exists only once intro.js has put `xi` on <html>, and it takes
       itself down with a CSS-only timer even if every later script dies; reduced motion switches all of it off.

   Vocabulary:
     #intro .glitch   the intro curtain (html.xi) and its wordmark; .mo-fly = flight to the brand, .out = gone
     .spot            pointer spotlight on cards (fine pointers only; --mx/--my from motion.js)
     .magnet          magnetic pull on primary buttons (fine pointers only; --mx-x/--mx-y)
     html.mo-leave    page-leave fade between the site's own pages
     details.mo-closing  FAQ row while it closes (motion.js animates the height and owns the open state)
     #ambient.mo-live the ambient canvas once it is painting (html[data-ambient])                       */

:root {
  --ease-out: cubic-bezier(.16, 1, .3, 1);
  --ease-spring: cubic-bezier(.34, 1.45, .44, 1);
  --mo-ease: cubic-bezier(.2, .9, .25, 1);
}

/* ── intro curtain ────────────────────────────────────────────────────────
   The markup carries [hidden]; only `html.xi` (set by the tiny synchronous /assets/intro.js, once per browser
   session, never in the admin preview) shows it. No script, no class, no curtain.                       */
#intro { display: none; }
html.xi #intro {
  display: grid; place-items: center; position: fixed; top: 0; right: 0; bottom: 0; left: 0; z-index: 200;
  background: var(--bg); transition: opacity .55s ease .15s;
  /* CSS-only bail-out: whatever happens to the scripts, the curtain is gone after 4.5 s */
  animation: xi-bail .01s linear 4.5s forwards;
}
@keyframes xi-bail { to { opacity: 0; visibility: hidden; pointer-events: none; } }
html.xi #intro.out { opacity: 0; pointer-events: none; }

.glitch {
  position: relative; font-family: var(--font, "Geist", system-ui, sans-serif); font-weight: 700; font-size: clamp(30px, 7vw, 54px);
  letter-spacing: -.02em; color: var(--text); line-height: 1; white-space: nowrap; opacity: 0; transform: translateY(4px);
  animation: mo-blur-in .55s var(--mo-ease) forwards, jit .32s steps(2) infinite .55s, flick 4s linear infinite .55s;
  will-change: transform; transition: transform .72s var(--ease-out), opacity .72s ease, font-weight .72s var(--ease-out), letter-spacing .72s var(--ease-out);
}
.glitch .a { color: var(--accent); }
.glitch::before, .glitch::after {
  content: attr(data-text); position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  background: var(--bg); overflow: hidden; pointer-events: none; opacity: .9;
}
.glitch::before { left: 2px; text-shadow: -2px 0 var(--accent); animation: g1 2.6s steps(9) infinite .55s; }
.glitch::after { left: -2px; text-shadow: 1px 0 rgba(255, 59, 107, .7); animation: g2 3.1s steps(11) infinite .55s; }
@keyframes enter { to { opacity: 1; transform: none; } }
@keyframes mo-blur-in { from { opacity: 0; transform: translate3d(0, 6px, 0); filter: blur(6px); } to { opacity: 1; transform: none; filter: blur(0); } }
@keyframes jit { 0% { transform: translate(0, 0); } 50% { transform: translate(-.5px, .5px); } 100% { transform: translate(.5px, -.5px); } }
@keyframes flick { 0%, 90%, 100% { opacity: 1; } 92% { opacity: .78; } 96% { opacity: .97; } }
@keyframes g1 {
  0% { -webkit-clip-path: inset(8% 0 86% 0); clip-path: inset(8% 0 86% 0); } 20% { -webkit-clip-path: inset(48% 0 33% 0); clip-path: inset(48% 0 33% 0); }
  40% { -webkit-clip-path: inset(14% 0 72% 0); clip-path: inset(14% 0 72% 0); } 60% { -webkit-clip-path: inset(82% 0 4% 0); clip-path: inset(82% 0 4% 0); }
  80% { -webkit-clip-path: inset(38% 0 47% 0); clip-path: inset(38% 0 47% 0); } 100% { -webkit-clip-path: inset(23% 0 62% 0); clip-path: inset(23% 0 62% 0); }
}
@keyframes g2 {
  0% { -webkit-clip-path: inset(74% 0 12% 0); clip-path: inset(74% 0 12% 0); } 20% { -webkit-clip-path: inset(20% 0 68% 0); clip-path: inset(20% 0 68% 0); }
  40% { -webkit-clip-path: inset(55% 0 30% 0); clip-path: inset(55% 0 30% 0); } 60% { -webkit-clip-path: inset(5% 0 88% 0); clip-path: inset(5% 0 88% 0); }
  80% { -webkit-clip-path: inset(68% 0 18% 0); clip-path: inset(68% 0 18% 0); } 100% { -webkit-clip-path: inset(30% 0 55% 0); clip-path: inset(30% 0 55% 0); }
}
.scan {
  position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; opacity: .4;
  background: repeating-linear-gradient(0deg, rgba(0, 0, 0, 0) 0 2px, rgba(0, 0, 0, .35) 2px 3px);
}
/* the flight (FLIP, set by motion.js): the glitch slices stop, the curtain's colour goes to ITS OWN colour at alpha 0,
   and the page underneath is already clickable */
html.xi #intro.mo-fly, html.xi #intro.out { pointer-events: none; }
#intro.mo-fly .glitch, #intro.out .glitch { animation: none !important; opacity: 1; }
#intro.mo-fly .glitch::before, #intro.mo-fly .glitch::after, #intro.mo-fly .scan,
#intro.out .glitch::before, #intro.out .glitch::after, #intro.out .scan { display: none; }
html.xi #intro.mo-fly { background: rgb(var(--bg-rgb, 11 12 15) / 0); transition: background-color .45s ease, opacity .55s ease .15s; }
/* The text the wordmark lands on ([data-intro-target]: "Xol" in the header, the big name on xol.dev) stays out of sight until
   the wordmark has become it. CSS-only and fail-visible: the hold is an animation that ends by itself after 4.5 s, and
   motion.js removes `xi` (or adds `xi-fade` for the plain fade) long before that. */
html.xi:not(.xi-fade) [data-intro-target] { animation: xi-target 4.5s steps(1, end); }
@keyframes xi-target { from { opacity: 0; } to { opacity: 1; } }
/* a part of the wordmark the landing spot does not have (".dev" on xol.dev) lets go during the flight */
.glitch .drop { transition: opacity .3s ease; }
#intro.mo-fly .glitch .drop { opacity: 0; }
@media print { html.xi #intro { display: none !important; } }

/* ── ambient canvas (html[data-ambient] = an animated mode; motion.js creates the canvas) ─────────────────
   Sized to the LARGE viewport: on iOS the toolbars collapse while you scroll, and a 100% box would change
   height every few frames. Decorative: behind everything, no pointer events, no layout.              */
#ambient {
  position: fixed; top: 0; left: 0; width: 100%; height: 100vh; height: 100lvh; z-index: -1; opacity: 0; transition: opacity 1.4s ease;
  pointer-events: none; transform: translateZ(0); -webkit-backface-visibility: hidden; backface-visibility: hidden; will-change: transform;
}
#ambient.mo-live { opacity: 1; }
/* <body> becomes a stacking context so the canvas (z-index -1) paints above the page colour and below the content */
html[data-ambient] body { isolation: isolate; }
/* the sites' own still glow + grid belong to the default ambient only */
html[data-ambient] .hero::before, html[data-ambient] .hero::after, html[data-ambient] body::before, html[data-ambient] body::after { display: none; }

/* ── pointer spotlight on cards ───────────────────────────────────────────
   isolation gives the card its own stacking context, so the z-index:-1 overlay sits above the card's
   background and below every child.                                                               */
.spot { position: relative; isolation: isolate; }
.spot::after {
  content: ""; position: absolute; top: 0; right: 0; bottom: 0; left: 0; z-index: -1; border-radius: inherit; pointer-events: none; opacity: 0;
  background: radial-gradient(260px circle at var(--mx, 50%) var(--my, 50%), rgb(var(--accent-rgb, 94 224 181) / .12), rgb(var(--accent-rgb, 94 224 181) / 0) 62%);
  transition: opacity .45s var(--mo-ease);
}
@media (hover: hover) and (pointer: fine) { .spot:hover::after { opacity: 1; } }

/* ── magnetic primary buttons (fine pointers only; motion.js sets the vars) ───────────────────────── */
.magnet {
  transform: translate3d(var(--mx-x, 0px), var(--mx-y, 0px), 0) scale(var(--press, 1));
  transition: transform .28s var(--ease-spring), background-color 140ms var(--ease, ease), border-color 140ms var(--ease, ease), box-shadow .32s var(--mo-ease);
}
/* the sites' own hover nudges the button up 1px; the pull replaces that */
.btn.magnet:hover, li a.magnet:hover { transform: translate3d(var(--mx-x, 0px), var(--mx-y, 0px), 0) scale(var(--press, 1)); }
.btn.magnet:active, li a.magnet:active { --press: .96; transform: translate3d(var(--mx-x, 0px), var(--mx-y, 0px), 0) scale(.96); }
.magnet.mo-pulling { transition-duration: .12s, 140ms, 140ms, .32s; }
/* the glow is for buttons in the page, not the one inside the frosted header (no animated shadow near a backdrop-filter) */
@media (hover: hover) { main .magnet:hover { box-shadow: 0 10px 26px -12px rgb(var(--accent-rgb, 94 224 181) / .28), 0 0 0 4px rgb(var(--accent-rgb, 94 224 181) / .12); } }

/* ── brand mark hover ─────────────────────────────────────────────────── */
.brand svg, svg.mark { transition: transform .5s var(--ease-spring); }
@media (hover: hover) { .brand:hover svg, svg.mark:hover { transform: rotate(-8deg) scale(1.06); } }

/* ── growing underline on text links ──────────────────────────────────── */
.foot nav a, .faq p a, .prose a:not(.btn), .sub a {
  text-decoration: none !important; background-image: linear-gradient(currentColor, currentColor);
  background-repeat: no-repeat; background-position: 0 100%; background-size: 0 1px;
  transition: background-size .32s var(--ease-out), color .32s var(--mo-ease);
}
@media (hover: hover) { .foot nav a:hover, .faq p a:hover, .prose a:not(.btn):hover, .sub a:hover { background-size: 100% 1px; } }

/* ── page-leave fade between the site's own pages ─────────────────────── */
main, .foot, body > footer { transition: opacity .18s ease; }
html.mo-leave main, html.mo-leave .foot, html.mo-leave body > footer { opacity: 0 !important; }

/* ── details / FAQ ────────────────────────────────────────────────────────
   The <summary> keeps its native display. motion.js cancels the default toggle, owns `open`, and animates the
   row's height; the answer eases in. While a row closes the chevron already points down again.        */
.faq summary::after { transition: transform .35s var(--ease-spring), border-color .32s var(--mo-ease); }
.faq details.mo-closing summary::after { transform: translateY(-70%) rotate(45deg); }
@media (prefers-reduced-motion: no-preference) {
  details[open] > *:not(summary) { animation: mo-fade .38s var(--ease-out) both; }
}
@keyframes mo-fade { from { opacity: 0; transform: translate3d(0, -4px, 0); } to { opacity: 1; transform: none; } }

/* ── no rubber-band at the top on desktop (a trackpad flick dragged the sticky bar down) ── */
@media (hover: hover) and (pointer: fine) { html { overscroll-behavior-y: none; } }

/* ── reduced motion: a short plain fade for the curtain, everything else off ───────────── */
@media (prefers-reduced-motion: reduce) {
  #ambient { display: none !important; }
  .glitch { animation: enter .4s ease forwards !important; transform: none !important; }
  .glitch::before, .glitch::after, .scan { display: none !important; }
  html.xi #intro, #intro .glitch { transition: opacity .2s ease !important; }
  #intro.out .glitch { animation: none !important; opacity: 0; }
  .magnet, .spot::after, .brand svg, svg.mark, .faq summary::after, .foot nav a, .faq p a, .prose a:not(.btn), .sub a, main, .foot, body > footer { transition: none !important; animation: none !important; }
  .magnet, .btn.magnet:hover, li a.magnet:hover { transform: none !important; }
  html.mo-leave main, html.mo-leave .foot, html.mo-leave body > footer { opacity: 1 !important; }
}
