1/* account-intro.js (2026-08-22) â the member area's opening film. 2 * 3 * Plays ONCE per browser, and only for a signed-in member: it waits for 4 * [data-account-email] to be filled with a real address by app.js before opening, 5 * so a logged-out visitor bouncing through /account never sees "Welcome back". 6 * A "Replay the intro" button in the dashboard calls GEXAccountIntro.play(). 7 * 8 * CSP: external file, no inline <style>/<script>, no style attributes in markup. 9 * The only JS-set styles are the canvas size and the progress bar width, applied 10 * through the CSSOM â allowed under the strict style-src 'self'. 11 * 12 * Audio autoplay is blocked everywhere, so the film opens on a "Begin" click. The 13 * picture follows the VOICE (audio.currentTime) and falls back to a wall clock if 14 * playback is blocked or the file fails, so it can never freeze mid-scene. 15 */ 16(function () { 17 "use strict"; 18 19 var SEEN_KEY = "gex_acct_intro_seen"; 20 var VO = "/assets/account-intro.mp3"; 21 var END = 19.8; // VO is ~19.1s; hold the last frame briefly 22 23 var root = null, CV = null, CX = null, raf = 0, audio = null; 24 var started = false, sceneIdx = -1, t0 = 0; 25 26 function seen() { try { return localStorage.getItem(SEEN_KEY) === "1"; } catch (e) { return false; } } 27 function markSeen() { try { localStorage.setItem(SEEN_KEY, "1"); } catch (e) {} } 28 29 /* ---- the grid: a control surface coming online, drawn line by line ---- */ 30 function sizeCanvas() { 31 if (!CV) return; 32 var d = Math.min(window.devicePixelRatio || 1, 2); 33 CV.width = window.innerWidth * d; CV.height = window.innerHeight * d; 34 CV.style.width = window.innerWidth + "px"; CV.style.height = window.innerHeight + "px"; 35 CX.setTransform(d, 0, 0, d, 0, 0); 36 } 37 function draw(t) { 38 if (!CX) return; 39 var w = window.innerWidth, h = window.innerHeight, cx = w / 2, cy = h / 2; 40 CX.fillStyle = "#070504"; 41 CX.fillRect(0, 0, w, h); 42 43 var step = 58; 44 var reach = Math.min(1, t / 6); // the grid draws itself in 45 var maxR = Math.sqrt(cx * cx + cy * cy) * reach; 46 47 CX.lineWidth = 1; 48 for (var x = cx % step; x < w; x += step) { 49 var dx = Math.abs(x - cx); 50 if (dx > maxR) continue; 51 CX.strokeStyle = "rgba(200,168,120," + (0.10 * (1 - dx / (maxR + 1))).toFixed(3) + ")"; 52 CX.beginPath(); CX.moveTo(x, 0); CX.lineTo(x, h); CX.stroke(); 53 } 54 for (var y = cy % step; y < h; y += step) { 55 var dy = Math.abs(y - cy); 56 if (dy > maxR) continue; 57 CX.strokeStyle = "rgba(200,168,120," + (0.10 * (1 - dy / (maxR + 1))).toFixed(3) + ")"; 58 CX.beginPath(); CX.moveTo(0, y); CX.lineTo(w, y); CX.stroke(); 59 } 60 61 // a slow sweep of light across the surface, like a panel refreshing 62 var sweep = ((t * 150) % (w + 460)) - 230; 63 var g = CX.createLinearGradient(sweep - 230, 0, sweep + 230, 0); 64 g.addColorStop(0, "transparent"); 65 g.addColorStop(0.5, "rgba(232,210,164,.055)"); 66 g.addColorStop(1, "transparent"); 67 CX.fillStyle = g; CX.fillRect(0, 0, w, h); 68 69 // warm pool under the copy 70 var pool = CX.createRadialGradient(cx, cy, 0, cx, cy, Math.max(w, h) * 0.6); 71 pool.addColorStop(0, "rgba(200,168,120,.07)"); 72 pool.addColorStop(1, "transparent"); 73 CX.fillStyle = pool; CX.fillRect(0, 0, w, h); 74 } 75 76 /* ---- scenes, timed to the voice ---- */ 77 var SCENES = [ 78 { a: 0.0, b: 3.2, html: '<p class="ai__line">Welcome <em>back</em>.</p>' }, 79 { a: 3.2, b: 7.4, html: '<p class="ai__line">This is your desk.<br>Everything you have access to, <em>in one place</em>.</p>' }, 80 { a: 7.4, b: 12.4, kind: "tiles" }, 81 { a: 12.4, b: 16.2, html: '<p class="ai__line">Your journal. Your levels.<br>Your <em>library</em>.</p>' }, 82 { a: 16.2, b: 20.5, html: '<p class="ai__line ai__line--hero">Now, get to <em>work</em>.</p>' } 83 ]; 84 function stage() { return root ? root.querySelector(".ai__stage") : null; } 85 function setScene(i) { 86 if (i === sceneIdx) return; 87 sceneIdx = i; 88 var st = stage(), s = SCENES[i]; 89 if (!st || !s) return; 90 if (s.kind === "tiles") { 91 st.innerHTML = 92 '<div class="ai__tiles">' + 93 '<div class="ai__tile"><span>Your products</span><b>Unlocked</b><p>The Library and the Indicator, and exactly what each one gives you.</p></div>' + 94 '<div class="ai__tile"><span>The regime</span><b>Today</b><p>How dealers are positioned, read before the open.</p></div>' + 95 '<div class="ai__tile"><span>Your rank</span><b>Earned</b><p>Progress that moves you up, and the price it has earned you.</p></div>' + 96 '</div>'; 97 var tiles = st.querySelectorAll(".ai__tile"); 98 for (var k = 0; k < tiles.length; k++) tiles[k].style.animationDelay = (k * 0.17) + "s"; 99 } else { 100 st.innerHTML = s.html; 101 } 102 } 103 104 function clock() { 105 var wall = (performance.now() - t0) / 1000; 106 return (audio && audio.currentTime > 0.05) ? audio.currentTime : wall; 107 } 108 function tick() { 109 var t = clock(); 110 draw(t); 111 var bar = root && root.querySelector(".ai__bar i"); 112 if (bar) bar.style.width = Math.min(100, (t / END) * 100) + "%"; 113 for (var i = 0; i < SCENES.length; i++) { 114 if (t >= SCENES[i].a && t < SCENES[i].b) { setScene(i); break; } 115 } 116 if (started && t >= END) { close(); return; } 117 raf = window.requestAnimationFrame(tick); 118 } 119 120 function begin() { 121 if (started) return; 122 started = true; t0 = performance.now(); 123 var gate = root.querySelector(".ai__gate"); 124 if (gate && gate.parentNode) gate.parentNode.removeChild(gate); 125 var skip = root.querySelector(".ai__skip"); 126 if (skip) skip.hidden = false;
127 try { 128 audio = new Audio(VO); 129 audio.preload = "auto"; 130 audio.play().catch(function () {}); 131 audio.addEventListener("error", function () { audio = null; }); 132 audio.addEventListener("ended", function () { window.setTimeout(close, 650); }); 133 } catch (e) { audio = null; } 134 window.cancelAnimationFrame(raf); 135 raf = window.requestAnimationFrame(tick); 136 } 137 138 function close() { 139 if (!root) return; 140 window.cancelAnimationFrame(raf); 141 if (audio) { try { audio.pause(); } catch (e) {} audio = null; } 142 markSeen(); 143 var node = root; 144 root = null; CV = null; CX = null; started = false; sceneIdx = -1; 145 document.body.classList.remove("ai-lock"); 146 try { 147 var shut = function () { if (node.parentNode) node.parentNode.removeChild(node); }; 148 node.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 600, easing: "ease" }).onfinish = shut; 149 /* a hidden tab pauses the fade forever â the teardown must not depend on it */ 150 window.setTimeout(shut, 850); 151 } catch (e) { if (node.parentNode) node.parentNode.removeChild(node); } 152 } 153 154 function open() { 155 if (root) return; 156 root = document.createElement("div"); 157 root.id = "acctIntro"; 158 root.className = "is-on"; 159 root.setAttribute("aria-label", "Member area opening"); 160 root.innerHTML = 161 '<canvas></canvas><div class="ai__vig"></div>' + 162 '<div class="ai__stage">' + 163 '<div class="ai__gate">' + 164 '<div class="ai__mark">▲</div>' + 165 '<h2 class="ai__t">Your member area</h2>' + 166 '<p class="ai__s">A quick tour of what lives here. Twenty seconds, once.</p>' + 167 '<button class="ai__btn" type="button">Begin</button>' + 168 '<button class="ai__skipgate" type="button">Skip →</button>' + 169 '</div>' + 170 '</div>' + 171 '<button class="ai__skip" type="button" hidden>Skip</button>' + 172 '<div class="ai__bar"><i></i></div>'; 173 document.body.appendChild(root); 174 document.body.classList.add("ai-lock"); 175 CV = root.querySelector("canvas"); CX = CV.getContext("2d"); 176 sizeCanvas(); 177 root.querySelector(".ai__btn").addEventListener("click", begin); 178 root.querySelector(".ai__skipgate").addEventListener("click", close); 179 root.querySelector(".ai__skip").addEventListener("click", close); 180 draw(0); 181 (function idle() { // the grid keeps breathing behind the gate 182 if (root && !started) { draw((performance.now() % 60000) / 1000); window.requestAnimationFrame(idle); } 183 })(); 184 } 185 186 /* Only for a signed-in member: app.js replaces the "Loadingâ¦" placeholder in 187 [data-account-email] with the real address once the session resolves. */ 188 function signedIn() { 189 var el = document.querySelector("[data-account-email]"); 190 return !!(el && el.textContent && el.textContent.indexOf("@") > -1); 191 } 192 function waitForMember(tries) { 193 if (seen() || root) return; 194 if (signedIn()) { open(); return; } 195 if (tries <= 0) return; 196 window.setTimeout(function () { waitForMember(tries - 1); }, 400); 197 } 198 199 window.addEventListener("resize", function () { if (root) sizeCanvas(); }); 200 201 /* The dashboard's "Replay" control â delegated so it works no matter when the 202 panel is rendered, and needs no inline handler (strict script-src). */ 203 document.addEventListener("click", function (e) { 204 var b = e.target && e.target.closest ? e.target.closest("[data-intro-replay]") : null; 205 if (!b) return; 206 e.preventDefault(); 207 open(); 208 }); 209 210 window.GEXAccountIntro = { 211 play: function () { open(); }, // the dashboard's "Replay" button 212 maybePlay: function () { waitForMember(30); } // ~12s of polling, then gives up 213 }; 214 215 if (document.readyState === "loading") { 216 document.addEventListener("DOMContentLoaded", function () { waitForMember(30); }); 217 } else { 218 waitForMember(30); 219 } 220})();
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.