PageSourceSearch

https://gex-levels.com/assets/landing.js

js gex-levels.com collected 2026-09-24 14:43:56 UTC 67,500 bytes, 1,660 lines download raw bytes

1/* =============================================================================
2   landing.js — GEX Levels 2026 landing page only.
3   Reuses the technical scaffolding of the previously-approved premium-dark.js:
4   - IntersectionObserver scroll-reveal
5   - Subtle scroll-driven parallax with rAF throttling
6   - Single Canvas 2D animation (no WebGL, no library, no tracker)
7   - Pause when off-screen and when the tab is hidden
8   - Respects prefers-reduced-motion (static frame, no parallax, no reveals)
9   - SVG fallback when Canvas is unavailable
10   Visual integration redesigned for the new art direction: floating market-level
11   planes in perspective, subtle grid, soft gold/neutral highlights. No glow neon,
12   no decorative particles, no fake market data, no cursor effects.
13   ========================================================================== */
14(function () {
15  "use strict";
16
17  var reduceMotion = window.matchMedia
18    ? window.matchMedia("(prefers-reduced-motion: reduce)").matches
19    : false;
20
21  // ---- scroll-reveal -----------------------------------------------------
22  function setupReveals() {
23    var targets = document.querySelectorAll("[data-pd-reveal]");
24    if (!("IntersectionObserver" in window) || reduceMotion) {
25      Array.prototype.forEach.call(targets, function (el) { el.classList.add("is-in"); });
26      return;
27    }
28    var io = new IntersectionObserver(function (entries) {
29      entries.forEach(function (entry) {
30        if (entry.isIntersecting) {
31          var el = entry.target;
32          el.classList.add("is-in");
33          io.unobserve(el);
34          // FAIL-SAFE: on some loads the 0.6s opacity fade gets interrupted and
35          // freezes at ~0.16, leaving content (notably the hero) nearly invisible.
36          // After the fade should have finished, pin the final visible state if it
37          // is still stuck. Inline opacity ends the stuck transition for good.
38          setTimeout(function () {
39            if (parseFloat(getComputedStyle(el).opacity) < 0.95) {
40              el.style.opacity = "1";
41              el.style.transform = "none";
42            }
43          }, 900);
44        }
45      });
46    }, { rootMargin: "0px 0px -8% 0px", threshold: 0.10 });
47    Array.prototype.forEach.call(targets, function (el) { io.observe(el); });
48    // Hard fail-safe for above-the-fold content: whatever the observer/transition
49    // does, anything in the initial viewport must be fully visible shortly after
50    // load. Below-the-fold elements keep their scroll-reveal.
51    setTimeout(function () {
52      Array.prototype.forEach.call(targets, function (el) {
53        var r = el.getBoundingClientRect();
54        if (r.top < (window.innerHeight || 800) && r.bottom > 0 &&
55            parseFloat(getComputedStyle(el).opacity) < 0.95) {
56          el.classList.add("is-in");
57          el.style.opacity = "1";
58          el.style.transform = "none";
59        }
60      });
61    }, 1500);
62  }
63
64  // ---- subtle parallax on the hero stage ---------------------------------
65  function setupParallax() {
66    if (reduceMotion) return;
67    var card = document.querySelector("[data-pd-hero-card]");
68    if (!card) return;
69    var ticking = false;
70    function onScroll() {
71      var rect = card.getBoundingClientRect();
72      var center = window.innerHeight / 2 - rect.top - rect.height / 2;
73      var offset = Math.max(-30, Math.min(30, center / 28));
74      card.style.transform = "translate3d(0," + (-offset * 0.4).toFixed(2) + "px,0)";
75    }
76    window.addEventListener("scroll", function () {
77      if (!ticking) {
78        window.requestAnimationFrame(function () { onScroll(); ticking = false; });
79        ticking = true;
80      }
81    }, { passive: true });
82    onScroll();
83  }
84
85  // ---- floating market-level planes (Canvas 2D) --------------------------
86  // Reuses the rAF + dpr-aware resize + off-screen-pause scaffolding of the
87  // previously-approved canvas code, but the visual layer is rebuilt:
88  // perspective-projected horizontal "level planes" labelled Call Wall / Gamma
89  // Flip / Put Wall, drifting slowly along a subtle grid. No prices.
90  function setupHeroScene() {
91    var canvas = document.querySelector("[data-pd-hero-canvas]");
92    var fallback = document.querySelector("[data-pd-hero-fallback]");
93
94    if (!canvas || typeof canvas.getContext !== "function") {
95      if (fallback) fallback.style.display = "block";
96      return;
97    }
98    var ctx = canvas.getContext("2d");
99    if (!ctx) {
100      if (fallback) fallback.style.display = "block";
101      return;
102    }
103
104    var dpr = Math.min(window.devicePixelRatio || 1, 2);
105    var w = 0, h = 0;
106
107    function resize() {
108      var rect = canvas.getBoundingClientRect();
109      dpr = Math.min(window.devicePixelRatio || 1, 2);
110      w = Math.max(2, Math.floor(rect.width));
111      h = Math.max(2, Math.floor(rect.height));
112      canvas.width  = w * dpr;
113      canvas.height = h * dpr;
114      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
115    }
116    resize();
117    var resizeTimer = null;
118    window.addEventListener("resize", function () {
119      window.clearTimeout(resizeTimer);
120      resizeTimer = window.setTimeout(resize, 60);
121    }, { passive: true });
122
123    // Three labelled level "planes" — y0 ratios on the canvas (top → bottom)
124    // call wall above, gamma flip mid, put wall below
125    var levels = [
126      { id: "call", y0: 0.22, color: "rgba(185,199,168,0.65)", soft: "rgba(185,199,168,0.10)", glow: "rgba(185,199,168,0.18)", drift: 0.10 },
127      { id: "flip", y0: 0.50, color: "rgba(200,168,120,0.85)", soft: "rgba(200,168,120,0.14)", glow: "rgba(200,168,120,0.28)", drift: 0.08 },
128      { id: "put",  y0: 0.80, color: "rgba(199,154,154,0.65)", soft: "rgba(199,154,154,0.10)", glow: "rgba(199,154,154,0.18)", drift: 0.12 }
129    ];
130
131    function drawGrid(t) {
132      // Subtle perspective grid: horizontal lines + vertical depth lines
133      ctx.save();
134      ctx.strokeStyle = "rgba(220, 220, 220, 0.05)";
135      ctx.lineWidth = 1;
136      var rows = 6, cols = 10;
137      ctx.beginPath();
138      for (var i = 1; i < rows; i++) {
139        var y = Math.round((i / rows) * h) + 0.5;
140        ctx.moveTo(0, y); ctx.lineTo(w, y);
141      }
142      // Vertical lines with slight drift to suggest very slow camera glide
143      var phase = (t * 0.00004) % 1;
144      for (var j = 0; j <= cols; j++) {
145        var x = Math.round(((j + phase) / cols) * w) + 0.5;
146        ctx.moveTo(x, 0); ctx.lineTo(x, h);
147      }
148      ctx.stroke();
149      ctx.restore();
150    }
151
152    function drawLevelPlane(level, t) {
153      // Two-point perspective: a horizontal "plane" projected with a vanishing
154      // depth band. We draw a soft filled band centred on a midline that
155      // breathes slowly, then a thin highlight line on top.
156      var center = level.y0 * h + Math.sin(t * 0.00012 + level.y0 * 6.0) * 6 * level.drift;
157      var bandTop = center - 14;
158      var bandBot = center + 14;
159      var grad = ctx.createLinearGradient(0, bandTop, 0, bandBot);
160      grad.addColorStop(0.0, "rgba(0,0,0,0)");
161      grad.addColorStop(0.5, level.soft);
162      grad.addColorStop(1.0, "rgba(0,0,0,0)");
163
164      ctx.save();
165      ctx.fillStyle = grad;
166      ctx.fillRect(0, bandTop, w, bandBot - bandTop);
167
168      // Highlight line with soft outer glow
169      ctx.strokeStyle = level.color;
170      ctx.lineWidth = 1.2;
171      ctx.shadowColor = level.glow;
172      ctx.shadowBlur = 12;
173      ctx.beginPath();
174      ctx.moveTo(0, center);
175      ctx.lineTo(w, center);
176      ctx.stroke();
177      ctx.restore();
178
179      // Small inline marker that drifts along the line
180      var markerX = ((Math.sin(t * 0.00018 + level.y0 * 3.2) + 1) / 2) * (w - 40) + 20;
181      ctx.save();
182      ctx.fillStyle = level.color;
183      ctx.shadowColor = level.glow;
184      ctx.shadowBlur = 14;
185      ctx.beginPath();
186      ctx.arc(markerX, center, 2.4, 0, Math.PI * 2);
187      ctx.fill();
188      ctx.restore();
189    }
190
191    function render(t) {
192      ctx.clearRect(0, 0, w, h);
193      // Background warm vignette
194      var bg = ctx.createRadialGradient(w * 0.15, h * 0.15, 0, w * 0.5, h * 0.5, Math.max(w, h));
195      bg.addColorStop(0, "rgba(28, 24, 18, 0.55)");
196      bg.addColorStop(1, "rgba(12, 13, 16, 0)");
197      ctx.fillStyle = bg;
198      ctx.fillRect(0, 0, w, h);
199
200      drawGrid(t);
201      for (var i = 0; i < levels.length; i++) drawLevelPlane(levels[i], t);
202    }
203
204    if (reduceMotion) { render(0); return; }
205
206    var running = true;
207    var visible = true;
208
209    if ("IntersectionObserver" in window) {
210      var visIo = new IntersectionObserver(function (entries) {
211        entries.forEach(function (e) { visible = e.isIntersecting; });
212      }, { threshold: 0 });
213      visIo.observe(canvas);
214    }
215    document.addEventListener("visibilitychange", function () {
216      running = !document.hidden;
217    }, { passive: true });
218
219    function frame(t) {
220      if (running && visible) render(t);
221      window.requestAnimationFrame(frame);
222    }
223    window.requestAnimationFrame(frame);
224  }
225
226  // ---- cursor-tracked 3D card tilt ---------------------------------------
227  // Activates on every element marked [data-tilt]. Mouse only (no touch, no
228  // keyboard interference). Max tilt is ~6deg; movement is rAF-throttled.
229  // Disabled under prefers-reduced-motion and on coarse pointers.
230  function setupCardTilt() {
231    if (reduceMotion) return;
232    var cards = document.querySelectorAll("[data-tilt]");
233    if (!cards.length) return;
234    // Skip on touch / coarse pointer devices to avoid blocked-press feel
235    if (window.matchMedia && window.matchMedia("(pointer: coarse)").matches) return;
236
237    var MAX_TILT = 6;   // degrees
238    var LIFT     = 4;   // px (z-translate via translateY for cross-browser safety)
239
240    Array.prototype.forEach.call(cards, function (card) {
241      var ticking = false;
242      var lx = 0, ly = 0;
243
244      function apply() {
245        ticking = false;
246        var rect = card.getBoundingClientRect();
247        var px = (lx - rect.left) / rect.width;   // 0..1
248        var py = (ly - rect.top)  / rect.height;  // 0..1
249        // Map 0..1 to -1..1 then scale
250        var rotY = (px - 0.5) *  2 * MAX_TILT;     // tilt around vertical axis
251        var rotX = (0.5 - py) *  2 * MAX_TILT;     // tilt around horizontal axis
252        card.style.transform =
253          "perspective(1000px) rotateX(" + rotX.toFixed(2) + "deg)" +
254          " rotateY(" + rotY.toFixed(2) + "deg)" +
255          " translateY(" + (-LIFT) + "px)";
256        card.style.setProperty("--px", (px * 100).toFixed(1) + "%");
257        card.style.setProperty("--py", (py * 100).toFixed(1) + "%");
258      }
259
260      function onMove(e) {
261        lx = e.clientX; ly = e.clientY;
262        if (!ticking) {
263          ticking = true;
264          window.requestAnimationFrame(apply);
265        }
266      }
267      function onLeave() {
268        card.style.transform = "perspective(1000px) rotateX(0) rotateY(0) translateY(0)";
269      }
270      // pointer events cover mouse without firing for touch when ":coarse" was true
271      card.addEventListener("pointermove", onMove);
272      card.addEventListener("pointerleave", onLeave);
273      card.addEventListener("blur", onLeave);
274    });
275  }
276
277  // ---- magnetic CTA hover -------------------------------------------------
278  // Primary CTAs marked [data-magnetic] ease a few pixels toward the cursor.
279  // Mouse only, rAF-throttled, capped at 6px, off under reduced motion.
280  function setupMagneticCtas() {
281    if (reduceMotion) return;
282    if (window.matchMedia && window.matchMedia("(pointer: coarse)").matches) return;
283    var ctas = document.querySelectorAll("[data-magnetic]");
284    if (!ctas.length) return;
285
286    var MAX_PULL = 6; // px
287
288    Array.prototype.forEach.call(ctas, function (cta) {
289      var ticking = false;
290      var lx = 0, ly = 0, inside = false;
291
292      function apply() {
293        ticking = false;
294        if (!inside) return;
295        var rect = cta.getBoundingClientRect();
296        var dx = (lx - (rect.left + rect.width / 2)) / (rect.width / 2);
297        var dy = (ly - (rect.top + rect.height / 2)) / (rect.height / 2);
298        var px = Math.max(-1, Math.min(1, dx)) * MAX_PULL;
299        var py = Math.max(-1, Math.min(1, dy)) * MAX_PULL;
300        cta.style.transform = "translate(" + px.toFixed(1) + "px," + py.toFixed(1) + "px)";
301      }
302
303      cta.addEventListener("pointerenter", function () { inside = true; });
304      cta.addEventListener("pointermove", function (e) {
305        lx = e.clientX; ly = e.clientY;
306        if (!ticking) { ticking = true; window.requestAnimationFrame(apply); }
307      });
308      cta.addEventListener("pointerleave", function () {
309        inside = false;
310        cta.style.transform = "";
311      });
312      cta.addEventListener("blur", function () {
313        inside = false;
314        cta.style.transform = "";
315      });
316    });
317  }
318
319  // ---- mobile nav --------------------------------------------------------
320  function setupMobileNav() {
321    var btn = document.querySelector(".ld-nav__burger");
322    var nav = document.getElementById("ld-mobile-nav");
323    if (!btn || !nav) return;
324    btn.addEventListener("click", function () {
325      var expanded = btn.getAttribute("aria-expanded") === "true";
326      btn.setAttribute("aria-expanded", String(!expanded));
327      nav.hidden = expanded;
328    });
329    Array.prototype.forEach.call(nav.querySelectorAll("a"), function (a) {
330      a.addEventListener("click", function () {
331        btn.setAttribute("aria-expanded", "false");
332        nav.hidden = true;
333      });
334    });
335  }
336
337  // ---- year stamp --------------------------------------------------------
338  function stampYear() {
339    var nodes = document.querySelectorAll("[data-pd-year]");
340    Array.prototype.forEach.call(nodes, function (el) {
341      el.textContent = String(new Date().getFullYear());
342    });
343  }
344
345  // ---- privacy-conscious analytics queue ---------------------------------
346  // No network. No third-party. No personal data. Stores up to 50 events
347  // in-memory for future debug or future provider integration. Sensitive
348  // keys (passwords, tokens, full email addresses) are stripped defensively.
349  function setupAnalytics() {
350    if (window.GEX && window.GEX.event) return; // already installed
351    var QUEUE = [];
352    var MAX  = 50;
353    var REDACT = /password|token|secret|api[_-]?key|authorization|cookie/i;
354    function clean(obj) {
355      if (!obj || typeof obj !== "object") return obj;
356      var out = {};
357      for (var k in obj) if (Object.prototype.hasOwnProperty.call(obj, k)) {
358        if (REDACT.test(k)) { out[k] = "[redacted]"; continue; }
359        var v = obj[k];
360        if (k === "email" && typeof v === "string") {
361          // Keep only the domain — never the local part.
362          var at = v.indexOf("@");
363          out[k] = at > -1 ? "[redacted]@" + v.slice(at + 1) : "[redacted]";
364          continue;
365        }
366        out[k] = typeof v === "object" ? clean(v) : v;
367      }
368      return out;
369    }
370    function record(name, data) {
371      if (typeof name !== "string" || !name) return;
372      if (QUEUE.length >= MAX) QUEUE.shift();
373      QUEUE.push({ n: name, d: clean(data || {}), t: Date.now(), p: location.pathname });
374    }
375    window.GEX = window.GEX || {};
376    window.GEX.event = record;
377    window.GEX.queue = function () { return QUEUE.slice(); };
378
379    // Auto-instrument: page_view, cta clicks (data-evt), product_viewed.
380    record("page_view", { path: location.pathname });
381    var product = document.body.getAttribute("class").indexOf("page-indicator") > -1
382      ? "indicator"
383      : document.body.getAttribute("class").indexOf("page-library") > -1
384        ? "library"
385        : document.body.getAttribute("class").indexOf("page-pricing") > -1
386          ? "pricing"
387          : document.body.getAttribute("class").indexOf("page-extension") > -1
388            ? "extension"
389            : document.body.getAttribute("class").indexOf("page-preview") > -1
390              ? "preview"
391              : document.body.getAttribute("class").indexOf("page-access") > -1
392                ? "access"
393                : null;
394    if (product) record("product_viewed", { product: product });
395    if (product === "pricing") record("pricing_viewed", {});
396
397    document.addEventListener("click", function (e) {
398      var t = e.target.closest && e.target.closest("[data-evt]");
399      if (!t) return;
400      record("cta_clicked", { id: t.getAttribute("data-evt"), label: (t.textContent || "").trim().slice(0, 64) });
401      var plan = t.getAttribute("data-plan");
402      if (plan) record("plan_selected", { plan: plan });
403    }, true);
404
405    // Form lifecycle on the access form.
406    var form = document.querySelector("[data-request-access-form]");
407    if (form) {
408      var started = false;
409      form.addEventListener("input", function () {
410        if (started) return;
411        started = true;
412        record("request_access_started", {});
413      }, { once: false, passive: true });
414      form.addEventListener("invalid", function (e) {
415        record("form_error", { field: e.target && e.target.name ? e.target.name : "unknown", kind: "validation" });
416      }, true);
417      form.addEventListener("submit", function () {
418        var fd = new FormData(form);
419        record("request_access_completed", {
420          product: fd.get("product_requested") || null,
421          has_discord: !!fd.get("discord_username"),
422          nda_acknowledged: !!fd.get("nda_acknowledged")
423          // never log the email local-part, full name, or message body
424        });
425      });
426    }
427  }
428
429  // ---- sticky CTA reveal -------------------------------------------------
430  function setupStickyCta() {
431    var bar = document.querySelector(".ld-stick");
432    if (!bar) return;
433    var threshold = 360; // only show after the user has scrolled past the hero
434    var visible = false;
435    function onScroll() {
436      var shouldShow = window.scrollY > threshold;
437      if (shouldShow !== visible) {
438        visible = shouldShow;
439        bar.classList.toggle("is-in", visible);
440      }
441    }
442    var ticking = false;
443    window.addEventListener("scroll", function () {
444      if (!ticking) {
445        ticking = true;
446        window.requestAnimationFrame(function () { onScroll(); ticking = false; });
447      }
448    }, { passive: true });
449    onScroll();
450  }
451
452  function init() {
453    setupReveals();
454    setupParallax();
455    setupHeroScene();
456    setupCardTilt();
457    setupMagneticCtas();
458    setupMobileNav();
459    setupStickyCta();
460    setupAnalytics();
461    stampYear();
462  }
463
464  if (document.readyState === "loading") {
465    document.addEventListener("DOMContentLoaded", init);
466  } else {
467    init();
468  }
469})();
470
471/* =============================================================================
472   MOTION SYSTEM 2026 — one controller, lazy modules.
473   - Single rAF loop shared by every animated surface (ambient bg, demo marker,
474     hero tag illumination, state rotation). Loop sleeps when nothing is active.
475   - Single IntersectionObserver activates/deactivates animators per visibility.
476   - Single scroll handler (rAF-throttled) drives nav progress, sticky context,
477     ambient intensity.
478   - Modules initialize only when their DOM hooks exist; nothing runs on pages
479     that do not opt in. Reduced motion: loop never starts; static states apply.
480   ========================================================================== */
481(function () {
482  "use strict";
483
484  var reduceMotion = window.matchMedia
485    ? window.matchMedia("(prefers-reduced-motion: reduce)").matches
486    : false;
487  var coarsePointer = window.matchMedia
488    ? window.matchMedia("(pointer: coarse)").matches
489    : false;
490
491  function record(name, data) {
492    try { if (window.GEX && window.GEX.event) window.GEX.event(name, data || {}); } catch (e) { /* noop */ }
493  }
494
495  /* ---- controller --------------------------------------------------------- */
496  var Motion = {
497    animators: [],          // { el, draw(t,dt), active, always }
498    running: false,
499    lastT: 0,
500    fpsInterval: 1000 / 30, // ambient runs at 30fps — plenty for slow drift
501    acc: 0,
502    io: null,
503
504    register: function (el, draw, opts) {
505      var item = { el: el, draw: draw, active: !el, always: Boolean(opts && opts.always) };
506      this.animators.push(item);
507      if (el && "IntersectionObserver" in window) {
508        if (!this.io) {
509          var self = this;
510          this.io = new IntersectionObserver(function (entries) {
511            entries.forEach(function (entry) {
512              for (var i = 0; i < self.animators.length; i++) {
513                if (self.animators[i].el === entry.target) {
514                  self.animators[i].active = entry.isIntersecting;
515                }
516              }
517            });
518            self.wake();
519          }, { rootMargin: "80px 0px" });
520        }
521        this.io.observe(el);
522      } else if (el) {
523        item.active = true;
524      }
525      this.wake();
526      return item;
527    },
528
529    activeCount: function () {
530      var n = 0;
531      for (var i = 0; i < this.animators.length; i++) {
532        if (this.animators[i].active || this.animators[i].always) n++;
533      }
534      return n;
535    },
536
537    wake: function () {
538      if (reduceMotion || this.running) return;
539      if (document.hidden) return;
540      if (!this.activeCount()) return;
541      this.running = true;
542      var self = this;
543      this.lastT = 0;
544      window.requestAnimationFrame(function tick(t) {
545        if (document.hidden || !self.activeCount()) { self.running = false; return; }
546        var dt = self.lastT ? t - self.lastT : 16;
547        self.lastT = t;
548        self.acc += dt;
549        if (self.acc >= self.fpsInterval) {
550          self.acc = 0;
551          for (var i = 0; i < self.animators.length; i++) {
552            var a = self.animators[i];
553            if (a.active || a.always) {
554              try { a.draw(t, dt); } catch (e) { /* never break the loop */ }
555            }
556          }
557        }
558        window.requestAnimationFrame(tick);
559      });
560    }
561  };
562
563  document.addEventListener("visibilitychange", function () {
564    if (!document.hidden) Motion.wake();
565  }, { passive: true });
566
567  // Expose a tiny instrumentation surface for the validation harness.
568  window.GEXMotion = {
569    loops: function () { return Motion.running ? 1 : 0; },
570    animators: function () { return Motion.animators.length; },
571    activeAnimators: function () { return Motion.activeCount(); }
572  };
573  // Page scenes (e.g. the Pricing "Three Access Paths" canvas) register on the
574  // same scheduler so the page never runs more than one rAF loop.
575  window.__gexRegisterAnimator = function (el, draw, opts) {
576    return Motion.register(el, draw, opts);
577  };
578
579  /* ---- shared scroll driver ------------------------------------------------ */
580  var scrollTasks = [];
581  function onScrollFrame() {
582    for (var i = 0; i < scrollTasks.length; i++) {
583      try { scrollTasks[i](); } catch (e) { /* noop */ }
584    }
585  }
586  var scrollTicking = false;
587  window.addEventListener("scroll", function () {
588    if (scrollTicking) return;
589    scrollTicking = true;
590    window.requestAnimationFrame(function () { scrollTicking = false; onScrollFrame(); });
591  }, { passive: true });
592
593  /* ---- ambient background -------------------------------------------------- */
594  function setupAmbient() {
595    var canvas = document.querySelector("[data-ambient-canvas]");
596    if (!canvas || typeof canvas.getContext !== "function") return;
597    var ctx = canvas.getContext("2d");
598    if (!ctx) return;
599
600    var w = 0, h = 0, dpr = 1;
601    function resize() {
602      dpr = Math.min(window.devicePixelRatio || 1, 1.5);
603      w = window.innerWidth; h = window.innerHeight;
604      canvas.width = Math.floor(w * dpr); canvas.height = Math.floor(h * dpr);
605      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
606    }
607    resize();
608    var rT = null;
609    window.addEventListener("resize", function () {
610      window.clearTimeout(rT); rT = window.setTimeout(resize, 120);
611    }, { passive: true });
612
613    // Intensity follows the section closest to the viewport center.
614    var zones = Array.prototype.slice.call(document.querySelectorAll("[data-ambient]"));
615    // Per-section atmospheres: same grid language, distinct mood.
616    var PROFILES = {
617      calm:    { i: 0.5,  gold: 0.0,  grid: 1.0 },
618      active:  { i: 1.0,  gold: 0.3,  grid: 1.25 },
619      gold:    { i: 0.85, gold: 1.0,  grid: 0.9 },
620      archive: { i: 0.4,  gold: 0.15, grid: 0.55 },
621      flow:    { i: 0.75, gold: 0.25, grid: 1.5 },
622      spot:    { i: 1.05, gold: 1.25, grid: 0.8 }
623    };
624    var intensity = 0.7, targetIntensity = 0.7, goldBias = 0, targetGold = 0;
625    var gridBias = 1, targetGrid = 1;
626    function sampleZone() {
627      var mid = window.innerHeight / 2, best = null, bestDist = Infinity;
628      for (var i = 0; i < zones.length; i++) {
629        var r = zones[i].getBoundingClientRect();
630        var center = r.top + r.height / 2;
631        var d = Math.abs(center - mid);
632        if (d < bestDist) { bestDist = d; best = zones[i]; }
633      }
634      var kind = best ? best.getAttribute("data-ambient") : "calm";
635      var p = PROFILES[kind] || PROFILES.calm;
636      targetIntensity = p.i;
637      targetGold = p.gold;
638      targetGrid = p.grid;
639    }
640    scrollTasks.push(sampleZone);
641    sampleZone();
642
643    var simplified = coarsePointer || w < 760;
644    var lines = [
645      { y0: 0.26, hue: "185,199,168", drift: 9 },
646      { y0: 0.52, hue: "200,168,120", drift: 7 },
647      { y0: 0.74, hue: "199,154,154", drift: 11 }
648    ];
649
650    function draw(t) {
651      intensity += (targetIntensity - intensity) * 0.04;
652      goldBias += (targetGold - goldBias) * 0.04;
653      gridBias += (targetGrid - gridBias) * 0.04;
654      ctx.clearRect(0, 0, w, h);
655
656      // Drifting light fields (gold + graphite)
657      var gx = w * (0.22 + 0.10 * Math.sin(t * 0.00005));
658      var gy = h * (0.30 + 0.12 * Math.cos(t * 0.00004));
659      var g1 = ctx.createRadialGradient(gx, gy, 0, gx, gy, Math.max(w, h) * 0.6);
660      g1.addColorStop(0, "rgba(200,168,120," + (0.022 + 0.026 * intensity + 0.018 * goldBias).toFixed(3) + ")");
661      g1.addColorStop(1, "rgba(0,0,0,0)");
662      ctx.fillStyle = g1;
663      ctx.fillRect(0, 0, w, h);
664
665      var hx = w * (0.85 - 0.08 * Math.sin(t * 0.00003));
666      var hy = h * (0.78 + 0.08 * Math.sin(t * 0.000045));
667      var g2 = ctx.createRadialGradient(hx, hy, 0, hx, hy, Math.max(w, h) * 0.5);
668      g2.addColorStop(0, "rgba(40,42,48," + (0.20 + 0.12 * intensity).toFixed(3) + ")");
669      g2.addColorStop(1, "rgba(0,0,0,0)");
670      ctx.fillStyle = g2;
671      ctx.fillRect(0, 0, w, h);
672
673      if (!simplified) {
674        // Slow depth grid
675        ctx.strokeStyle = "rgba(218,213,199," + ((0.018 + 0.020 * intensity) * gridBias).toFixed(3) + ")";
676        ctx.lineWidth = 1;
677        ctx.beginPath();
678        var phase = (t * 0.000012) % 1;
679        var cols = 14;
680        for (var j = 0; j <= cols; j++) {
681          var x = Math.round(((j + phase) / cols) * w) + 0.5;
682          ctx.moveTo(x, 0); ctx.lineTo(x, h);
683        }
684        var rows = 7;
685        for (var k = 1; k < rows; k++) {
686          var y = Math.round((k / rows) * h) + 0.5;
687          ctx.moveTo(0, y); ctx.lineTo(w, y);
688        }
689        ctx.stroke();
690
691        // Faint horizontal market-level lines with slow breathing
692        for (var m = 0; m < lines.length; m++) {
693          var L = lines[m];
694          var ly = L.y0 * h + Math.sin(t * 0.00008 + m * 2.1) * L.drift;
695          var alpha = (0.05 + 0.06 * intensity) * (m === 1 ? (1 + goldBias * 0.8) : 1);
696          ctx.strokeStyle = "rgba(" + L.hue + "," + alpha.toFixed(3) + ")";
697          ctx.beginPath();
698          ctx.moveTo(0, ly); ctx.lineTo(w, ly);
699          ctx.stroke();
700        }
701      }
702    }
703
704    if (reduceMotion) { draw(0); return; }
705    Motion.register(null, draw, { always: true });
706    record("ambient_motion_started", { simplified: simplified });
707  }
708
709  /* ---- nav: progress + active section -------------------------------------- */
710  function setupNav() {
711    var bar = document.querySelector("[data-nav-progress]");
712    var links = Array.prototype.slice.call(document.querySelectorAll(".ld-nav__links a[href^='#']"));
713    var sections = links.map(function (a) {
714      return document.getElementById(a.getAttribute("href").slice(1));
715    });
716    function update() {
717      if (bar) {
718        var doc = document.documentElement;
719        var max = doc.scrollHeight - window.innerHeight;
720        var p = max > 0 ? Math.min(1, window.scrollY / max) : 0;
721        bar.style.transform = "scaleX(" + p.toFixed(4) + ")";
722      }
723      var mid = window.innerHeight * 0.4;
724      var current = -1;
725      for (var i = 0; i < sections.length; i++) {
726        if (!sections[i]) continue;
727        var r = sections[i].getBoundingClientRect();
728        if (r.top <= mid && r.bottom > mid) { current = i; break; }
729      }
730      for (var j = 0; j < links.length; j++) {
731        links[j].classList.toggle("is-active", j === current);
732      }
733    }
734    scrollTasks.push(update);
735    update();
736  }
737
738  /* ---- hero: tag activation sequence + soft illumination cycle ------------- */
739  function setupHeroTags() {
740    var tags = Array.prototype.slice.call(document.querySelectorAll("[data-stage-tag]"));
741    if (!tags.length) return;
742    var order = ["call", "put", "flip", "focus", "zone"];
743    tags.sort(function (a, b) {
744      return order.indexOf(a.getAttribute("data-stage-tag")) - order.indexOf(b.getAttribute("data-stage-tag"));
745    });
746    tags.forEach(function (tag, i) {
747      window.setTimeout(function () { tag.classList.add("is-live"); }, reduceMotion ? 0 : 500 + i * 260);
748    });
749    if (reduceMotion) return;
750    var hot = 0;
751    window.setInterval(function () {
752      if (document.hidden) return;
753      tags.forEach(function (t) { t.classList.remove("is-hot"); });
754      tags[hot % tags.length].classList.add("is-hot");
755      hot += 1;
756    }, 3400);
757  }
758
759  /* ---- product pair: contextual emphasis ----------------------------------- */
760  function setupPair() {
761    var pair = document.querySelector("[data-pair]");
762    var context = document.querySelector("[data-pair-context]");
763    if (!pair || !context) return;
764    var DEFAULT = context.textContent;
765    var COPY = {
766      indicator: "Indicator — subscription, $9.99/mo or $99.99/yr with trial. Lives inside TradingView.",
767      library: "Library — one-time $249.99 purchase, NDA-protected. Independent from the Indicator."
768    };
769    var seen = {};
770    function focus(kind) {
771      context.textContent = COPY[kind] || DEFAULT;
772      if (kind && !seen[kind]) {
773        seen[kind] = true;
774        record("product_card_focused", { product: kind });
775      }
776    }
777    Array.prototype.forEach.call(pair.querySelectorAll("[data-pair-card]"), function (card) {
778      var kind = card.getAttribute("data-pair-card");
779      card.addEventListener("pointerenter", function () { focus(kind); });
780      card.addEventListener("focus", function () { focus(kind); });
781    });
782    pair.addEventListener("pointerleave", function () { context.textContent = DEFAULT; });
783  }
784
785  /* ---- indicator demo -------------------------------------------------------- */
786  function setupDemo() {
787    var demo = document.querySelector("[data-demo]");
788    if (!demo) return;
789    var stage = demo.querySelector("[data-demo-stage]");
790    var status = demo.querySelector("[data-demo-status]");
791    var marker = demo.querySelector("[data-demo-marker]");
792    var panel = demo.querySelector("[data-demo-panel]");
793    var panelSymbol = demo.querySelector("[data-demo-panel-symbol]");
794    var panelNote = demo.querySelector("[data-demo-panel-note]");
795    var tabs = Array.prototype.slice.call(demo.querySelectorAll("[data-demo-symbol]"));
796
797    // Deterministic illustrative layouts (percent of stage height + demo price text).
798    var LAYOUTS = {
799      NQ:  { note: "QQQ option-flow context", call: [16, "21,450"], f1: [30, "21,180"], ucl: [4, 9], flip: [48, "20,925"], bz: [42, 14], f2: [62, "20,610"], put: [80, "20,250"], dcl: [78, 16] },
800      ES:  { note: "SPY option-flow context", call: [20, "6,140"], f1: [34, "6,085"], ucl: [7, 9], flip: [52, "6,010"], bz: [46, 13], f2: [66, "5,955"], put: [84, "5,890"], dcl: [82, 14] },
801      QQQ: { note: "QQQ direct context", call: [14, "538.00"], f1: [28, "531.50"], ucl: [3, 8], flip: [44, "524.00"], bz: [38, 13], f2: [58, "517.50"], put: [76, "509.00"], dcl: [74, 16] },
802      SPY: { note: "SPY direct context", call: [18, "612.00"], f1: [32, "606.50"], ucl: [6, 9], flip: [50, "599.00"], bz: [44, 13], f2: [64, "593.50"], put: [82, "586.00"], dcl: [80, 14] }
803    };
804    var NAMES = { call: "Call Wall", put: "Put Wall", flip: "Gamma Flip", f1: "Focus 1", f2: "Focus 2" };
805
806    var els = {};
807    Array.prototype.forEach.call(demo.querySelectorAll("[data-demo-el]"), function (el) {
808      els[el.getAttribute("data-demo-el")] = el;
809    });
810    var labels = {};
811    Array.prototype.forEach.call(demo.querySelectorAll("[data-demo-label]"), function (el) {
812      labels[el.getAttribute("data-demo-label")] = el;
813    });
814
815    var current = "NQ";
816    var seqTimers = [];
817    var played = false;
818
819    function clearTimers() {
820      seqTimers.forEach(function (id) { window.clearTimeout(id); });
821      seqTimers = [];
822    }
823
824    function apply(symbol) {
825      var L = LAYOUTS[symbol] || LAYOUTS.NQ;
826      ["call", "f1", "flip", "f2", "put"].forEach(function (key) {
827        var el = els[key];
828        if (!el) return;
829        el.style.top = L[key][0] + "%";
830        if (labels[key]) labels[key].textContent = NAMES[key] + " · " + L[key][1];
831      });
832      if (els.ucl) { els.ucl.style.top = L.ucl[0] + "%"; els.ucl.style.height = L.ucl[1] + "%"; }
833      if (els.bz)  { els.bz.style.top = L.bz[0] + "%";  els.bz.style.height = L.bz[1] + "%"; }
834      if (els.dcl) { els.dcl.style.top = L.dcl[0] + "%"; els.dcl.style.height = L.dcl[1] + "%"; }
835      if (panelSymbol) panelSymbol.textContent = symbol;
836      if (panelNote) panelNote.textContent = L.note;
837    }
838
839    function playSequence() {
840      clearTimers();
841      var parts = [els.call, els.put, els.flip, els.f1, els.f2, els.ucl, els.dcl, els.bz, panel, marker];
842      parts.forEach(function (el) { el && el.classList.remove("is-on"); });
843      if (status) { status.textContent = "Loading levels…"; status.classList.remove("is-live"); }
844
845      if (reduceMotion) {
846        parts.forEach(function (el) { el && el.classList.add("is-on"); });
847        if (status) { status.textContent = "Levels available · illustrative"; status.classList.add("is-live"); }
848        return;
849      }
850
851      var steps = [
852        [380, els.call], [560, els.put], [780, els.flip],
853        [980, els.f1], [1120, els.f2],
854        [1320, els.ucl], [1460, els.dcl], [1640, els.bz],
855        [1840, panel], [1900, marker]
856      ];
857      steps.forEach(function (step) {
858        seqTimers.push(window.setTimeout(function () {
859          if (step[1]) step[1].classList.add("is-on");
860        }, step[0]));
861      });
862      seqTimers.push(window.setTimeout(function () {
863        if (status) { status.textContent = "Levels available · illustrative"; status.classList.add("is-live"); }
864      }, 2050));
865    }
866
867    apply(current);
868
869    // Marker drifts along the flip line while the demo is visible.
870    if (marker && stage && !reduceMotion) {
871      Motion.register(stage, function (t) {
872        var flip = els.flip;
873        if (!flip || !marker.classList.contains("is-on")) return;
874        var stageH = stage.clientHeight || 1;
875        var top = parseFloat(flip.style.top || "48") / 100 * stageH;
876        var x = (Math.sin(t * 0.00022) + 1) / 2;
877        marker.style.left = (8 + x * 84) + "%";
878        marker.style.top = (top - 3 + Math.sin(t * 0.0011) * 4) + "px";
879      });
880    }
881
882    // Play once on first visibility; replay on symbol change.
883    if ("IntersectionObserver" in window && !reduceMotion) {
884      var io = new IntersectionObserver(function (entries) {
885        entries.forEach(function (entry) {
886          if (entry.isIntersecting && !played) {
887            played = true;
888            playSequence();
889            io.disconnect();
890          }
891        });
892      }, { threshold: 0.35 });
893      io.observe(demo);
894    } else {
895      playSequence();
896    }
897
898    // Illustrative data states — demonstrates how the real product degrades.
899    var stateButtons = Array.prototype.slice.call(demo.querySelectorAll("[data-demo-state]"));
900    var overlay = demo.querySelector("[data-demo-overlay]");
901    var overlayText = demo.querySelector("[data-demo-overlay-text]");
902    var STATE_COPY = {
903      available: { status: "Levels available \u00b7 illustrative", live: true },
904      stale: { status: "Stale \u00b7 refreshed 14 min ago \u00b7 illustrative", live: false },
905      locked: { status: "Locked \u00b7 subscription required", live: false, overlay: "Subscription required \u2014 the Indicator never partially renders." },
906      unavailable: { status: "Unavailable \u00b7 no silent fallback", live: false, overlay: "Inputs not currently published. The product says so instead of guessing." }
907    };
908    function applyState(name) {
909      var def = STATE_COPY[name] || STATE_COPY.available;
910      demo.setAttribute("data-demo-mode", name);
911      stateButtons.forEach(function (b) {
912        b.setAttribute("aria-pressed", String(b.getAttribute("data-demo-state") === name));
913      });
914      if (status) {
915        status.textContent = def.status;
916        status.classList.toggle("is-live", Boolean(def.live));
917      }
918      if (overlay) {
919        overlay.hidden = !def.overlay;
920        if (overlayText && def.overlay) overlayText.textContent = def.overlay;
921      }
922      record("indicator_demo_interacted", { state: name });
923    }
924    stateButtons.forEach(function (btn) {
925      btn.addEventListener("click", function () { applyState(btn.getAttribute("data-demo-state")); });
926    });
927
928    tabs.forEach(function (tab) {
929      tab.addEventListener("click", function () {
930        var symbol = tab.getAttribute("data-demo-symbol");
931        if (symbol === current) return;
932        current = symbol;
933        tabs.forEach(function (t) { t.setAttribute("aria-selected", String(t === tab)); });
934        apply(symbol);
935        if (demo.getAttribute("data-demo-mode") && demo.getAttribute("data-demo-mode") !== "available") applyState("available");
936        played = true;
937        playSequence();
938        record("indicator_demo_interacted", { symbol: symbol });
939      });
940    });
941  }
942
943  /* ---- feature card: rotating states --------------------------------------- */
944  function setupFeatureStates() {
945    var el = document.querySelector("[data-fv-states]");
946    if (!el) return;
947    var states = ["Available", "Locked", "Stale", "Unavailable"];
948    var i = 0;
949    if (reduceMotion) { el.textContent = states[0]; return; }
950    window.setInterval(function () {
951      if (document.hidden) return;
952      i = (i + 1) % states.length;
953      el.textContent = states[i];
954    }, 2600);
955  }
956
957  /* ---- library: path + domain selection ------------------------------------- */
958  function setupLibrary() {
959    var grid = document.querySelector("[data-lib-grid]");
960    if (!grid) return;
961    var path = document.querySelector("[data-lib-path]");
962    var context = document.querySelector("[data-lib-context]");
963    var DEFAULT = context ? context.textContent : "";
964    var COPY = {
965      "1": "Domain 01 — OptionFlow & OrderFlow: the vocabulary the rest of the system builds on.",
966      "2": "Domain 02 — Bookmap & Volatility: order books and volatility regimes on the same chart.",
967      "3": "Domain 03 — Execution & Risk: construction, sizing, drawdown limits, review.",
968      "4": "Domain 04 — Professional Workflow: routines and checklists for the working week."
969    };
970    if (path && "IntersectionObserver" in window) {
971      var io = new IntersectionObserver(function (entries) {
972        entries.forEach(function (entry) {
973          if (entry.isIntersecting) { path.classList.add("is-on"); io.disconnect(); }
974        });
975      }, { threshold: 0.6 });
976      io.observe(path.parentElement);
977    } else if (path) {
978      path.classList.add("is-on");
979    }
980    var cards = Array.prototype.slice.call(grid.querySelectorAll("[data-lib-domain]"));
981    var seen = {};
982    function select(card) {
983      var id = card.getAttribute("data-lib-domain");
984      cards.forEach(function (c) { c.classList.toggle("is-selected", c === card); });
985      if (context) context.textContent = COPY[id] || DEFAULT;
986      if (!seen[id]) { seen[id] = true; record("library_domain_selected", { domain: id }); }
987    }
988    cards.forEach(function (card) {
989      card.addEventListener("click", function () { select(card); });
990      card.addEventListener("focus", function () { select(card); });
991      card.addEventListener("keydown", function (e) {
992        if (e.key === "Enter" || e.key === " ") { e.preventDefault(); select(card); }
993      });
994    });
995  }
996
997  /* ---- methodology flow ------------------------------------------------------ */
998  function setupMethodology() {
999    var flow = document.querySelector("[data-meth-flow]");
1000    if (!flow) return;
1001    var stages = Array.prototype.slice.call(flow.querySelectorAll("[data-meth-stage]"));
1002    function start() {
1003      flow.classList.add("is-flowing");
1004      stages.forEach(function (stage, i) {
1005        window.setTimeout(function () { stage.classList.add("is-lit"); },
1006          reduceMotion ? 0 : 350 + i * 340);
1007      });
1008    }
1009    if ("IntersectionObserver" in window && !reduceMotion) {
1010      var io = new IntersectionObserver(function (entries) {
1011        entries.forEach(function (entry) {
1012          if (entry.isIntersecting) { start(); io.disconnect(); }
1013        });
1014      }, { threshold: 0.3 });
1015      io.observe(flow);
1016    } else {
1017      start();
1018    }
1019  }
1020
1021  /* ---- plans: focus events ----------------------------------------------------- */
1022  function setupPlanEvents() {
1023    var seen = {};
1024    Array.prototype.forEach.call(document.querySelectorAll("[data-plan-card]"), function (card) {
1025      var plan = card.getAttribute("data-plan-card");
1026      function mark() {
1027        if (seen[plan]) return;
1028        seen[plan] = true;
1029        record("pricing_plan_focused", { plan: plan });
1030      }
1031      card.addEventListener("pointerenter", mark);
1032      card.addEventListener("focusin", mark);
1033    });
1034  }
1035
1036  /* ---- FAQ: animated open/close ------------------------------------------------ */
1037  function setupFaq() {
1038    var items = Array.prototype.slice.call(document.querySelectorAll("[data-faq-item]"));
1039    items.forEach(function (item) {
1040      var summary = item.querySelector("summary");
1041      var body = item.querySelector("p");
1042      if (!summary || !body) return;
1043      item.addEventListener("toggle", function () {
1044        if (item.open) record("faq_opened", { id: item.id || "" });
1045        if (reduceMotion || !item.open) return;
1046        body.style.overflow = "hidden";
1047        body.style.maxHeight = "0px";
1048        body.style.opacity = "0";
1049        window.requestAnimationFrame(function () {
1050          body.style.transition = "max-height 320ms cubic-bezier(.2,.7,.2,1), opacity 320ms cubic-bezier(.2,.7,.2,1)";
1051          body.style.maxHeight = body.scrollHeight + 24 + "px";
1052          body.style.opacity = "1";
1053          window.setTimeout(function () {
1054            body.style.transition = body.style.maxHeight = body.style.overflow = body.style.opacity = "";
1055          }, 360);
1056        });
1057      });
1058    });
1059  }
1060
1061  /* ---- sticky CTA: context awareness -------------------------------------------- */
1062  function setupStickyContext() {
1063    var stick = document.querySelector("[data-stick]");
1064    if (!stick) return;
1065    var copy = stick.querySelector("[data-stick-copy]");
1066    var cta = stick.querySelector("[data-stick-cta]");
1067    var finalSection = document.querySelector("[data-final]");
1068    var zones = Array.prototype.slice.call(document.querySelectorAll("[data-stick-zone]"));
1069
1070    var CONTEXTS = {
1071      products:    { copy: "Three products, separate access — never bundled.", label: "See Indicator plans", href: "#pricing" },
1072      indicator:   { copy: "Indicator from $9.99/month · 3-day trial.", label: "Request Indicator access", href: "/request-access" },
1073      library:     { copy: "Library $249.99 one-time · NDA-protected.", label: "Preview the Library", href: "/preview" },
1074      methodology: { copy: "Plain methodology, plainly priced.", label: "Compare plans", href: "#pricing" },
1075      pricing:     { copy: "Indicator $99.99/yr or $9.99/mo · Library $249.99 once.", label: "Compare plans", href: "/pricing" },
1076      faq:         { copy: "Questions answered plainly — ready when you are.", label: "Request Indicator access", href: "/request-access" }
1077    };
1078    var plansRow = document.querySelector(".ld-plans");
1079
1080    var active = "";
1081    var shown = {};
1082    var suppressed = false;
1083
1084    function setContext(zone) {
1085      if (zone === active) return;
1086      active = zone;
1087      var ctx = CONTEXTS[zone] || CONTEXTS.pricing;
1088      function swap() {
1089        if (copy) copy.textContent = ctx.copy;
1090        if (cta) {
1091          cta.textContent = ctx.label;
1092          cta.setAttribute("href", ctx.href);
1093          cta.setAttribute("data-evt", "home_stick_" + zone);
1094        }
1095      }
1096      if (reduceMotion || !copy) { swap(); return; }
1097      stick.classList.add("is-ctx-swap");
1098      window.setTimeout(function () {
1099        swap();
1100        stick.classList.remove("is-ctx-swap");
1101      }, 210);
1102      if (!shown[zone] && stick.classList.contains("is-in")) {
1103        shown[zone] = true;
1104        record("sticky_cta_shown", { zone: zone });
1105      }
1106    }
1107
1108    function update() {
1109      // Hidden in the hero (before the first mapped section), while the plan
1110      // cards occupy the viewport, and over the final CTA / footer.
1111      var suppress = false;
1112      if (finalSection) {
1113        var r = finalSection.getBoundingClientRect();
1114        if (r.top < window.innerHeight && r.bottom > 0) suppress = true;
1115      }
1116      if (!suppress && plansRow) {
1117        var pr = plansRow.getBoundingClientRect();
1118        if (pr.top < window.innerHeight * 0.8 && pr.bottom > window.innerHeight * 0.45) suppress = true;
1119      }
1120      var mid = window.innerHeight * 0.55;
1121      var zone = "";
1122      for (var i = 0; i < zones.length; i++) {
1123        var zr = zones[i].getBoundingClientRect();
1124        if (zr.top <= mid && zr.bottom > mid) {
1125          zone = zones[i].getAttribute("data-stick-zone");
1126          break;
1127        }
1128      }
1129      if (!zone) suppress = true;
1130      if (suppress !== suppressed) {
1131        suppressed = suppress;
1132        stick.classList.toggle("is-suppressed", suppressed);
1133      }
1134      if (zone && !suppress) setContext(zone);
1135    }
1136    scrollTasks.push(update);
1137    update();
1138
1139    if (cta) {
1140      cta.addEventListener("click", function () {
1141        record("sticky_cta_clicked", { zone: active || "default" });
1142      });
1143    }
1144  }
1145
1146  /* ---- hero stage pointer parallax ------------------------------------------------ */
1147  function setupHeroParallax() {
1148    if (reduceMotion || coarsePointer) return;
1149    var stage = document.querySelector(".ld-hero__stage .ld-stage__frame");
1150    var hero = document.querySelector(".ld-hero");
1151    if (!stage || !hero) return;
1152    var ticking = false, lx = 0, ly = 0, inside = false;
1153    hero.addEventListener("pointermove", function (e) {
1154      lx = e.clientX; ly = e.clientY; inside = true;
1155      if (ticking) return;
1156      ticking = true;
1157      window.requestAnimationFrame(function () {
1158        ticking = false;
1159        if (!inside) return;
1160        var r = stage.getBoundingClientRect();
1161        var dx = Math.max(-1, Math.min(1, (lx - (r.left + r.width / 2)) / (r.width / 2)));
1162        var dy = Math.max(-1, Math.min(1, (ly - (r.top + r.height / 2)) / (r.height / 2)));
1163        stage.style.transform = "rotateX(" + (2 - dy * 2.4).toFixed(2) + "deg) rotateY(" + (-1 + dx * 2.8).toFixed(2) + "deg)";
1164      });
1165    }, { passive: true });
1166    hero.addEventListener("pointerleave", function () {
1167      inside = false;
1168      stage.style.transform = "";
1169    });
1170  }
1171
1172  function init() {
1173    setupAmbient();
1174    setupNav();
1175    setupHeroTags();
1176    setupHeroParallax();
1177    setupPair();
1178    setupDemo();
1179    setupFeatureStates();
1180    setupLibrary();
1181    setupMethodology();
1182    setupPlanEvents();
1183    setupFaq();
1184    setupStickyContext();
1185  }
1186
1187  if (document.readyState === "loading") {
1188    document.addEventListener("DOMContentLoaded", init);
1189  } else {
1190    init();
1191  }
1192})();
1193
1194/* =============================================================================
1195   PRICING SCENE — "The Three Access Paths".
1196   Original procedural Canvas 2D (no Three.js, no Vanta, no remote scenes):
1197   deep market grid, drifting data nodes with faint interconnects, gold and
1198   graphite light fields, an orbital three-ring instrument anchored to the
1199   hero's right side, three luminous access paths that separate toward the
1200   plan cards on scroll, a moving spotlight behind the featured card, and a
1201   single converging line behind the final CTA. One canvas, driven by the
1202   shared Motion controller; static fallback on mobile/reduced-motion.
1203   ========================================================================== */
1204(function () {
1205  "use strict";
1206
1207  var reduceMotion = window.matchMedia
1208    ? window.matchMedia("(prefers-reduced-motion: reduce)").matches
1209    : false;
1210  var coarsePointer = window.matchMedia
1211    ? window.matchMedia("(pointer: coarse)").matches
1212    : false;
1213
1214  function init() {
1215    var canvas = document.querySelector("[data-pricing-scene]");
1216    if (!canvas || typeof canvas.getContext !== "function") return;
1217    var ctx = canvas.getContext("2d");
1218    if (!ctx) return;
1219
1220    var GOLD = "200,168,120";
1221    var GOLD_SOFT = "214,186,140";
1222    var SILVER = "196,200,206";
1223    var PUTRED = "199,154,154";
1224    var CALLGRN = "185,199,168";
1225
1226    var w = 0, h = 0, dpr = 1;
1227    var simplified = coarsePointer || window.innerWidth < 760;
1228
1229    function resize() {
1230      dpr = Math.min(window.devicePixelRatio || 1, 1.5);
1231      w = window.innerWidth; h = window.innerHeight;
1232      canvas.width = Math.floor(w * dpr);
1233      canvas.height = Math.floor(h * dpr);
1234      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
1235      if (staticMode) drawFrame(0);
1236    }
1237
1238    // ---- anchors -----------------------------------------------------------
1239    var anchors = {};
1240    ["hero", "plans", "calm", "final"].forEach(function (k) {
1241      anchors[k] = document.querySelector('[data-scene-anchor="' + k + '"]');
1242    });
1243    var cards = {
1244      lib: document.querySelector('[data-plan-path="lib"]'),
1245      yearly: document.querySelector('[data-plan-path="yearly"]'),
1246      monthly: document.querySelector('[data-plan-path="monthly"]')
1247    };
1248
1249    // ---- pointer parallax (desktop only) ------------------------------------
1250    var px = 0, py = 0, tpx = 0, tpy = 0;
1251    if (!coarsePointer && !reduceMotion) {
1252      document.addEventListener("pointermove", function (e) {
1253        tpx = (e.clientX / Math.max(1, w)) - 0.5;
1254        tpy = (e.clientY / Math.max(1, h)) - 0.5;
1255      }, { passive: true });
1256    }
1257
1258    // ---- nodes (deterministic, no randomness per frame) ---------------------
1259    var NODES = [];
1260    for (var i = 0; i < (simplified ? 0 : 14); i++) {
1261      NODES.push({
1262        sx: (i * 0.37 + 0.05) % 1,
1263        sy: ((i * 0.61) + 0.13) % 1,
1264        spd: 0.5 + (i % 5) * 0.16,
1265        ph: i * 1.7
1266      });
1267    }
1268
1269    // ---- grain tile ----------------------------------------------------------
1270    var grain = null;
1271    if (!simplified && !reduceMotion) {
1272      grain = document.createElement("canvas");
1273      grain.width = grain.height = 96;
1274      var gctx = grain.getContext("2d");
1275      var img = gctx.createImageData(96, 96);
1276      for (var g = 0; g < img.data.length; g += 4) {
1277        var v = ((g * 2654435761) >>> 16) % 255; // deterministic hash noise
1278        img.data[g] = img.data[g + 1] = img.data[g + 2] = v;
1279        img.data[g + 3] = 7;
1280      }
1281      gctx.putImageData(img, 0, 0);
1282    }
1283
1284    // ---- scroll-state (0 hero, 1 plans, 2 calm, 3 final) ---------------------
1285    var state = 0;
1286    function targetState() {
1287      function vis(el) {
1288        if (!el) return -1;
1289        var r = el.getBoundingClientRect();
1290        var mid = h * 0.5;
1291        if (r.top > h) return -1;
1292        if (r.bottom < 0) return 2;             // passed
1293        return 1 - Math.max(0, Math.min(1, (r.top - mid * 0.2) / (mid)));
1294      }
1295      var f = anchors.final && anchors.final.getBoundingClientRect();
1296      if (f && f.top < h * 0.85) return 3;
1297      var c = anchors.calm && anchors.calm.getBoundingClientRect();
1298      if (c && c.top < h * 0.55) return 2;
1299      var p = anchors.plans && anchors.plans.getBoundingClientRect();
1300      if (p && p.top < h * 0.72) return 1;
1301      return 0;
1302    }
1303
1304    function lerp(a, b, t) { return a + (b - a) * t; }
1305    function clamp01(v) { return Math.max(0, Math.min(1, v)); }
1306    function smooth(v) { return v * v * (3 - 2 * v); }
1307
1308    function rectCenterTop(el) {
1309      if (!el) return null;
1310      var r = el.getBoundingClientRect();
1311      return { x: r.left + r.width / 2, y: r.top + 8, w: r.width, h: r.height, cy: r.top + r.height / 2 };
1312    }
1313
1314    // ---- drawing layers -------------------------------------------------------
1315    function drawFields(t, energy) {
1316      var gx = w * (0.2 + 0.06 * Math.sin(t * 0.00004));
1317      var gy = h * (0.25 + 0.08 * Math.cos(t * 0.00005));
1318      var g1 = ctx.createRadialGradient(gx, gy, 0, gx, gy, Math.max(w, h) * 0.65);
1319      g1.addColorStop(0, "rgba(" + GOLD + "," + (0.05 + 0.05 * energy).toFixed(3) + ")");
1320      g1.addColorStop(1, "rgba(0,0,0,0)");
1321      ctx.fillStyle = g1; ctx.fillRect(0, 0, w, h);
1322      var hx = w * 0.86, hy = h * 0.8;
1323      var g2 = ctx.createRadialGradient(hx, hy, 0, hx, hy, Math.max(w, h) * 0.55);
1324      g2.addColorStop(0, "rgba(38,40,46," + (0.18 + 0.1 * energy).toFixed(3) + ")");
1325      g2.addColorStop(1, "rgba(0,0,0,0)");
1326      ctx.fillStyle = g2; ctx.fillRect(0, 0, w, h);
1327    }
1328
1329    function drawTunnel(t, alpha) {
1330      // soft central depth rings toward a vanishing point in the hero
1331      if (alpha <= 0.01) return;
1332      var cx = w * 0.5, cy = h * 0.42;
1333      ctx.save();
1334      ctx.strokeStyle = "rgba(" + SILVER + "," + (0.05 * alpha).toFixed(3) + ")";
1335      for (var i = 1; i <= 4; i++) {
1336        var k = i / 4 + ((t * 0.00001) % 0.25);
1337        ctx.beginPath();
1338        ctx.ellipse(cx, cy, w * 0.62 * k, h * 0.4 * k, 0, 0, Math.PI * 2);
1339        ctx.stroke();
1340      }
1341      ctx.restore();
1342    }
1343
1344    function drawGridAndLevels(t, energy) {
1345      ctx.save();
1346      ctx.strokeStyle = "rgba(218,213,199," + (0.015 + 0.02 * energy).toFixed(3) + ")";
1347      ctx.lineWidth = 1;
1348      ctx.beginPath();
1349      var phase = (t * 0.00001) % 1;
1350      for (var j = 0; j <= 13; j++) {
1351        var x = Math.round(((j + phase) / 13) * w) + 0.5;
1352        ctx.moveTo(x, 0); ctx.lineTo(x, h);
1353      }
1354      ctx.stroke();
1355      // market-level lines at depths; occasional illumination
1356      var hues = [CALLGRN, GOLD, PUTRED, SILVER];
1357      for (var m = 0; m < 4; m++) {
1358        var y = h * (0.18 + m * 0.21) + Math.sin(t * 0.00006 + m * 2.4) * (10 - m * 2);
1359        var lit = Math.max(0, Math.sin(t * 0.00013 + m * 2.1) - 0.92) * 12; // rare slow glow
1360        ctx.strokeStyle = "rgba(" + hues[m] + "," + ((0.04 + 0.05 * energy) * (1 + lit)).toFixed(3) + ")";
1361        ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke();
1362      }
1363      ctx.restore();
1364    }
1365
1366    function drawNodes(t, energy) {
1367      if (!NODES.length || energy <= 0.05) return;
1368      var pts = [];
1369      for (var i = 0; i < NODES.length; i++) {
1370        var n = NODES[i];
1371        var x = (n.sx + Math.sin(t * 0.00002 * n.spd + n.ph) * 0.04) * w;
1372        var y = (n.sy + Math.cos(t * 0.000016 * n.spd + n.ph) * 0.05) * h;
1373        pts.push([x, y]);
1374      }
1375      ctx.save();
1376      // faint interconnects between near nodes
1377      ctx.strokeStyle = "rgba(" + SILVER + "," + (0.045 * energy).toFixed(3) + ")";
1378      for (var a = 0; a < pts.length; a++) {
1379        for (var b = a + 1; b < pts.length; b++) {
1380          var dx = pts[a][0] - pts[b][0], dy = pts[a][1] - pts[b][1];
1381          if (dx * dx + dy * dy < 180 * 180) {
1382            ctx.beginPath(); ctx.moveTo(pts[a][0], pts[a][1]); ctx.lineTo(pts[b][0], pts[b][1]); ctx.stroke();
1383          }
1384        }
1385      }
1386      for (var k = 0; k < pts.length; k++) {
1387        ctx.fillStyle = "rgba(" + (k % 3 === 0 ? GOLD : SILVER) + "," + (0.3 * energy).toFixed(3) + ")";
1388        ctx.beginPath(); ctx.arc(pts[k][0], pts[k][1], 1.4, 0, Math.PI * 2); ctx.fill();
1389      }
1390      ctx.restore();
1391    }
1392
1393    // orbital instrument: 3 rings, one per access path
1394    var RINGS = [
1395      { hue: SILVER, rx: 1.0, rz: 0.32, tilt: 0.5, spd: 0.00006, width: 1.1, alpha: 0.5 },   // library
1396      { hue: GOLD, rx: 0.78, rz: 0.46, tilt: -0.35, spd: 0.00009, width: 1.6, alpha: 0.9 },  // yearly (featured)
1397      { hue: GOLD_SOFT, rx: 0.56, rz: 0.24, tilt: 1.1, spd: 0.000075, width: 1.1, alpha: 0.6 } // monthly
1398    ];
1399
1400    function orbCenter() {
1401      var heroR = anchors.hero && anchors.hero.getBoundingClientRect();
1402      if (!heroR) return null;
1403      var cx = w * (w > 1100 ? 0.76 : 0.5);
1404      var cy = heroR.top + heroR.height * (w > 1100 ? 0.46 : 0.2);
1405      return { x: cx + px * 14, y: cy + py * 10, r: Math.min(w * 0.16, 230) };
1406    }
1407
1408    function drawOrbital(t, alpha) {
1409      var c = orbCenter();
1410      if (!c || alpha <= 0.01) return;
1411      var rise = (1 - alpha) * -60; // moves upward as it fades
1412      ctx.save();
1413      ctx.translate(c.x, c.y + rise);
1414      // core
1415      var core = ctx.createRadialGradient(0, 0, 0, 0, 0, c.r * 0.5);
1416      core.addColorStop(0, "rgba(" + GOLD + "," + (0.16 * alpha).toFixed(3) + ")");
1417      core.addColorStop(1, "rgba(0,0,0,0)");
1418      ctx.fillStyle = core;
1419      ctx.fillRect(-c.r, -c.r, c.r * 2, c.r * 2);
1420      ctx.strokeStyle = "rgba(" + SILVER + "," + (0.4 * alpha).toFixed(3) + ")";
1421      ctx.beginPath(); ctx.arc(0, 0, c.r * 0.16, 0, Math.PI * 2); ctx.stroke();
1422      // rings + orbiting nodes
1423      for (var i = 0; i < RINGS.length; i++) {
1424        var R = RINGS[i];
1425        var rot = t * R.spd + i;
1426        ctx.save();
1427        ctx.rotate(R.tilt + Math.sin(t * 0.00002 + i) * 0.05 + px * 0.08);
1428        ctx.strokeStyle = "rgba(" + R.hue + "," + (R.alpha * 0.5 * alpha).toFixed(3) + ")";
1429        ctx.lineWidth = R.width;
1430        ctx.beginPath();
1431        ctx.ellipse(0, 0, c.r * R.rx, c.r * R.rz, 0, 0, Math.PI * 2);
1432        ctx.stroke();
1433        var nx = Math.cos(rot) * c.r * R.rx;
1434        var ny = Math.sin(rot) * c.r * R.rz;
1435        ctx.fillStyle = "rgba(" + R.hue + "," + (R.alpha * alpha).toFixed(3) + ")";
1436        ctx.shadowColor = "rgba(" + R.hue + "," + (0.6 * alpha).toFixed(3) + ")";
1437        ctx.shadowBlur = 10;
1438        ctx.beginPath(); ctx.arc(nx, ny, i === 1 ? 3.4 : 2.4, 0, Math.PI * 2); ctx.fill();
1439        ctx.restore();
1440      }
1441      ctx.restore();
1442    }
1443
1444    var PATHS = [
1445      { key: "lib", hue: SILVER, alpha: 0.5, width: 1.1 },
1446      { key: "yearly", hue: GOLD, alpha: 0.95, width: 1.8 },
1447      { key: "monthly", hue: GOLD_SOFT, alpha: 0.6, width: 1.2 }
1448    ];
1449
1450    function drawPaths(t, progress, fade) {
1451      if (progress <= 0.01 || fade <= 0.01) return;
1452      var c = orbCenter();
1453      if (!c) return;
1454      var startY = c.y - 30;
1455      for (var i = 0; i < PATHS.length; i++) {
1456        var P = PATHS[i];
1457        var target = rectCenterTop(cards[P.key]);
1458        if (!target || target.y < -h || target.y > h * 2) continue;
1459        var seg = clamp01(progress * 1.35 - i * 0.12); // progressive illumination
1460        if (seg <= 0) continue;
1461        ctx.save();
1462        ctx.strokeStyle = "rgba(" + P.hue + "," + (P.alpha * 0.55 * fade * seg).toFixed(3) + ")";
1463        ctx.lineWidth = P.width;
1464        ctx.shadowColor = "rgba(" + P.hue + "," + (0.35 * fade).toFixed(3) + ")";
1465        ctx.shadowBlur = 6;
1466        var x0 = c.x + (i - 1) * 18, y0 = startY;
1467        var x1 = target.x, y1 = target.y;
1468        var mx = lerp(x0, x1, 0.5), my = Math.min(y0, y1) - 40;
1469        // draw-on via partial quadratic sampling
1470        ctx.beginPath();
1471        var steps = 22;
1472        for (var sIdx = 0; sIdx <= Math.floor(steps * smooth(seg)); sIdx++) {
1473          var u = sIdx / steps;
1474          var xx = (1 - u) * (1 - u) * x0 + 2 * (1 - u) * u * mx + u * u * x1;
1475          var yy = (1 - u) * (1 - u) * y0 + 2 * (1 - u) * u * my + u * u * y1;
1476          if (sIdx === 0) ctx.moveTo(xx, yy); else ctx.lineTo(xx, yy);
1477        }
1478        ctx.stroke();
1479        ctx.restore();
1480      }
1481    }
1482
1483    function drawSpotlight(t, alpha) {
1484      if (alpha <= 0.01) return;
1485      var target = rectCenterTop(cards.yearly);
1486      if (!target || target.cy < -200 || target.cy > h + 200) return;
1487      var sx = target.x + Math.sin(t * 0.00008) * 36;
1488      var grad = ctx.createRadialGradient(sx, target.cy, 0, sx, target.cy, Math.max(280, target.h));
1489      grad.addColorStop(0, "rgba(" + GOLD + "," + (0.085 * alpha).toFixed(3) + ")");
1490      grad.addColorStop(1, "rgba(0,0,0,0)");
1491      ctx.fillStyle = grad;
1492      ctx.fillRect(0, 0, w, h);
1493    }
1494
1495    function drawFinalConvergence(t, alpha) {
1496      if (alpha <= 0.01) return;
1497      var f = anchors.final && anchors.final.getBoundingClientRect();
1498      if (!f || f.top > h) return;
1499      var y = f.top + f.height * 0.5;
1500      ctx.save();
1501      ctx.strokeStyle = "rgba(" + GOLD + "," + (0.4 * alpha).toFixed(3) + ")";
1502      ctx.lineWidth = 1.4;
1503      ctx.shadowColor = "rgba(" + GOLD + ",0.45)";
1504      ctx.shadowBlur = 10;
1505      var sweep = w * 0.5 * alpha;
1506      ctx.beginPath();
1507      ctx.moveTo(w * 0.5 - sweep, y);
1508      ctx.lineTo(w * 0.5 + sweep, y);
1509      ctx.stroke();
1510      ctx.restore();
1511    }
1512
1513    function drawGrain(t) {
1514      if (!grain) return;
1515      ctx.save();
1516      ctx.globalAlpha = 0.5;
1517      var ox = -((t * 0.004) % 96), oy = -((t * 0.003) % 96);
1518      for (var x = ox; x < w; x += 96) {
1519        for (var y = oy; y < h; y += 96) ctx.drawImage(grain, x, y);
1520      }
1521      ctx.restore();
1522    }
1523
1524    function drawFrame(t) {
1525      ctx.clearRect(0, 0, w, h);
1526      px = lerp(px, tpx, 0.06); py = lerp(py, tpy, 0.06);
1527      state = lerp(state, targetState(), reduceMotion ? 1 : 0.05);
1528
1529      var heroA = clamp01(1 - state);            // orbital visibility
1530      var pathP = clamp01(state);                // path draw-on 0..1 toward plans
1531      var pathFade = clamp01(1 - (state - 1.4)); // fade through calm
1532      var calm = clamp01(state - 1);             // 0 plans → 1 calm
1533      var finalA = clamp01(state - 2);           // final convergence
1534      var energy = lerp(1, 0.35, clamp01(calm)); // background activity
1535
1536      drawFields(t, energy);
1537      if (simplified) {
1538        // mobile: gradient depth + a single static orbital outline in the hero
1539        var c = orbCenter();
1540        if (c && heroA > 0.05) {
1541          ctx.strokeStyle = "rgba(" + GOLD + "," + (0.35 * heroA).toFixed(3) + ")";
1542          ctx.lineWidth = 1.4;
1543          ctx.beginPath();
1544          ctx.ellipse(w * 0.5, c.y, Math.min(w * 0.34, 180), Math.min(w * 0.34, 180) * 0.4, -0.3, 0, Math.PI * 2);
1545          ctx.stroke();
1546        }
1547        return;
1548      }
1549      drawTunnel(t, heroA * energy);
1550      drawGridAndLevels(t, energy);
1551      drawNodes(t, energy * (1 - finalA));
1552      drawOrbital(t, heroA);
1553      drawPaths(t, pathP, pathFade);
1554      drawSpotlight(t, clamp01(pathP * 1.2) * pathFade);
1555      drawFinalConvergence(t, finalA);
1556      drawGrain(t);
1557    }
1558
1559    var staticMode = reduceMotion || false;
1560    resize();
1561    var rT = null;
1562    window.addEventListener("resize", function () {
1563      window.clearTimeout(rT);
1564      rT = window.setTimeout(function () { simplified = coarsePointer || window.innerWidth < 760; resize(); }, 120);
1565    }, { passive: true });
1566
1567    if (reduceMotion) {
1568      state = targetState();
1569      drawFrame(0);
1570      window.addEventListener("scroll", function () {
1571        window.requestAnimationFrame(function () { state = targetState(); drawFrame(0); });
1572      }, { passive: true });
1573      return;
1574    }
1575
1576    // register on the shared Motion controller when available, else local rAF
1577    if (window.GEXMotion && typeof window.__gexRegisterAnimator === "function") {
1578      window.__gexRegisterAnimator(null, drawFrame, { always: true });
1579    } else {
1580      var running = true;
1581      document.addEventListener("visibilitychange", function () { running = !document.hidden; }, { passive: true });
1582      (function loop(t) {
1583        if (running) drawFrame(t || 0);
1584        window.requestAnimationFrame(loop);
1585      })(0);
1586    }
1587    if (window.GEX && window.GEX.event) window.GEX.event("ambient_motion_started", { page: "pricing", simplified: simplified });
1588  }
1589
1590  if (document.readyState === "loading") {
1591    document.addEventListener("DOMContentLoaded", init);
1592  } else {
1593    init();
1594  }
1595})();
1596
1597// Blog CTA routing: on Library-adjacent content pages the Library purchase
1598// is the primary revenue action ($249.99 one-time vs $9.99/mo trial).
1599// Indicator-specific article URLs keep their existing CTA ordering.
1600(function () {
1601  var path = window.location.pathname;
1602  if (!path.startsWith("/blog/") || /indicator/i.test(path)) return;
1603  function routeBlogCtas() {
1604    var buyCta = document.querySelector(".ld-buy-cta");
1605    if (buyCta) {
1606      var p = buyCta.querySelector(".ld-buy-cta__primary");
1607      var s = buyCta.querySelector(".ld-buy-cta__secondary");
1608      if (p && /indicator/i.test(p.getAttribute("href") || "")) {
1609        p.setAttribute("href", "/checkout");
1610        p.textContent = "Get the Library — $249.99";
1611        if (s) { s.setAttribute("href", "/checkout/indicator-monthly"); s.textContent = "Start free trial — $9.99/mo"; }
1612      }
1613    }
1614    var fin = document.querySelector("[data-final] .ld-btn--primary");
1615    if (fin && /indicator/i.test(fin.getAttribute("href") || "")) {
1616      fin.setAttribute("href", "/checkout");
1617      fin.textContent = "Get the Library — $249.99";
1618      var finG = document.querySelector("[data-final] .ld-btn--ghost");
1619      if (finG) { finG.setAttribute("href", "/checkout/indicator-monthly"); finG.textContent = "Start free trial"; }
1620    }
1621  }
1622  if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", routeBlogCtas); } else { routeBlogCtas(); }
1623})();
1624
1625/* ── Copy the TradingView indicator (extension page) ────────────────────────
1626   The Pine source is injected at build time as base64 on the button. Copying
1627   from the page rather than a downloaded file guarantees LF-only text: a stray
1628   CR on line 1 stops TradingView matching //@version and it silently falls back
1629   to Pine v1, which loads in its own pane. Base64 also keeps Pine syntax like
1630   "style = plot.style_linebr" from tripping the strict-CSP inline-style audit. */
1631(function gexCopyPineIndicator() {
1632  var btn = document.querySelector("[data-pine-copy]");
1633  if (!btn) return;
1634  var cached = "";
1635  function source() {
1636    if (cached) return cached;
1637    try {
1638      var raw = atob(btn.getAttribute("data-pine-b64") || "");
1639      var bytes = Uint8Array.from(raw, function (c) { return c.charCodeAt(0); });
1640      cached = new TextDecoder("utf-8").decode(bytes).split(String.fromCharCode(13)).join("");
1641    } catch (e) { cached = ""; }
1642    return cached;
1643  }
1644  btn.addEventListener("click", async function () {
1645    var text = source();
1646    var label = btn.textContent;
1647    if (!text || text.indexOf("//@version") !== 0) {
1648      btn.textContent = "Unavailable — reload the page";
1649      setTimeout(function () { btn.textContent = label; }, 2400);
1650      return;
1651    }
1652    try {
1653      await navigator.clipboard.writeText(text);
1654      btn.textContent = "Copied — paste in the Pine Editor";
1655    } catch (e) {
1656      btn.textContent = "Copy blocked by the browser";
1657    }
1658    setTimeout(function () { btn.textContent = label; }, 2400);
1659  });
1660})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.