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.