/*
 * TIMELESS — visual-redesign effects layer
 * =========================================================================
 * Hand-written reimplementations of the Aceternity/godly.design/10x.app
 * "showpiece" effects (spotlight-follow, aurora, tilt/glare, gradient
 * borders, moving-border buttons, tracing beam, noise/grid texture, lens
 * zoom) in plain CSS + the vendored anime.js v4 (js/effects.js). Nothing
 * here is imported from those sites — no React, no Framer Motion, no
 * Tailwind — every effect is rebuilt from scratch against this site's own
 * token layer (css/tokens.css).
 *
 * THREE RULES EVERYTHING BELOW FOLLOWS, no exceptions:
 *   1. Animation is transform/opacity ONLY. Nothing here ever animates
 *      width/height/top/left/box-shadow/filter frame-by-frame — the
 *      perf pass this build is built on depends on that discipline.
 *   2. Every keyframe animation is written as a plain CSS `animation`, so
 *      the global `prefers-reduced-motion` rule in css/base.css (which
 *      forces `animation-duration: 0.001ms`) suppresses it automatically —
 *      nothing here needs its own reduced-motion override to be safe.
 *   3. Pointer-driven effects (spotlight, tilt, glare) are gated behind
 *      `(hover: hover) and (pointer: fine)` at the CSS level (the custom
 *      properties they read do nothing without the surrounding rule) AND
 *      js/effects.js checks the same media query before it ever attaches a
 *      pointermove listener — a touch device pays for neither the paint
 *      nor the JS.
 */

/* ================================================================ texture
   A faint dot-grid over the ambient gradient (css/base.css's .bg-ambient) —
   the restrained "textured ground" every reference site uses instead of a
   flat colour. Static: no motion, so no reduced-motion concern. Masked to
   fade out toward the edges so it reads as texture behind content, not a
   busy tiled pattern covering the whole viewport uniformly. */
.bg-grid {
  position: fixed; inset: 0; z-index: -1; pointer-events: none;
  background-image: radial-gradient(circle, color-mix(in srgb, var(--tl-text) 55%, transparent) 1px, transparent 1px);
  background-size: var(--tl-grid-size) var(--tl-grid-size);
  opacity: var(--tl-grid-opacity);
  -webkit-mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 0%, transparent 75%);
  mask-image: radial-gradient(ellipse 80% 60% at 50% 0%, #000 0%, transparent 75%);
}
.bg-noise {
  position: fixed; inset: 0; z-index: -1; pointer-events: none;
  opacity: var(--tl-noise-opacity); mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ============================================================ spotlight ==
   A radial glow that follows the pointer, used on the hero and the bento
   grid. js/effects.js sets --spot-x/--spot-y (percentages) on pointermove
   and toggles .tl-spotlight-active; both are no-ops without the surrounding
   (hover:hover) query below, so a touch device never even sees the glow
   appear (and js/effects.js never attaches the listener there either). */
/*
 * .tl-spotlight-host `isolation: isolate` gives it its own local stacking
 * context, so the z-index values below (0 for the glow, 1 for real content)
 * only ever compete with EACH OTHER — they can't leak out and fight the
 * fixed header (z-index 100) or the lightbox (200) elsewhere on the page.
 * The glow itself is appended by js/effects.js as this element's LAST
 * child, so without an explicit z-index order it would paint on top of the
 * hero text purely by being later in the DOM — z-index: 0 here plus
 * z-index: 1 on the actual content (.hero-inner, .bento-tile) keeps the
 * glow strictly behind whatever it's illuminating, however it wanders.
 */
.tl-spotlight-host { position: relative; isolation: isolate; }
.tl-spotlight-host > .hero-inner,
.tl-spotlight-host > .bento-tile { position: relative; z-index: 1; }
.tl-spotlight {
  position: absolute; inset: 0; z-index: 0; pointer-events: none; border-radius: inherit;
  opacity: 0; transition: opacity var(--tl-dur-4) var(--tl-ease);
  background: radial-gradient(var(--tl-spotlight-size) circle at var(--spot-x, 50%) var(--spot-y, 0%),
    color-mix(in srgb, var(--tl-light-blue) 65%, transparent) 0%, transparent 70%);
}
@media (hover: hover) and (pointer: fine) {
  .tl-spotlight-host.is-spotlighting > .tl-spotlight { opacity: var(--tl-spotlight-opacity); }
}

/* ================================================================ aurora ==
   Two large, blurred, colour-shifting blobs behind the hero copy — pure
   CSS, transform-only motion (a slow drift + scale), so it's exactly as
   cheap as the rest of this site's entrance animation and fully stilled by
   prefers-reduced-motion via the global rule in base.css. filter:blur is
   applied ONCE as a static property, never animated per-frame. */
.hero-aurora {
  position: absolute; inset: -30% -15% auto -15%; height: 620px; z-index: -1;
  overflow: hidden; pointer-events: none;
}
.hero-aurora::before, .hero-aurora::after {
  content: ""; position: absolute; width: 62%; padding-top: 62%; border-radius: 50%;
  filter: blur(70px); mix-blend-mode: screen; opacity: 0.55; will-change: transform;
}
.hero-aurora::before {
  left: -8%; top: -6%;
  background: radial-gradient(circle, var(--tl-light-blue), transparent 72%);
  animation: tl-aurora-a 20s ease-in-out infinite alternate;
}
.hero-aurora::after {
  right: -12%; top: 8%;
  background: radial-gradient(circle, var(--tl-turquoise), transparent 72%);
  animation: tl-aurora-b 26s ease-in-out infinite alternate;
  animation-delay: -8s;
}
:root[data-theme="light"] .hero-aurora::before,
:root[data-theme="light"] .hero-aurora::after { opacity: 0.35; }
@keyframes tl-aurora-a { from { transform: translate3d(0, 0, 0) scale(1); } to { transform: translate3d(5%, 8%, 0) scale(1.12); } }
@keyframes tl-aurora-b { from { transform: translate3d(0, 0, 0) scale(1.05); } to { transform: translate3d(-6%, -5%, 0) scale(0.96); } }

/* ============================================================ bento grid ==
   Home's stat tiles, replacing the flat 4-up grid with an asymmetric
   layout: large tiles (countdown, net position, next gate) get 2x the
   footprint of the small ones. grid-auto-flow: dense lets the small tiles
   fill in around whichever order the large ones land in. */
.bento-grid {
  display: grid; grid-template-columns: repeat(6, 1fr);
  grid-auto-flow: dense; grid-auto-rows: minmax(128px, auto);
  gap: var(--tl-sp-4);
}
.bento-tile {
  grid-column: span 2;
  background: var(--tl-bg-raised); border: 1px solid var(--tl-border); border-radius: var(--tl-radius-lg);
  padding: var(--tl-sp-5); box-shadow: var(--tl-shadow-sm);
  display: flex; flex-direction: column; justify-content: center;
  transition: transform var(--tl-dur-3) var(--tl-ease), border-color var(--tl-dur-3) var(--tl-ease), background var(--tl-dur-3) var(--tl-ease);
}
.bento-tile:hover { transform: translateY(-3px); border-color: var(--tl-border-strong); background: var(--tl-bg-raised-2); }
/*
 * .stat-tile-warn (components.css) sets its own background/border-color at
 * the same specificity (one class) as .bento-tile's above — and this file
 * loads AFTER components.css, so without this compound rule .bento-tile's
 * plain background would always win over the warning tint, silently
 * un-doing the "tasks overdue" highlight the Home page still relies on.
 */
.bento-tile.stat-tile-warn { background: var(--tl-warn-bg); border-color: color-mix(in srgb, var(--tl-warn) 45%, transparent); }
.bento-tile.stat-tile-warn:hover { background: var(--tl-warn-bg); }
.bento-lg {
  grid-column: span 3; grid-row: span 2;
  padding: var(--tl-sp-6); box-shadow: var(--tl-shadow-md); position: relative; overflow: hidden;
}
/* fs-h2, not the full display scale — a big tile can hold a short PHRASE
   ("Zain's answer", "Free entry or £15") as easily as a number, and the
   display scale (up to 5.5rem) was sized for a handful of digits, not text. */
.bento-lg .stat-value { font-size: var(--tl-fs-h2); line-height: var(--tl-lh-heading); }
.bento-lg .stat-label { font-size: var(--tl-fs-lg); }
.bento-lg-icon { font-size: 1.5rem; margin-bottom: var(--tl-sp-3); opacity: 0.8; }
@media (max-width: 900px) {
  .bento-grid { grid-template-columns: repeat(4, 1fr); }
  .bento-lg { grid-column: span 4; grid-row: span 1; }
  .bento-tile { grid-column: span 2; }
}
@media (max-width: 560px) {
  .bento-grid { grid-template-columns: repeat(2, 1fr); }
  .bento-lg, .bento-tile { grid-column: span 2; }
}

/* ===================================================== gradient borders ==
   A slow-rotating conic-gradient ring, one pixel wide, sitting exactly on
   an element's own border — the ::before paints a larger gradient square
   BEHIND the element's content but ABOVE its background (negative z-index
   painting order), and ::after re-covers everything but the outer ring
   with the element's own flat surface colour, leaving only the ring
   visible. Rotation is transform-only (the whole gradient square spins);
   nothing about the element's box ever changes size.
   Requires the host to already set its own background to --tl-bg-raised
   (true of .card / .cost-pool / everything this is applied to here). */
/*
 * Registered so the browser treats --tl-angle as an interpolatable <angle>
 * (letting it animate smoothly frame to frame) rather than an opaque
 * string. Without this @property, the fallback further down keeps the ring
 * visible — just not spinning — on a browser that doesn't support it.
 */
@property --tl-angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}
.gradient-border { position: relative; z-index: 0; isolation: isolate; }
/*
 * The ring rotates by animating the CONIC GRADIENT'S OWN START ANGLE
 * (`from var(--tl-angle)`), never by transforming the pseudo-element
 * itself. A card is rarely square (the Cost pool cards are roughly
 * 1:1.5) — an actual `transform: rotate()` on a non-square rectangle
 * swings its corners far outside the card's own footprint at most angles
 * (this shipped broken that way during development: a spinning rectangle,
 * not a square, throws its corners well past the card at ~45°/135°/etc,
 * showing as a stray blue trapezoid poking out beside the card). Animating
 * the gradient's angle instead repaints in place — the pseudo-element's own
 * box never moves or rotates, so there is nothing to bleed outside it
 * regardless of the card's aspect ratio.
 */
.gradient-border::before {
  content: ""; position: absolute; inset: calc(-1 * var(--tl-border-glow-width)); z-index: -2;
  border-radius: inherit;
  background: conic-gradient(from var(--tl-angle), var(--tl-light-blue), var(--tl-turquoise), var(--tl-deep-blue), var(--tl-light-blue));
  opacity: 0.6;
  animation: tl-spin-angle var(--tl-dur-spin-1) linear infinite;
}
.gradient-border::after {
  content: ""; position: absolute; inset: 0; z-index: -1; border-radius: inherit;
  background: var(--tl-bg-raised);
}
@keyframes tl-spin-angle { to { --tl-angle: 360deg; } }
@keyframes tl-spin { to { transform: rotate(360deg); } }

/* ===================================================== moving-border btn ==
   Same technique, but idle by default (a plain solid button — the site's
   primary-button colour never changes at rest) and only spins up on
   hover/focus, so it reads as a deliberate flourish rather than a
   constantly-busy control. The rotating layer is a large square clipped by
   the button's own overflow:hidden, so a slow rotation reads as a moving
   glint travelling around the edge rather than a spinning disc. */
.btn-primary {
  position: relative; z-index: 0; overflow: hidden; isolation: isolate;
  border-color: transparent;
}
.btn-primary::before {
  content: ""; position: absolute; inset: -160%; z-index: -2;
  background: conic-gradient(var(--tl-turquoise), var(--tl-light-blue), var(--tl-deep-blue), var(--tl-turquoise));
  opacity: 0; transition: opacity var(--tl-dur-3) var(--tl-ease);
}
.btn-primary::after {
  content: ""; position: absolute; inset: 1.5px; z-index: -1; border-radius: inherit;
  background: var(--tl-accent);
}
@media (hover: hover) and (pointer: fine) {
  .btn-primary:hover::before, .btn-primary:focus-visible::before {
    opacity: 1; animation: tl-spin var(--tl-dur-spin-2) linear infinite;
  }
}
.btn-primary:focus-visible::before { opacity: 1; }

/* ==================================================== tilt / glare cards ==
   js/effects.js writes --tilt-x/--tilt-y/--glare-x/--glare-y on the card
   itself as the pointer moves across it, and toggles .is-tilting. The
   transform/opacity rules that actually respond to those variables live
   behind the same (hover:hover) and (pointer:fine) query as the spotlight —
   on a touch device the variables may exist but nothing reads them. */
.tilt-card { position: relative; }
.tilt-card__glare {
  position: absolute; inset: 0; z-index: 2; border-radius: inherit; pointer-events: none;
  opacity: 0; transition: opacity var(--tl-dur-3) var(--tl-ease);
  background: radial-gradient(220px circle at var(--glare-x, 50%) var(--glare-y, 50%),
    rgba(255, 255, 255, var(--tl-glare-opacity)), transparent 65%);
}
@media (hover: hover) and (pointer: fine) {
  .tilt-card { transform: perspective(900px) rotateX(var(--tilt-x, 0deg)) rotateY(var(--tilt-y, 0deg)); transition: transform var(--tl-dur-2) var(--tl-ease); }
  .tilt-card.is-tilting { transition: transform 60ms linear; }
  .tilt-card.is-tilting .tilt-card__glare { opacity: 1; }
}

/* ===================================================== tier ladder stack ==
   Partners' tier tally, restyled from a loose row of badges into a ranked,
   gradient-bordered stack — the tier ladder as a physical object rather
   than a caption. */
.tier-stack { display: flex; flex-direction: column; gap: var(--tl-sp-2); margin-bottom: var(--tl-sp-6); max-width: 420px; }
.tier-stack-item {
  display: flex; align-items: center; justify-content: space-between; gap: var(--tl-sp-3);
  padding: var(--tl-sp-3) var(--tl-sp-4); border-radius: var(--tl-radius-md);
  background: var(--tl-bg-raised); font-size: var(--tl-fs-sm);
}
.tier-stack-item.is-unassigned { opacity: 0.65; }
.tier-stack-name { font-weight: var(--tl-fw-semibold); color: var(--tl-text); }
.tier-stack-count { font-variant-numeric: tabular-nums; color: var(--tl-text-dim); font-weight: var(--tl-fw-bold); }

/* ============================================================ tick money ==
   The brief number-counting effect on Cost's headline tiles. Purely a text
   overlay during the tween (js/motion.js's tickMoney) — the final string is
   always identical to what would have been written with no animation at
   all, so nothing here can ever leave a stale or wrong figure on screen. */
.tl-tick { font-variant-numeric: tabular-nums; }

/* ============================================================ tracing beam
   Timeline's month rail. Replaces the flat `border-left: 2px solid` on
   .timeline-list with a real SVG line so it can be drawn in with scroll —
   an ordinary border can't be partially drawn. pathLength="1" (set in the
   markup) means stroke-dasharray/-dashoffset below are fractions of the
   line's length regardless of the list's actual rendered height, so this
   keeps working whether a month has two rows or twelve. */
.timeline-list { list-style: none; position: relative; padding-left: 0; border-left: none; }
.timeline-rail { position: absolute; left: 0; top: 0; bottom: 0; width: 2px; pointer-events: none; }
.timeline-rail-svg { width: 100%; height: 100%; display: block; overflow: visible; }
.timeline-rail-line {
  stroke: var(--tl-border-strong); stroke-width: 2; fill: none;
  stroke-dasharray: 1; stroke-dashoffset: 1;
}
/* No JS / anime unavailable / reduced motion: show the rail fully drawn
   rather than leave it looking broken or invisible — motion is additive
   here, never load-bearing for whether the rail is visible at all. */
.timeline-rail-line.is-static { stroke-dashoffset: 0; }

/* ======================================================== venue carousel ==
   Momentum, peek and a proper "opening" feel — the scroll-snap track itself
   (already correct) is untouched; this only adds a partial peek of the next
   card and a softer edge fade so the strip reads as continuing off-screen. */
.venue-carousel { position: relative; }
.venue-track {
  scroll-snap-stop: always;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--tl-sp-2), #000 calc(100% - var(--tl-sp-2)), transparent 100%);
  mask-image: linear-gradient(to right, transparent 0, #000 var(--tl-sp-2), #000 calc(100% - var(--tl-sp-2)), transparent 100%);
}
.venue-shot {
  transition: transform var(--tl-dur-3) var(--tl-ease), border-color var(--tl-dur-3) var(--tl-ease);
}
@media (hover: hover) and (pointer: fine) {
  .venue-shot:hover { transform: scale(1.03); }
}

/* ============================================================ lens zoom ==
   Lightbox: a small circular loupe follows the pointer over the enlarged
   photo, showing a magnified crop of the SAME <img> via background-image +
   background-position (never re-decoding or resizing anything). Only ever
   bound under (hover:hover) and (pointer:fine) — a touch visitor gets the
   plain, already-correct pinch-zoomable lightbox image with no loupe.
*/
.lightbox-stage { position: relative; display: inline-flex; max-width: 100%; max-height: 100%; }
.lightbox-lens {
  position: absolute; width: 220px; height: 220px; border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, 0.85); box-shadow: var(--tl-shadow-lg);
  pointer-events: none; opacity: 0; transition: opacity var(--tl-dur-2) var(--tl-ease);
  background-repeat: no-repeat;
  transform: translate(-50%, -50%);
}
.lightbox-stage.is-lensing .lightbox-lens { opacity: 1; }
@media (hover: none), (pointer: coarse) {
  .lightbox-lens { display: none; }
}

/* ================================================================ elevation
   "Real elevation using the existing shadow tokens" — a static resting
   shadow per surface, never itself transitioned/animated (animating
   box-shadow is exactly what the perf pass this build sits on forbids). A
   hover LIFT is a transform-only translateY paired with that already-static
   shadow, which reads as the surface rising without the shadow itself ever
   being interpolated frame to frame. Sponsor cards are excluded here
   deliberately — .tilt-card already owns their transform (rotateX/rotateY),
   and a competing translateY hover rule would fight it for the same
   property. */
.card, .stat-tile, .venue-card, .nav-tile, .cost-pool, .sponsor-card, .tier-stack-item {
  box-shadow: var(--tl-shadow-sm);
}
.venue-card, .nav-tile {
  transition: transform var(--tl-dur-3) var(--tl-ease), border-color var(--tl-dur-3) var(--tl-ease), background var(--tl-dur-3) var(--tl-ease);
}
@media (hover: hover) and (pointer: fine) {
  .venue-card:hover { transform: translateY(-3px); }
}

/* ===================================================== floor-plan chrome ==
   Toolbar/inspector surface treatment only — the canvas (.fp-svg and
   everything inside it) is untouched by this rule set. */
.fp-plan { box-shadow: var(--tl-shadow-md); }
.fp-palette-btn, .fp-tab {
  transition: transform var(--tl-dur-1) var(--tl-ease), border-color var(--tl-dur-2) var(--tl-ease);
}
.fp-palette-btn:active { transform: scale(0.96); }
.fp-totals, .fp-inspector { box-shadow: var(--tl-shadow-sm); }

/* ==================================================== reduced-motion belt
   The global rule in base.css already collapses every `animation-duration`
   and `transition-duration` to ~0, which silences tl-spin/tl-aurora-*/the
   spotlight's opacity fade and the tilt card's transition on its own. This
   extra rule additionally removes the tilt/glare and spotlight EFFECTS
   themselves (not just their motion) for anyone who has asked for less
   motion — a static rotated card or a fixed glow patch would still be an
   unrequested visual change even at zero animation duration. */
@media (prefers-reduced-motion: reduce) {
  .tilt-card { transform: none !important; }
  .tilt-card__glare, .tl-spotlight { display: none !important; }
  .hero-aurora::before, .hero-aurora::after { animation: none; }
}
