1/** 2 * page_overlay.js â client-side navigation over the live sky. 3 * 4 * The content pages (/what-we-do, /pricing, /privacy, /eula, /for/*) 5 * exist as real standalone documents â ad landings, app-store legal 6 * links, and crawlers get fast static pages with the CSS sky. But when 7 * a visitor navigates to one FROM the homepage, a full navigation 8 * would tear down the WebGL renderer and reboot the whole simulation. 9 * Instead, this module intercepts the click, fetches the target page, 10 * and injects its nav + main + footer into #page-layer, which scrolls 11 * over the still-running sky. history.pushState keeps the URL real, so 12 * deep links, reloads, and the back button all behave: a reload of 13 * /pricing simply serves the static page. 14 * 15 * The injected markup is styled by pages.css, whose rules are all 16 * scoped under .page-shell â #page-layer carries that class, so the 17 * content styles apply inside the layer and nothing leaks into the 18 * hero. The standalone-only ground (static sky, grain) hangs off 19 * body.page-shell and never comes along. 20 */ 21(function () { 22 'use strict'; 23 24 // Paths eligible for overlay navigation. Anything else (mailto:, 25 // external, /delete-account, unknown) falls through to a normal 26 // navigation. 27 var ROUTE_RE = /^\/(what-we-do|pricing|privacy|eula|contact|thanks|status|for\/[a-z0-9-]+)$/; 28 29 // Fragments worth transplanting from a fetched document, in render 30 // order. The skip-link is deliberately left behind â the layer keeps 31 // its own focus management. 32 var PARTS = ['nav.site-nav', 'main.page', 'footer.site-footer']; 33 34 var HOME_TITLE = document.title; 35 // This router runs on the homepage AND on the content pages (which 36 // boot the full homepage simulation stack). Only the homepage has a 37 // bare-sky state to return to. On a content page, the first click to 38 // "/" INSTALLS the homepage into this document â hero markup fetched 39 // from "/", style.css attached â so the running sky carries on 40 // instead of rebooting through a navigation. After that the page 41 // behaves exactly like the homepage. 42 var IS_HOME = !!document.querySelector('.hero-content'); 43 var installing = false; 44 var layer = null; 45 var cache = {}; // path -> { title, html } 46 var isOpen = false; 47 var loadGeneration = 0; // invalidates slower responses from older routes 48 49 function ensureLayer() { 50 if (!layer) layer = document.getElementById('page-layer'); 51 return layer; 52 } 53 54 function currentPath() { 55 var p = window.location.pathname.replace(/\/+$/, ''); 56 return p === '' ? '/' : p; 57 } 58 59 function render(path, entry) { 60 var l = ensureLayer(); 61 if (!l) { window.location.href = path; return; } 62 l.innerHTML = entry.html; 63 ensureTurnstile(l); 64 ensureStatus(l); 65 document.title = entry.title; 66 l.hidden = false; 67 l.scrollTop = 0; 68 document.body.classList.add('page-open'); 69 window._pageOverlayOpen = true; // divider_halo.js zeroes the shader halo 70 isOpen = true; 71 // Fade the incoming page in on EVERY route change, not just the 72 // first open. (When the layer carried a 30% scrim this re-fade 73 // blanked the whole viewport and read like a reload; scrimless, 74 // it's just the content fading in over the visible sky.)
75 l.style.animation = 'none'; 76 void l.offsetWidth; 77 l.style.animation = ''; 78 // Hand focus to the layer so keyboard/screen-reader users land on 79 // the new content, mirroring a real navigation. 80 l.focus({ preventScroll: true }); 81 } 82 83 function close(restoreTitle) { 84 loadGeneration++; 85 if (!isOpen) return; 86 var l = ensureLayer(); 87 if (l) { l.hidden = true; l.innerHTML = ''; } 88 if (window._statusPage) window._statusPage.unmount(); 89 document.body.classList.remove('page-open'); 90 window._pageOverlayOpen = false; 91 if (restoreTitle !== false) document.title = HOME_TITLE; 92 isOpen = false; 93 } 94 95 function extract(doc) { 96 var html = ''; 97 for (var i = 0; i < PARTS.length; i++) { 98 var el = doc.querySelector(PARTS[i]); 99 if (el) html += el.outerHTML; 100 } 101 return { title: doc.title || HOME_TITLE, html: html }; 102 } 103 104 function load(path, push) { 105 var generation = ++loadGeneration; 106 if (push) history.pushState({ slwOverlay: path }, '', path); 107 if (cache[path]) { render(path, cache[path]); return; } 108 109 // Extensionless first (Cloudflare Pages serves /pricing); fall back 110 // to the .html file so the router also works on dumb static servers 111 // (local python -m http.server previews). 112 fetch(path, { credentials: 'same-origin' }) 113 .then(function (res) { 114 if (res.ok) return res.text(); 115 return fetch(path + '.html', { credentials: 'same-origin' }) 116 .then(function (res2) { 117 if (!res2.ok) throw new Error('fetch failed: ' + path); 118 return res2.text(); 119 }); 120 }) 121 .then(function (text) { 122 // A later navigation won the race. Do not let this older response 123 // replace its markup/title or turn a Back action into a forward jump. 124 if (generation !== loadGeneration || currentPath() !== path) return; 125 var doc = new DOMParser().parseFromString(text, 'text/html'); 126 var entry = extract(doc); 127 if (!entry.html) throw new Error('no content: ' + path); 128 cache[path] = entry; 129 render(path, entry); 130 }) 131 .catch(function () { 132 if (generation !== loadGeneration || currentPath() !== path) return; 133 // Anything went wrong: do the navigation for real. 134 window.location.href = path; 135 }); 136 } 137 138 // ========================================================================== 139 // HOME INSTALLATION (content pages only) 140 // ========================================================================== 141 142 // The deploy stamps every script URL with ?v=<sha>. Dynamic loads 143 // must carry the same stamp or they'd fetch unversioned (cacheable- 144 // stale) copies and mix builds. 145 function assetVersion() { 146 var s = document.querySelector('script[src*="page_overlay.js"]'); 147 var m = s && s.getAttribute('src').match(/\?v=[^&]*/); 148 return m ? m[0] : ''; 149 } 150 151 // Replica of index.html's head handshake: the shingled wordmark stays 152 // hidden until Yellowtail is loaded AND the boot brand state is 153 // settled (html.wm-ready). Content pages don't carry the inline 154 // original; by install time secret.js (loaded at page boot) has 155 // already settled the brand, so bootOk starts true. 3s backstop. 156 function setupWordmarkHandshake() { 157 var fontOk = false, bootOk = !!window._secretBooted, done = false; 158 function maybe() { 159 if (!done && fontOk && bootOk) { 160 done = true; 161 document.documentElement.classList.add('wm-ready'); 162 } 163 } 164 function fontReady() { fontOk = true; maybe(); } 165 window._wmBootReady = function () { bootOk = true; maybe(); }; 166 if (document.fonts && document.fonts.load) { 167 document.fonts.load('1em Yellowtail').then(function (faces) { 168 if (faces.length) fontReady(); else document.fonts.ready.then(fontReady); 169 }).catch(fontReady); 170 document.fonts.ready.then(fontReady); 171 } else { 172 fontOk = true; 173 } 174 setTimeout(function () { fontOk = true; bootOk = true; maybe(); }, 3000); 175 } 176 177 function installHome() { 178 installing = true; 179 return fetch('/', { credentials: 'same-origin' }) 180 .then(function (res) { 181 if (!res.ok) throw new Error('home fetch failed'); 182 return res.text(); 183 }) 184 .then(function (text) { 185 var doc = new DOMParser().parseFromString(text, 'text/html'); 186 var hero = doc.querySelector('main.hero'); 187 var foot = doc.querySelector('body > footer.site-footer'); 188 if (!hero || !foot) throw new Error('home extract failed'); 189 190 // The sky-page stylesheet. Its viewport pinning (fixed body, 191 // overflow hidden) is exactly what the home view needs; the 192 // overlay keeps scrolling internally. 193 var link = document.createElement('link'); 194 link.rel = 'stylesheet'; 195 link.href = '/style.css' + assetVersion(); 196 document.head.appendChild(link); 197 198 // Retire this page's static article chrome permanently â 199 // REMOVED, not hidden, so module boot querySelectors (logo_fade 200 // grabs the first .site-footer) resolve to the home elements. 201 // All further navigation happens in #page-layer. 202 var l = ensureLayer(); 203 var chrome = document.querySelectorAll( 204 'body >
204 .skip-link, body > .site-nav, body > main, body > .site-footer'); 205 for (var i = 0; i < chrome.length; i++) { 206 chrome[i].parentNode.removeChild(chrome[i]); 207 } 208 // Drop the content-page body styling (static sky ground, page 209 // grain, the sky-live boot cover) â style.css owns body now. 210 document.body.classList.remove('page-shell'); 211 212 document.body.insertBefore(document.importNode(hero, true), l); 213 document.body.insertBefore(document.importNode(foot, true), l); 214 HOME_TITLE = doc.title || HOME_TITLE; 215 setupWordmarkHandshake(); 216 217 // Pinned viewport needs the homepage's iOS touch blocker. 218 document.addEventListener('touchmove', function (e) { 219 if (!e.target.closest || 220 !(e.target.closest('#secret-panel') || e.target.closest('#page-layer'))) { 221 e.preventDefault(); 222 } 223 }, { passive: false }); 224 225 // Every module is already running (content pages boot the full 226 // homepage stack) â logo_fade just needs to re-resolve its 227 // managed elements, since the footer it grabbed at boot was 228 // removed above and the hero didn't exist yet. divider_halo 229 // re-queries per frame on its own; secret.js's footer knock is 230 // document-delegated. 231 if (window._logoFade && window._logoFade.rescan) { 232 window._logoFade.rescan(); 233 } 234 235 IS_HOME = true; 236 installing = false; 237 }); 238 } 239 240 function goHome() { 241 if (installing) return; 242 history.pushState({}, '', '/'); 243 installHome() 244 .then(function () { 245 if (isOpen) close(); 246 else document.title = HOME_TITLE; 247 }) 248 .catch(function () { 249 // Install failed â fall back to the real navigation (the URL 250 // is already "/", so this is a plain load of the homepage). 251 window.location.href = '/'; 252 }); 253 } 254 255 document.addEventListener('click', function (e) { 256 if (e.defaultPrevented || e.button !== 0 || 257 e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return; 258 if (!e.target.closest) return; 259 var a = e.target.closest('a[href]'); 260 if (!a || a.target || a.hasAttribute('download')) return; 261 if (a.origin !== window.location.origin) return; 262 263 var path = a.pathname.replace(/\/+$/, ''); 264 if (path === '' || path === '/index.html') path = '/'; 265 266 // Brand link / home: drop back to the bare sky. On a content page 267 // the first trip installs the homepage into this document so the 268 // sky carries on (see installHome). 269 if (path === '/') { 270 if (!IS_HOME) { 271 e.preventDefault(); 272 goHome(); 273 return; 274 } 275 if (!isOpen) return; 276 e.preventDefault(); 277 history.pushState({}, '', '/'); 278 close(); 279 return; 280 } 281 282 if (!ROUTE_RE.test(path)) return; 283 e.preventDefault(); 284 285 // Same-page fragment link inside the open overlay: just scroll. 286 if (isOpen && path === currentPath() && a.hash) { 287 var target = ensureLayer().querySelector(a.hash); 288 if (target && target.scrollIntoView) target.scrollIntoView(); 289 return; 290 } 291 292 load(path, true); 293 }); 294 295 window.addEventListener('popstate', function () { 296 var path = currentPath(); 297 if (ROUTE_RE.test(path)) load(path, false); 298 else close(); 299 }); 300 301 // Turnstile for injected forms: api.js auto-renders widgets present 302 // at ITS load time, but a .cf-turnstile div arriving later via 303 // innerHTML needs an explicit turnstile.render(). Tokens are 304 // single-use, so failed submits reset the widget for a retry. 305 var turnstileApi = null; 306 function ensureTurnstile(root) { 307 var els = root.querySelectorAll('.cf-turnstile:empty'); 308 if (!els.length) return; 309 if (!turnstileApi) { 310 turnstileApi = new Promise(function (resolve, reject) { 311 if (window.turnstile) { resolve(); return; } 312 var s = document.createElement('script'); 313 s.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; 314 s.onload = resolve; 315 s.onerror = reject; 316 document.head.appendChild(s); 317 }); 318 } 319 turnstileApi.then(function () { 320 for (var i = 0; i < els.length; i++) { 321 if (!els[i].hasChildNodes()) { 322 // Keep the ID so retries reset this form's widget, not whichever 323 // Turnstile instance happens to be first in the document. 324 els[i]._turnstileWidgetId = window.turnstile.render(els[i]); 325 } 326 } 327 }).catch(function () { /* widget absent; the server will refuse */ }); 328 } 329 330 // /status renders from /api/status via js/status_page.js â a module 331 // rather than an inline script, because inline scripts in injected 332 // HTML never run. status.html loads it statically; every other page 333 // fetches it here on first use (deploy-stamped, like the home 334 // install's dynamic loads). mount() on a root without status markup 335 // is a no-op that also stops any previous route's poll loop. 336 var statusApi = null; 337 function ensureStatus(root) { 338 if (window._statusPage) { window._statusPage.mount(root); return; } 339 if (!root.querySelector('#status-components')) return; 340 if (!statusApi) { 341 statusApi = new Promise(function (resolve, reject) { 342 var s = document.createElement('script');
343 s.src = '/js/status_page.js' + assetVersion(); 344 s.onload = resolve; 345 s.onerror = reject; 346 document.head.appendChild(s); 347 }); 348 } 349 statusApi.then(function () { 350 // Still showing the same content? (A fast second click could 351 // have moved on; that render's own mount call wins.) 352 if (window._statusPage && root.querySelector('#status-components')) { 353 window._statusPage.mount(root); 354 } 355 }).catch(function () { /* headline stays "Checkingâ¦"; reload recovers */ }); 356 } 357 358 // Phone mask: (xxx) xxx-xxxx as you type, 10 digits max. Delegated so 359 // it works on the static page and injected into the layer alike. 360 document.addEventListener('input', function (e) { 361 var t = e.target; 362 if (!t || t.id !== 'cf-phone') return; 363 var d = t.value.replace(/\D/g, '').slice(0, 10); 364 if (d.length > 6) t.value = '(' + d.slice(0, 3) + ') ' + d.slice(3, 6) + '-' + d.slice(6); 365 else if (d.length > 3) t.value = '(' + d.slice(0, 3) + ') ' + d.slice(3); 366 else if (d.length > 0) t.value = '(' + d; 367 else t.value = ''; 368 }); 369 370 // Marketing tags: a link posted somewhere carries utm_source / 371 // utm_medium / utm_campaign. Remember them for the session so a visitor 372 // who lands on the homepage and finds the contact page later still 373 // arrives labelled with the post that brought them. 374 var UTM_KEYS = ['utm_source', 'utm_medium', 'utm_campaign']; 375 var UTM_STORE = 'sw_utm'; 376 function rememberUtm() { 377 try { 378 var params = new URLSearchParams(location.search); 379 var found = {}; 380 var any = false; 381 UTM_KEYS.forEach(function (key) { 382 var value = (params.get(key) || '').trim().slice(0, 100); 383 if (value) { found[key] = value; any = true; } 384 }); 385 if (any) { 386 sessionStorage.setItem(UTM_STORE, JSON.stringify(found)); 387 reportArrival(found); 388 } 389 } catch (e) { /* storage unavailable: the tags just don't carry */ } 390 } 391 // Tell the pipeline a tagged link brought someone: once per set of tags 392 // per session, the tags and the page, nothing about the person. 393 function reportArrival(found) { 394 try { 395 var stamp = JSON.stringify(found); 396 if (sessionStorage.getItem('sw_utm_reported') === stamp) return; 397 sessionStorage.setItem('sw_utm_reported', stamp); 398 } catch (e) { /* fine: at worst it is reported twice */ } 399 var body = JSON.stringify({ 400 utm_source: found.utm_source || '', 401 utm_medium: found.utm_medium || '', 402 utm_campaign: found.utm_campaign || '', 403 page: location.pathname 404 }); 405 try { 406 if (navigator.sendBeacon) { 407 navigator.sendBeacon('/api/arrival', new Blob([body], { type: 'application/json' })); 408 } else { 409 fetch('/api/arrival', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: body, keepalive: true }).catch(function () {}); 410 } 411 } catch (e) { /* fine */ } 412 } 413 function storedUtm() { 414 try { return JSON.parse(sessionStorage.getItem(UTM_STORE) || '{}') || {}; } 415 catch (e) { return {}; } 416 } 417 rememberUtm(); 418 419 // "Other" under Trade, license jurisdiction, and referral source reveals a 420 // text box. Scoped to the select's own form: when the contact page is 421 // opened over a directly loaded one, both documents' forms are present. 422 var OTHER_WRAPS = { 'cf-trade': 'cf-trade-other-wrap', 'cf-source': 'cf-source-other-wrap', 'cf-license-state': 'cf-license-state-other-wrap' }; 423 document.addEventListener('change', function (e) { 424 var t = e.target; 425 if (!t || !OTHER_WRAPS[t.id]) return; 426 var wrap = t.form && t.form.querySelector('[id="' + OTHER_WRAPS[t.id] + '"]'); 427 if (wrap) wrap.hidden = t.value !== 'Other'; 428 }); 429 430 // Contact form: document-level delegation so it works both on the 431 // static /contact page and when that page is injected into the layer 432 // (inline scripts don't execute on injected HTML). 433 document.addEventListener('submit', function (e) { 434 var form = e.target; 435 if (!form || form.id !== 'contact-form') return; 436 e.preventDefault(); 437 438 var button = form.querySelector('button[type="submit"]'); 439 var errorNote = form.querySelector('.form-error'); 440 var done = form.parentNode.querySelector('.contact-done'); 441 var data = {}; 442 var fd = new FormData(form); 443 fd.forEach(function (v, k) { data[k] = v; }); 444 var utm = storedUtm();
445 UTM_KEYS.forEach(function (key) { if (!data[key] && utm[key]) data[key] = utm[key]; }); 446 447 if (button) { button.disabled = true; button.textContent = 'Sendingâ¦'; } 448 if (errorNote) errorNote.hidden = true; 449 450 fetch('/api/contact', { 451 method: 'POST', 452 headers: { 'Content-Type': 'application/json' }, 453 credentials: 'same-origin', 454 body: JSON.stringify(data) 455 }) 456 .then(function (res) { return res.json(); }) 457 .then(function (out) { 458 if (!out.ok) throw new Error(out.error || 'failed'); 459 // A page of its own, not a message in place: it reads as done, a 460 // second click cannot resend, and /thanks is the address ad and 461 // analytics tools count as a conversion. Routed like any other 462 // page so the sky carries on underneath. 463 form.hidden = true; 464 if (done) done.hidden = false; 465 load('/thanks', true); 466 }) 467 .catch(function (err) { 468 if (button) { button.disabled = false; button.textContent = 'Send it'; } 469 if (errorNote) { 470 errorNote.textContent = (err && err.message && err.message !== 'failed') 471 ? err.message 472 : 'That didnât go through â give it another try, or come back in a minute.'; 473 errorNote.hidden = false; 474 } 475 // Turnstile tokens are single-use; arm a fresh one for retry. 476 if (window.turnstile && window.turnstile.reset) { 477 try { 478 var widget = form.querySelector('.cf-turnstile'); 479 var widgetId = widget && widget._turnstileWidgetId; 480 // Direct page loads are auto-rendered and do not expose an ID; 481 // there is only one protected surface, so the fallback is safe. 482 if (widgetId != null) window.turnstile.reset(widgetId); 483 else window.turnstile.reset(); 484 } catch (e2) { /* fine */ } 485 } 486 }); 487 }); 488 489 // Esc returns to the sky (unless the secret panel is up â its own 490 // shortcuts own the keyboard while open). 491 document.addEventListener('keydown', function (e) { 492 if (e.key !== 'Escape' || !isOpen || !IS_HOME) return; 493 var sp = document.getElementById('secret-panel'); 494 if (sp && !sp.classList.contains('hidden')) return; 495 history.pushState({}, '', '/'); 496 close(); 497 }); 498})();
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.