1/* ZeroRank home.js â the homepage-only sections (radar, bento, workflow, growth curve, MCP transmission). */ 2(function () { 3 "use strict"; 4 5 var radar = document.getElementById("radar"); 6 var sweep = document.getElementById("sweep"); 7 var hub = document.getElementById("hub"); 8 /* the chrome is shared with pages that have no radar â bail rather than 9 throw, or the reveal observer below never gets to run */ 10 if (!radar) return; 11 var nodes = [].slice.call(radar.querySelectorAll(".node")); 12 var still = window.matchMedia("(prefers-reduced-motion: reduce)").matches; 13 14 /* ââ rays ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 15 [ left %, glow centre y %, glow y-radius % ] â measured off node 16 9377:6376. Columns sit 95.072px apart; the glow centre and radius vary 17 column to column, which is what keeps the wash from looking mechanical. 18 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 19 var RAYS = [ 20 [ 0.0339, 44.526, 54.515], 21 [ 7.9504, 44.526, 54.515], 22 [15.8673, 44.526, 54.515], 23 [23.7838, 44.526, 54.515], 24 [31.7344, 44.526, 54.515], 25 [39.6571, 34.351, 49.414], 26 [47.5800, 46.806, 54.515], 27 [55.5025, 46.806, 54.515], 28 [63.4252, 56.155, 49.414], 29 [71.3478, 46.806, 54.515], 30 [79.2704, 46.806, 54.515], 31 [87.1931, 56.155, 49.414], 32 [95.1005, 57.238, 49.414] 33 ]; 34 35 var raysEl = document.getElementById("rays"); 36 RAYS.forEach(function (r, i) { 37 var el = document.createElement("div"); 38 el.className = "ray"; 39 el.style.left = r[0] + "%"; 40 el.style.setProperty("--cy", r[1] + "%"); 41 el.style.setProperty("--ry", r[2] + "%"); 42 el.style.setProperty("--dur", (38 + (i % 5) * 9) + "s"); 43 el.style.setProperty("--delay", (i * -4.7) + "s"); 44 raysEl.appendChild(el); 45 }); 46 47 /* ââ nodes âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 48 Each node is built twice: an amber question-mark chip for the unknown 49 state, and the resolved prompt pill. They share one grid cell so they 50 swap in place. 51 ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ */ 52 var QMARK = 53 '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"' + 54 ' stroke-linecap="round" stroke-linejoin="round">' + 55 '<circle cx="12" cy="12" r="10"/>' + 56 '<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>' + 57 '<path d="M12 17h.01"/></svg>'; 58 59 nodes.forEach(function (n, i) { 60 n.innerHTML = 61 '<div class="node__pills">' + 62 '<div class="pill pill--unknown"><span class="pill__icon">' + QMARK + '</span></div>' + 63 '<div class="pill pill--prompt">' + 64 '<span class="pill__icon pill__icon--ai"></span>' + 65 '<span>' + n.dataset.prompt + '</span>' + 66 '</div>' + 67 '</div>' + 68 '<div class="node__dot"></div>'; 69 70 n.__angle = parseFloat(n.dataset.angle); 71 n.__hit = false; 72 n.__delay = 120 + i * 160; 73 }); 74 75 if (still) { 76 nodes.forEach(function (n) { n.classList.add("is-live", "is-resolved"); }); 77 return; 78 } 79 80 var PERIOD = 9000; /* ~4.5s of sweep, ~4.5s to read the result */ 81 var CLEAR_AT = 0.88; 82 var start = null; 83 var cycle = -1; 84 var rafId = null; 85 var running = false; 86 87 function reset() { 88 radar.classList.add("is-instant"); 89 radar.classList.remove("is-clearing"); 90 nodes.forEach(function (n) { 91 n.classList.remove("is-live", "is-resolved"); 92 n.__hit = false; 93 }); 94 void radar.offsetWidth; 95 radar.classList.remove("is-instant"); 96 97 nodes.forEach(function (n) { 98 setTimeout(function () { n.classList.add("is-live"); }, n.__delay); 99 }); 100 101 hub.classList.remove("is-pulsing"); 102 void hub.offsetWidth; 103 hub.classList.add("is-pulsing"); 104 } 105 106 function frame(now) { 107 if (start === null) start = now; 108 109 var elapsed = now - start; 110 var t = (elapsed % PERIOD) / PERIOD; 111 var thisCycle = Math.floor(elapsed / PERIOD); 112 var angle = -90 + t * 360; 113 114 if (thisCycle !== cycle) { cycle = thisCycle; reset(); } 115 116 sweep.style.transform = "rotate(" + angle + "deg)"; 117
118 nodes.forEach(function (n) { 119 if (!n.__hit && angle >= n.__angle) { 120 n.__hit = true; 121 n.classList.add("is-resolved"); 122 } 123 }); 124 125 if (t > CLEAR_AT) radar.classList.add("is-clearing"); 126 127 rafId = requestAnimationFrame(frame); 128 } 129 130 /* ââ playback gate ââââââââââââââââââââââââââââââââââââââââââââââââââ 131 Left ungated, the sweep starts at page load and you arrive mid-cycle â 132 usually just in time to watch it clear. It now starts from zero once a 133 fifth of the radar is on screen, and rewinds when it leaves. */ 134 function play() { 135 if (running) return; 136 running = true; 137 start = null; 138 cycle = -1; 139 rafId = requestAnimationFrame(frame); 140 } 141 function stop() { 142 running = false; 143 if (rafId) cancelAnimationFrame(rafId); 144 rafId = null; 145 } 146 147 if ("IntersectionObserver" in window) { 148 new IntersectionObserver(function (entries) { 149 entries.forEach(function (e) { e.isIntersecting ? play() : stop(); }); 150 }, { threshold: 0.2 }).observe(radar); 151 } else { 152 play(); 153 } 154})(); 155 156 157/* ---------------------------------------------------------------- */ 158 159 160(function () { 161 var section = document.querySelector('.ll-bento'); 162 if (!section) return; 163 164 var reduce = window.matchMedia('(prefers-reduced-motion: reduce)'); 165 var cards = [].slice.call(section.querySelectorAll('.ll-bento__card')); 166 var gauge = section.querySelector('.ll-gauge__dial'); 167 var loop = section.dataset.loop !== 'false'; 168 169 /* offset-path is what keeps the chart's head glued to the leading edge. 170 Without it the head would drift off the curve, so hide it instead. */ 171 if (!(window.CSS && CSS.supports && CSS.supports('offset-path', 'path("M 0,0 L 1,1")'))) { 172 var head = section.querySelector('.ll-chart__head'); 173 if (head) head.style.display = 'none'; 174 } 175 176 /* ---------- counters + needle, driven off one clock so the number and 177 the dial can never disagree with each other ---------------------- */ 178 179 var easeOut = function (t) { return 1 - Math.pow(1 - t, 3); }; 180 181 function makeDriver(el, cycle, onFrame) { 182 var t0 = null, raf = null, live = false; 183 184 function frame(now) { 185 if (t0 === null) t0 = now; 186 var t = (now - t0) / cycle; 187 if (!loop && t >= 1) { onFrame(1); raf = null; return; } 188 onFrame(t % 1); 189 raf = requestAnimationFrame(frame); 190 } 191 return { 192 start: function () { if (live) return; live = true; t0 = null; raf = requestAnimationFrame(frame); }, 193 stop: function () { live = false; if (raf) cancelAnimationFrame(raf); raf = null; }, 194 reset: function () { 195 t0 = null; 196 if (live && !raf) raf = requestAnimationFrame(frame); 197 } 198 }; 199 } 200 201 /* 0 -> peak (eased), hold, then fall away so the loop can restart clean. 202 In play-once mode there is nothing to restart, so it rises and holds. */ 203 function phase(t) { 204 if (!loop) return easeOut(Math.min(1, t / .52)); 205 if (t < .52) return easeOut(t / .52); 206 if (t < .84) return 1; 207 return 1 - easeOut(Math.min(1, (t - .84) / .12)); 208 } 209 210 var drivers = {}; 211 212 /* --- the dial --- */ 213 if (gauge) { 214 var pctEl = section.querySelector('.ll-gauge__pct'); 215 var TARGET = 0.80; 216 drivers.gauge = makeDriver(gauge, parseFloat(getComputedStyle(section).getPropertyValue('--ll-gauge-loop')) * 1000 || 5800, 217 function (t) { 218 var p = phase(t) * TARGET; 219 gauge.style.setProperty('--p', p.toFixed(4)); 220 if (pctEl) pctEl.textContent = Math.round(p * 100) + '%'; 221 }); 222 } 223 224 /* --- the big number --- */ 225 var volVal = section.querySelector('[data-count-to="2.4"]'); 226 var volGlow = section.querySelector('.ll-vol__glow'); 227 if (volVal) { 228 drivers.volume = makeDriver(volVal, parseFloat(getComputedStyle(section).getPropertyValue('--ll-count-loop')) * 1000 || 6600, 229 function (t) { 230 var txt = (phase(t) * 2.4).toFixed(1) + 'M'; 231 volVal.textContent = txt; 232 if (volGlow) volGlow.textContent = txt; 233 }); 234 } 235 236 function play() { for (var k in drivers) drivers[k].start(); } 237 function pause() { for (var k in drivers) drivers[k].stop(); } 238 239 /* ---------- in view -> run. out of view -> stop and rewind ---------- */ 240 241 if (reduce.matches) { 242 section.classList.add('is-live'); 243 if (gauge) gauge.style.setProperty('--p', '0.8'); 244 var p1 = section.querySelector('.ll-gauge__pct'); if (p1) p1.textContent = '80%'; 245 if (volVal) { volVal.textContent = '2.4M'; if (volGlow) volGlow.textContent = '2.4M'; } 246 return; 247 } 248 249 var io = new IntersectionObserver(function (entries) {
250 entries.forEach(function (e) { 251 if (e.isIntersecting) { section.classList.add('is-live'); play(); } 252 else { section.classList.remove('is-live'); pause(); } 253 }); 254 }, { threshold: .18 }); 255 io.observe(section); 256 257 /* The cards used to replay from the top on hover. They loop on their own, so 258 that only ever fought the loop already running â removed. */ 259}()); 260 261 262/* ---------------------------------------------------------------- */ 263 264 265(function(){ 266 var flow = document.querySelector('.wf .flow'); 267 var stage = document.querySelector('.wf .flow-stage'); 268 if(!flow || !stage) return; 269 270 var $ = function(id){ return document.getElementById(id); }; 271 var reduced = window.matchMedia('(prefers-reduced-motion: reduce)'); 272 273 /* ---------- fit the fixed-geometry diagram to its column ---------- */ 274 var FLOW_W = 550.314, FLOW_H = 474.686; 275 function fit(){ 276 var stacked = window.matchMedia('(max-width:1179px)').matches; 277 if(!stacked){ 278 flow.style.setProperty('--fs', 1); 279 stage.style.height = ''; 280 return; 281 } 282 var s = Math.min(1, stage.clientWidth / FLOW_W); 283 flow.style.setProperty('--fs', s); 284 stage.style.height = (FLOW_H * s) + 'px'; 285 } 286 fit(); 287 if(window.ResizeObserver){ 288 /* fit() writes stage.style.height. Observing document.body meant that 289 write resized the observed element, and Chrome reports the re-entry as 290 "ResizeObserver loop completed with undelivered notifications". 291 Watching the stage's parent and deferring the write by one frame 292 breaks the cycle. */ 293 var fitQueued = false; 294 var ro = new ResizeObserver(function(){ 295 if (fitQueued) return; 296 fitQueued = true; 297 requestAnimationFrame(function(){ fitQueued = false; fit(); }); 298 }); 299 ro.observe(stage.parentNode || stage); 300 } 301 window.addEventListener('resize', fit); 302 303 /* ---------- the run ---------- */ 304 var NODES = ['n1','n2','n3','n4','n5']; 305 var CONNS = ['c1','c2','c4']; 306 var timers = []; 307 308 function clear(){ timers.forEach(clearTimeout); timers = []; } 309 function at(ms, fn){ timers.push(setTimeout(fn, ms)); } 310 311 function set(id, state){ $(id).dataset.state = state; } 312 function fire(nodeId, connId){
313 $(nodeId).dataset.fired = 'true'; 314 $(connId).classList.add('is-on'); 315 } 316 317 function reset(){ 318 flow.classList.add('is-resetting'); 319 NODES.forEach(function(id){ 320 var n = $(id); 321 n.dataset.state = 'idle'; 322 n.dataset.fired = 'false'; 323 }); 324 CONNS.forEach(function(id){ $(id).classList.remove('is-on'); }); 325 void flow.offsetWidth; /* commit the snap */ 326 flow.classList.remove('is-resetting'); 327 } 328 329 /* durations, ms */ 330 var RUN = 1250, RUN_LONG = 1650, RUN_END = 1150, 331 DRAW = 750, DRAW_BRANCH = 950, HOLD = 2000; 332 333 function play(){ 334 clear(); 335 reset(); 336 337 var t = 350; 338 339 at(t, function(){ set('n1','running'); }); 340 t += RUN; 341 at(t, function(){ set('n1','done'); fire('n1','c1'); }); 342 t += DRAW; 343 344 at(t, function(){ set('n2','running'); }); 345 t += RUN; 346 at(t, function(){ set('n2','done'); fire('n2','c2'); }); 347 t += DRAW; 348 349 at(t, function(){ set('n3','running'); }); 350 t += RUN_LONG; 351 at(t, function(){ set('n3','done'); fire('n3','c4'); }); 352 t += DRAW_BRANCH; 353 354 at(t, function(){ set('n4','running'); }); 355 t += RUN_END; 356 at(t, function(){ set('n4','done'); }); 357 t += HOLD; 358 359 at(t, play); 360 } 361 362 /* the design's resting state, for reduced motion */ 363 function still(){ 364 clear(); 365 set('n1','done'); $('n1').dataset.fired = 'true'; $('c1').classList.add('is-on'); 366 set('n2','done'); $('n2').dataset.fired = 'true'; $('c2').classList.add('is-on'); 367 set('n3','running'); 368 $('n3').dataset.fired = 'true'; $('c4').classList.add('is-on'); 369 } 370 371 function start(){ reduced.matches ? still() : play(); } 372 if(reduced.addEventListener) reduced.addEventListener('change', start); 373 374 /* only run while on screen */ 375 if(window.IntersectionObserver){ 376 new IntersectionObserver(function(entries){ 377 entries.forEach(function(e){ e.isIntersecting ? start() : clear(); }); 378 }, {threshold:.15}).observe(flow); 379 } else { 380 start(); 381 } 382})(); 383 384 385/* ---------------------------------------------------------------- */ 386 387 388(function () { 389 var NS = 'http://www.w3.org/2000/svg'; 390 var W = 1920, H = 882, X_END = 1918, BAR_W = 2; 391 392 /* The curve, straight out of Figma (node 9377:5979 vectorPaths). */ 393 var C = [[0, 811.5], [595, 789], [1460.5, 811.5], [X_END, 5.5]]; 394 395 function bez(t, a, b, c, d) { 396 var m = 1 - t; 397 return m*m*m*a + 3*m*m*t*b + 3*m*t*t*c + t*t*t*d; 398 } 399 400 /* x(t) is monotonic on this curve, so a bisection is exact enough and cheap. */ 401 function yAtX(x) { 402 var lo = 0, hi = 1, t = 0.5; 403 for (var i = 0; i < 22; i++) { 404 t = (lo + hi) / 2; 405 if (bez(t, C[0][0], C[1][0], C[2][0], C[3][0]) < x) lo = t; else hi = t; 406 } 407 return bez(t, C[0][1], C[1][1], C[2][1], C[3][1]); 408 } 409 410 function buildBars(section) { 411 var g = section.querySelector('[data-ll-bars]'); 412 if (!g) return; 413 414 var cs = getComputedStyle(section); 415 var pitch = parseFloat(cs.getPropertyValue('--ll-bar-pitch')) || 10; 416 var lead = parseFloat(cs.getPropertyValue('--ll-lead')) || 0.68; 417 418 if (g.dataset.pitch === String(pitch)) return; // nothing changed 419 g.dataset.pitch = String(pitch); 420 421 var frag = document.createDocumentFragment(); 422 for (var x = 0; x <= X_END; x += pitch) { 423 var y = yAtX(x); 424 var r = document.createElementNS(NS, 'rect'); 425 r.setAttribute('class', 'll-bar'); 426 r.setAttribute('x', x); 427 r.setAttribute('y', y.toFixed(2)); 428 r.setAttribute('width', BAR_W); 429 r.setAttribute('height', (H - y).toFixed(2)); 430 r.setAttribute('fill', 'url(#llBarGrad)'); 431 r.style.setProperty('--ll-d', (x / W * lead).toFixed(4)); 432 frag.appendChild(r); 433 } 434 435 g.textContent = ''; 436 g.appendChild(frag); 437 438 var stat = section.querySelector('[data-ll-static]'); 439 if (stat) stat.remove(); 440 } 441 442 var sections = document.querySelectorAll('[data-ll-growth]'); 443 if (!sections.length) return; 444 445 var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; 446 447 Array.prototype.forEach.call(sections, function (section) { 448 buildBars(section); 449 450 var rt; 451 addEventListener('resize', function () { 452 clearTimeout(rt); 453 rt = setTimeout(function () { buildBars(section); }, 200); 454 }); 455 456 if (reduce) { section.classList.add('is-revealed'); return; } 457 458 if (section.getAttribute('data-ll-growth') === 'scrub') { 459 var ticking = false;
460 461 var update = function () { 462 ticking = false; 463 var r = section.getBoundingClientRect(); 464 var vh = window.innerHeight || document.documentElement.clientHeight; 465 var p = (vh * 0.9 - r.top) / (vh * 0.75); 466 p = Math.max(0, Math.min(1, p)); 467 section.style.setProperty('--ll-p', p.toFixed(4)); 468 section.classList.toggle('is-revealed', p > 0.02); 469 }; 470 471 var onScroll = function () { 472 if (!ticking) { ticking = true; requestAnimationFrame(update); } 473 }; 474 475 addEventListener('scroll', onScroll, { passive: true }); 476 addEventListener('resize', onScroll); 477 update(); 478 return; 479 } 480 481 if (!('IntersectionObserver' in window)) { 482 section.classList.add('is-revealed'); 483 return; 484 } 485 486 var io = new IntersectionObserver(function (entries) { 487 entries.forEach(function (entry) { 488 if (!entry.isIntersecting) return; 489 entry.target.classList.add('is-revealed'); 490 io.unobserve(entry.target); 491 }); 492 }, { threshold: 0.2 }); 493 494 io.observe(section); 495 }); 496})(); 497 498 499/* ---------------------------------------------------------------- */ 500 501 502(function () { 503 var section = document.querySelector('[data-ll-mcp]'); 504 var field = section && section.querySelector('[data-mcp-stars]'); 505 if (!section || !field) return; 506 507 /* Starfield. One star per ~3600px², capped, so density holds as the card 508 resizes. Sizes are deliberately sub-pixel-ish: most land 0.7â1.6px. */ 509 var built = 0; 510 511 function build() { 512 var r = section.getBoundingClientRect(); 513 if (!r.width) return; 514 515 var n = Math.max(50, Math.min(190, Math.round((r.width * r.height) / 3600))); 516 if (n === built) return; 517 built = n; 518 519 var frag = document.createDocumentFragment(); 520 521 for (var i = 0; i < n; i++) { 522 var glint = i % 22 === 7; // ~1 in 22 gets a cross 523 var s = glint 524 ? 5 + Math.random() * 4 // 5â9px glints 525 : 0.7 + Math.pow(Math.random(), 2.2) * 1.5; // biased small 526 527 var el = document.createElement('span'); 528 el.className = 'll-mcp__star' + (glint ? ' ll-mcp__star--glint' : ''); 529 el.style.left = (Math.random() * 100).toFixed(3) + '%'; 530 el.style.top = (Math.random() * 100).toFixed(3) + '%'; 531 el.style.width = s.toFixed(2) + 'px'; 532 el.style.height = s.toFixed(2) + 'px'; 533 el.style.setProperty('--a', (glint ? 0.5 + Math.random() * 0.4 534 : 0.3 + Math.random() * 0.7).toFixed(3)); 535 el.style.setProperty('--d', (glint ? 2.6 + Math.random() * 3.4 536 : 1.3 + Math.random() * 2.9).toFixed(2) + 's'); 537 // negative delay => every star starts mid-cycle, nothing blinks in unison 538 el.style.setProperty('--delay', (-Math.random() * 6).toFixed(2) + 's'); 539 frag.appendChild(el); 540 } 541 542 field.replaceChildren(frag); 543 } 544 545 build(); 546 547 var t; 548 addEventListener('resize', function () { 549 clearTimeout(t); 550 t = setTimeout(build, 220); 551 }, { passive: true }); 552 553 /* Only run the transmission while the section is on screen. */ 554 if ('IntersectionObserver' in window) { 555 new IntersectionObserver(function (entries) { 556 entries.forEach(function (e) { 557 section.classList.toggle('is-live', e.isIntersecting); 558 }); 559 }, { threshold: 0.12 }).observe(section); 560 } else { 561 section.classList.add('is-live'); 562 } 563})(); 564 565 566/* ---------------------------------------------------------------- */
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.