1(function () { 2 var phaseTwoHrefs = ['gear-shop.html', 'classes.html', 'mobile-app.html']; 3 4 function textNodes(root) { 5 var nodes = []; 6 var walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, { 7 acceptNode: function (node) { 8 if (!node.nodeValue || !node.nodeValue.trim()) return NodeFilter.FILTER_REJECT; 9 var parent = node.parentElement; 10 if (!parent || /^(SCRIPT|STYLE|NOSCRIPT|TEXTAREA)$/i.test(parent.tagName)) return NodeFilter.FILTER_REJECT; 11 return NodeFilter.FILTER_ACCEPT; 12 } 13 }); 14 while (walker.nextNode()) nodes.push(walker.currentNode); 15 return nodes; 16 } 17 18 function cleanupCopy() { 19 textNodes(document.body).forEach(function (node) { 20 node.nodeValue = node.nodeValue 21 .replace(/\bClub16\b/g, 'Club16') 22 .replace(/\b13\.5K\+\b/g, '13,500+') 23 .replace(/\b13\.5k\+\b/g, '13,500+') 24 .replace(/\s+\./g, '.'); 25 }); 26 } 27 28 function hidePhaseTwoLinks() { 29 document.querySelectorAll('a[href]').forEach(function (link) { 30 var href = link.getAttribute('href') || ''; 31 if (phaseTwoHrefs.some(function (item) { return href.indexOf(item) !== -1; })) { 32 link.style.display = 'none'; 33 link.setAttribute('aria-hidden', 'true'); 34 } 35 }); 36 } 37 38 function setupHomeHeroPlaylist() { 39 /* Hero rotation is owned by hero-video-playlist.js (crossfade compilation). */ 40 } 41 42 var attributionKeys = [ 43 'utm_source', 44 'utm_medium', 45 'utm_campaign', 46 'utm_term', 47 'utm_content', 48 'gclid', 49 'fbclid', 50 'msclkid' 51 ]; 52 53 function readStoredAttribution() { 54 try { 55 return JSON.parse(sessionStorage.getItem('club16.attribution') || '{}') || {}; 56 } catch (_) { 57 return {}; 58 } 59 } 60 61 function readAttribution() { 62 var attribution = readStoredAttribution(); 63 var params = new URLSearchParams(window.location.search); 64 65 attributionKeys.forEach(function (key) { 66 var value = params.get(key); 67 if (value) attribution[key] = value; 68 }); 69 70 if (!attribution.landing_page) attribution.landing_page = window.location.pathname; 71 if (document.referrer && document.referrer.indexOf(window.location.origin) !== 0) { 72 attribution.referrer = document.referrer; 73 } 74 75 return attribution; 76 } 77 78 function setupAttributionCapture() { 79 var attribution = readAttribution(); 80 var hasMarketingValue = attributionKeys.some(function (key) { return Boolean(attribution[key]); }) || Boolean(attribution.referrer); 81 if (!hasMarketingValue) return; 82 83 try { 84 sessionStorage.setItem('club16.attribution', JSON.stringify(attribution)); 85 } catch (_) {} 86 } 87 88 function showGuestPassMessage(card, message, tone) { 89 var existing = card.querySelector('.form-alert'); 90 if (!existing) { 91 existing = document.createElement('div'); 92 existing.className = 'form-alert'; 93 existing.setAttribute('aria-live', 'polite'); 94 var button = card.querySelector('.btn-form'); 95 if (button) button.insertAdjacentElement('beforebegin', existing); 96 else card.appendChild(existing); 97 } 98 99 existing.className = 'form-alert form-alert--' + (tone || 'error'); 100 existing.textContent = message; 101 } 102 103 function normalizeGuestPassConsent(card) { 104 if (card.querySelector('input[name="email_consent"]') && card.querySelector('input[name="sms_consent"]')) return; 105 106 var oldSms = card.querySelector('#sms'); 107 var oldSmsWrap = oldSms ? oldSms.closest('.form-checkbox') : null; 108 var oldConsent = card.querySelector('.form-consent'); 109 var button = card.querySelector('.btn-form'); 110 var target = oldSmsWrap || oldConsent || button; 111 if (!target) return; 112 113 var emailConsent = document.createElement('div'); 114 emailConsent.className = 'form-checkbox form-checkbox--consent'; 115 emailConsent.innerHTML = '<input type="checkbox" id="email_consent" name="email_consent" value="1"><label for="email_consent">I agree to receive marketing emails from Club16 Trevor Linden Fitness regarding membership, club updates, promotions, events, and wellness-related information. Your consent is voluntary and is not required to purchase products or services. I can unsubscribe anytime.</label>'; 116 117 var smsConsent = document.createElement('div'); 118 smsConsent.className = 'form-checkbox form-checkbox--consent'; 119 smsConsent.innerHTML = '<input type="checkbox" id="sms_consent" name="sms_consent" value="1"><label for="sms_consent">
119I agree to receive recurring automated marketing text messages (promotions, offers, and events) from Club16 Trevor Linden Fitness at the mobile number provided. Your consent is voluntary and is not required to purchase products or services. Up to 4 messages per month. Standard message and data rates may apply. Reply STOP to unsubscribe or HELP for assistance.</label>'; 120 121 target.insertAdjacentElement('beforebegin', emailConsent); 122 emailConsent.insertAdjacentElement('afterend', smsConsent); 123 if (oldSmsWrap) oldSmsWrap.remove(); 124 if (oldConsent) oldConsent.remove(); 125 } 126 127 function setupGuestPassSubmit() { 128 if (!document.body.classList.contains('guest-pass-page')) return; 129 130 var card = document.getElementById('guest-pass-form'); 131 if (!card) return; 132 133 normalizeGuestPassConsent(card); 134 135 var fields = { 136 name: card.querySelector('input[aria-label="Your Name"]'), 137 email: card.querySelector('input[aria-label="Email"]'), 138 phone: card.querySelector('input[aria-label="Phone"]'), 139 club: card.querySelector('select[aria-label="Preferred Club"]'), 140 emailConsent: card.querySelector('input[name="email_consent"]'), 141 smsConsent: card.querySelector('input[name="sms_consent"]') 142 }; 143 var button = card.querySelector('.btn-form'); 144 if (!fields.name || !fields.email || !fields.phone || !fields.club || !button) return; 145 146 fields.name.required = true; 147 fields.club.required = true; 148 if (fields.club.options.length) fields.club.options[0].value = ''; 149 150 function selectedClub() { 151 if (fields.club.selectedIndex <= 0) return ''; 152 return fields.club.options[fields.club.selectedIndex].textContent.trim(); 153 } 154 155 function setInvalid(field, invalid) { 156 field.setAttribute('aria-invalid', invalid ? 'true' : 'false'); 157 } 158 159 function fail(field, message) { 160 if (field) { 161 setInvalid(field, true); 162 if (field.focus) field.focus(); 163 if (field.reportValidity) field.reportValidity(); 164 } 165 showGuestPassMessage(card, message, 'error'); 166 return false; 167 } 168 169 // Obvious email typos (28 Sept 2026): "@gmail.con" passed the check below, HubSpot refused it and two guest passes were 170 // lost in a week. Only unambiguous ones: endings that are not real, and misspelled big providers. Returns the likely 171 // address, or '' when the email looks fine. Same helper in lead-funnel.js, joint-landing.js, production-final.js, giveaway.js. 172 function emailTypo(em) { 173 var m = /^([^@\s]+)@([^@\s]+)\.([a-z]+)$/i.exec(String(em || '').trim()); 174 if (!m) return ''; 175 var dom = m[2].toLowerCase(), tld = m[3].toLowerCase(); 176 var fixed = { gmial: 'gmail', gmai: 'gmail', gmal: 'gmail', gamil: 'gmail', gnail: 'gmail', gmaill: 'gmail', gmali: 'gmail', 177 hotmial: 'hotmail', hotmal: 'hotmail', hotnail: 'hotmail', yaho: 'yahoo', yahooo: 'yahoo', outlok: 'outlook', 178 iclod: 'icloud', icoud: 'icloud' }[dom] || dom; 179 var t = { con: 1, cmo: 1, comm: 1, coom: 1, vom: 1, xom: 1, ocm: 1, cim: 1 }[tld] || (fixed === 'gmail' && tld !== 'com') ? 'com' : tld; 180 return fixed !== dom || t !== tld ? m[1] + '@' + fixed + '.' + t : ''; 181 } 182 function validate() { 183 [fields.name, fields.email, fields.phone, fields.club].forEach(function (field) { setInvalid(field, false); }); 184 185 if (!fields.name.value.trim()) return fail(fields.name, 'Please enter your name.'); 186 // checkValidity() is NOT enough on its own: HTML5 deliberately accepts "user@gmail" with 187 // no top-level domain, and GymSales rejects it. Seven lost guest-pass submissions between 188 // 30 Aug and 11 Sept 2026 were exactly that typo. Same rule as EMAIL_RE in api/submit-lead.js. 189 // Email is required (Naous, 25 Sept 2026: "no submissions without email"); a phone alone is 190 // no longer enough. 191 if (!fields.email.value.trim()) return fail(fields.email, 'Please enter your email address.'); 192 if (!fields.email.checkValidity() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(fields.email.value.trim())) 193 return fail(fields.email, 'Please enter a complete email address, including the part after the dot.'); 194 if (emailTypo(fields.email.value)) return fail(fields.email, 'Please check your email address. Did you mean ' + emailTypo(fields.email.value) + '?'); 195 if (!selectedClub()) return fail(fields.club, 'Please choose your preferred Club16 location.'); 196 return true; 197 } 198 199 function payload() { 200 return { 201 form_type: 'guest_pass', 202 full_name: fields.name.value.trim(), 203 email: fields.email.value.trim(), 204 phone: fields.phone.value.trim(), 205 preferred_club: selectedClub(), 206 email_consent: fields.emailConsent && fields.emailConsent.checked ? '1' : '0', 207 sms_consent: fields.smsConsent && fields.smsConsent.checked ? '1' : '0', 208 page: window.location.pathname, 209 page_url: window.location.href, 210 referrer: document.referrer, 211 utm: readAttribution() 212 }; 213 } 214 215 function submit(event) { 216 event.preventDefault(); 217 event.stopImmediatePropagation(); 218 if (!validate()) return; 219 220 var originalText = button.textContent; 221 button.disabled = true; 222 button.setAttribute('aria-busy', 'true'); 223 button.textContent = 'Sending...'; 224 showGuestPassMessage(card, 'Sending your pass request...', 'success'); 225 226 // Minted at submit and shared three ways: the API (server CAPI Lead), and â via 227 // sessionStorage â the thank-you page, whose browser Lead tag fires only when this 228 // token exists. Same id on both legs, so Meta dedupes; no token, no event. 229 var eventId;
230 try { 231 eventId = (window.crypto && crypto.randomUUID) ? crypto.randomUUID() : 'gp-' + Date.now() + '-' + Math.floor(Math.random() * 1e9); 232 } catch (_) { eventId = 'gp-' + Date.now(); } 233 234 fetch('/api/submit-lead', { 235 method: 'POST', 236 headers: { 'Content-Type': 'application/json' }, 237 body: JSON.stringify(Object.assign(payload(), { event_id: eventId })) 238 }) 239 .then(function (response) { 240 return response.json().catch(function () { return {}; }).then(function (data) { 241 if (!response.ok || !data.ok) { 242 throw new Error(data.error || 'Your pass could not be submitted. Please try again.'); 243 } 244 return data; 245 }); 246 }) 247 .then(function (data) { 248 try { sessionStorage.setItem('club16.leadEventId', eventId); } catch (_) {} 249 // Pilot clubs are handed to HubSpot and a setter phones them, so they get the 250 // confirmation that PROMISES that call. Every other club is self-serve and keeps 251 // the "watch your inbox" page, which is the only true thing to tell them. 252 // Labels are the exact <option> text on this form; submit-lead's PILOT_COMPANY_SLUG 253 // is the server-side twin (8066 Central City, 8067 Newton). 254 // Since 23 Sept (T22-8) every club goes to HubSpot, so the server's answer decides: 255 // route 'hubspot' means a setter will call. The club list stays as the fallback for 256 // a response without a route. 257 var PILOT_CONFIRM = { 'Surrey, Central City': 1, 'Surrey, Newton': 1 }; 258 var viaHubspot = data.route === 'hubspot' || PILOT_CONFIRM[selectedClub()]; 259 var target = viaHubspot ? '3-day-pass-confirmed' : 'guest-pass-confirmed.html'; 260 if (data.id) target += '?lead_id=' + encodeURIComponent(data.id); 261 window.location.href = target; 262 }) 263 .catch(function (error) { 264 button.disabled = false; 265 button.removeAttribute('aria-busy'); 266 button.textContent = originalText; 267 showGuestPassMessage(card, error.message || 'Your pass could not be submitted. Please try again.', 'error'); 268 }); 269 } 270 271 button.addEventListener('click', submit, true); 272 [fields.name, fields.email, fields.phone, fields.club].forEach(function (field) { 273 field.addEventListener('keydown', function (event) { 274 if (event.key !== 'Enter') return; 275 event.preventDefault(); 276 button.click(); 277 }); 278 }); 279 } 280 281 function setupGuestPassAutofill() { 282 if (!document.body.classList.contains('guest-pass-page')) return; 283 284 var card = document.getElementById('guest-pass-form'); 285 if (!card) return; 286 287 var fields = { 288 name: card.querySelector('input[aria-label="Your Name"]'), 289 email: card.querySelector('input[aria-label="Email"]'), 290 phone: card.querySelector('input[aria-label="Phone"]'), 291 club: card.querySelector('select[aria-label="Preferred Club"]') 292 }; 293 if (!fields.name || !fields.email || !fields.phone || !fields.club) return; 294 295 fields.name.setAttribute('name', 'name'); 296 fields.name.setAttribute('autocomplete', 'name'); 297 fields.name.setAttribute('autocapitalize', 'words'); 298 fields.email.setAttribute('name', 'email'); 299 fields.email.setAttribute('autocomplete', 'email'); 300 fields.phone.setAttribute('name', 'phone'); 301 fields.phone.setAttribute('autocomplete', 'tel'); 302 fields.phone.setAttribute('inputmode', 'tel'); 303 fields.club.setAttribute('name', 'preferred_club'); 304 305 var params = new URLSearchParams(window.location.search); 306 var clubTouched = false; 307 308 function normalize(value) { 309 return String(value || '') 310 .toLowerCase() 311 .replace(/she'?s/g, 'shes') 312 .replace(/&/g, 'and') 313 .replace(/[^a-z0-9]+/g, ' ') 314 .trim(); 315 } 316 317 function readJson(key) { 318 try { 319 return JSON.parse(localStorage.getItem(key) || 'null'); 320 } catch (_) { 321 return null; 322 } 323 } 324 325 function writeJson(key, value) { 326 try { 327 localStorage.setItem(key, JSON.stringify(value)); 328 } catch (_) {} 329 } 330 331 function getParam(keys) { 332 for (var i = 0; i < keys.length; i += 1) { 333 var value = params.get(keys[i]); 334 if (value) return value; 335 } 336 return ''; 337 } 338 339 function valueFromObject(source, keys) { 340 if (!source) return ''; 341 for (var i = 0; i < keys.length; i += 1) { 342 if (source[keys[i]]) return source[keys[i]]; 343 } 344 return ''; 345 } 346 347 function objectFromQueryString(value) { 348 if (!value || value.indexOf('=') === -1) return {}; 349 var query = value.charAt(0) === '?' ? value.slice(1) : value; 350 var nested = new URLSearchParams(query); 351 var first = nested.get('first_name') || nested.get('firstName') || ''; 352 var last = nested.get('last_name') || nested.get('lastName') || ''; 353 return { 354 name: nested.get('name') || nested.get('full_name') || nested.get('fullName') || [first, last].filter(
354Boolean).join(' '), 355 email: nested.get('email') || '', 356 phone: nested.get('phone') || nested.get('tel') || nested.get('mobile') || '', 357 club: nested.get('club') || nested.get('preferred_club') || nested.get('preferredClub') || nested.get('location') || '' 358 }; 359 } 360 361 function parseUserInfoParam() { 362 var raw = getParam(['uinfo', 'info', 'lead']); 363 if (!raw) return {}; 364 365 var candidates = [raw]; 366 try { 367 candidates.push(decodeURIComponent(raw)); 368 } catch (_) {} 369 370 if (window.atob) { 371 try { 372 candidates.push(window.atob(raw)); 373 } catch (_) {} 374 try { 375 var base64 = raw.replace(/-/g, '+').replace(/_/g, '/'); 376 while (base64.length % 4) base64 += '='; 377 candidates.push(window.atob(base64)); 378 } catch (_) {} 379 } 380 381 for (var i = 0; i < candidates.length; i += 1) { 382 var candidate = candidates[i]; 383 var queryObject = objectFromQueryString(candidate); 384 if (queryObject.name || queryObject.email || queryObject.phone || queryObject.club) return queryObject; 385 try { 386 var parsed = JSON.parse(candidate); 387 if (parsed && typeof parsed === 'object') return parsed; 388 } catch (_) {} 389 } 390 391 return {}; 392 } 393 394 function optionLabelForClub(value) { 395 var raw = String(value || '').trim(); 396 if (!raw) return ''; 397 398 var explicit = { 399 abbotsford: 'Abbotsford', 400 brentwood: 'Brentwood Burnaby', 401 'brentwood burnaby': 'Brentwood Burnaby', 402 chilliwack: 'Chilliwack', 403 coquitlam: 'Coquitlam', 404 'downtown van': 'Downtown Vancouver', 405 'downtown vancouver': 'Downtown Vancouver', 406 langley: 'Langley', 407 'maple ridge': 'Maple Ridge', 408 'north van': 'North Vancouver', 409 'north vancouver': 'North Vancouver', 410 richmond: 'Richmond', 411 'south burnaby': 'South Burnaby', 412 'south surrey': 'South Surrey/White Rock', 413 'south surrey white rock': 'South Surrey/White Rock', 414 'surrey central': 'Surrey, Central City', 415 'surrey central city': 'Surrey, Central City', 416 'surrey newton': 'Surrey, Newton', 417 tsawwassen: 'Tsawwassen/Ladner', 418 ladner: 'Tsawwassen/Ladner', 419 'tsawwassen ladner': 'Tsawwassen/Ladner', 420 'shes fit metrotown': "She's FIT! Metrotown", 421 metrotown: "She's FIT! Metrotown", 422 'shes fit delta': "She's FIT! North Delta", 423 'shes fit north delta': "She's FIT! North Delta", 424 'north delta': "She's FIT! North Delta", 425 'shes fit fleetwood': "She's FIT! Surrey Fleetwood", 426 'shes fit surrey fleetwood': "She's FIT! Surrey Fleetwood", 427 fleetwood: "She's FIT! Surrey Fleetwood" 428 }; 429 430 var normalized = normalize(raw); 431 if (explicit[normalized]) return explicit[normalized]; 432 433 var options = Array.prototype.slice.call(fields.club.options); 434 var exact = options.find(function (option) { 435 return option.index > 0 && normalize(option.textContent) === normalized; 436 }); 437 if (exact) return exact.textContent; 438 439 var partial = options.find(function (option) { 440 var optionNorm = normalize(option.textContent); 441 return option.index > 0 && (optionNorm.indexOf(normalized) !== -1 || normalized.indexOf(optionNorm) !== -1); 442 }); 443 return partial ? partial.textContent : ''; 444 } 445 446 function setClub(value, force) { 447 var label = optionLabelForClub(value); 448 if (!label) return; 449 if (!force && fields.club.selectedIndex > 0) return; 450 fields.club.value = label; 451 } 452 453 function selectedClub() { 454 if (fields.club.selectedIndex <= 0) return ''; 455 return fields.club.options[fields.club.selectedIndex].textContent.trim(); 456 } 457 458 function saveLead() { 459 writeJson('club16.guestPassLead', { 460 name: fields.name.value.trim(), 461 email: fields.email.value.trim(), 462 phone: fields.phone.value.trim(), 463 club: selectedClub(), 464 updatedAt: new Date().toISOString() 465 }); 466 } 467 468 function fillHomeClub(force) { 469 var savedHome = readJson('club16.homeClub'); 470 var city = document.querySelector('.loc-city'); 471 var club = savedHome && savedHome.name ? savedHome.name : city ? city.textContent : ''; 472 if (!club) return; 473 if (!force && (clubTouched || fields.club.selectedIndex > 0)) return; 474 setClub(club, force); 475 } 476 477 function fullNameFromObject(source) { 478 var explicit = valueFromObject(source, ['name', 'full_name', 'fullName']); 479 if (explicit) return explicit; 480 var first = valueFromObject(source, ['first_name', 'firstName']); 481 var last = valueFromObject(source, ['last_name', 'lastName']);
482 return [first, last].filter(Boolean).join(' '); 483 } 484 485 var userInfo = parseUserInfoParam(); 486 var savedLead = readJson('club16.guestPassLead') || {}; 487 var savedHomeClub = readJson('club16.homeClub') || {}; 488 var queryName = getParam(['name', 'full_name', 'fullName']) || 489 [getParam(['first_name', 'firstName']), getParam(['last_name', 'lastName'])].filter(Boolean).join(' '); 490 var seed = { 491 name: queryName || fullNameFromObject(userInfo) || savedLead.name || '', 492 email: getParam(['email']) || valueFromObject(userInfo, ['email']) || savedLead.email || '', 493 phone: getParam(['phone', 'tel', 'mobile']) || valueFromObject(userInfo, ['phone', 'tel', 'mobile']) || savedLead.phone || '', 494 club: getParam(['club', 'preferred_club', 'preferredClub', 'location', 'homeClub']) || 495 valueFromObject(userInfo, ['club', 'preferred_club', 'preferredClub', 'location', 'homeClub']) || 496 savedLead.club || 497 savedHomeClub.name || 498 '' 499 }; 500 501 if (seed.name && !fields.name.value) fields.name.value = seed.name; 502 if (seed.email && !fields.email.value) fields.email.value = seed.email; 503 if (seed.phone && !fields.phone.value) fields.phone.value = seed.phone; 504 setClub(seed.club, false); 505 fillHomeClub(false); 506 507 [fields.name, fields.email, fields.phone].forEach(function (field) { 508 field.addEventListener('input', saveLead); 509 field.addEventListener('change', saveLead); 510 }); 511 fields.club.addEventListener('change', function () { 512 clubTouched = true; 513 saveLead(); 514 }); 515 516 document.querySelectorAll('.guest-pass-page .btn-form').forEach(function (button) { 517 button.addEventListener('click', saveLead, true); 518 }); 519 520 var city = document.querySelector('.loc-city'); 521 if (city && window.MutationObserver) { 522 new MutationObserver(function () { 523 fillHomeClub(false); 524 }).observe(city, { childList: true, characterData: true, subtree: true }); 525 } 526 } 527 528 function fixMembershipLanguage() { 529 if (!/memberships\.html|join\.html/.test(window.location.pathname)) return; 530 531 document.querySelectorAll('.tier-card, .membership-option').forEach(function (card) { 532 var text = card.textContent || ''; 533 if (text.indexOf('VIP') !== -1) { 534 card.querySelectorAll('.dim').forEach(function (item) { 535 if (/Recovery amenities require VIP/i.test(item.textContent)) item.remove(); 536 }); 537 } 538 }); 539 540 var heroSub = document.querySelector('.page-hero-sub'); 541 if (heroSub && /Cancel/i.test(heroSub.textContent)) { 542 heroSub.textContent = 'Start with your home club, choose all-location access, or add recovery services where available. No contract. Written 30-day notice is required for cancellation.'; 543 } 544 545 document.querySelectorAll('.faq-item').forEach(function (item) { 546 var question = item.querySelector('.faq-q, summary'); 547 var answer = item.querySelector('.faq-a'); 548 if (question && answer && /cancel/i.test(question.textContent || '')) { 549 answer.textContent = 'Submit one full calendar month of written notice through your home club. If your account has a special term or balance, the club team can review it with you before the cancellation is processed.'; 550 } 551 }); 552 } 553 554 function removeUnverifiedShesFitReviews() { 555 if (!document.body.classList.contains('shesfit-page')) return; 556 Array.from(document.querySelectorAll('section')).forEach(function (section) { 557 var text = section.textContent || ''; 558 if (/in their own words|member stories|google review/i.test(text) && /â â â â â |5-star/i.test(text)) { 559 section.remove(); 560 } 561 }); 562 document.querySelectorAll('.hero-pill').forEach(function (pill) { 563 if (/find your she/i.test(pill.textContent)) pill.remove(); 564 }); 565 document.querySelectorAll('a').forEach(function (link) { 566 if (/3-day|free pass/i.test(link.textContent || '') && /join\.html/.test(link.getAttribute('href') || '')) { 567 link.setAttribute('href', 'guest-pass.html'); 568 } 569 }); 570 } 571 572 function setupAmenitiesClubChecker() { 573 if (!/amenities\.html/.test(window.location.pathname) && !document.body.classList.contains('amenities-page')) return; 574 var form = document.querySelector('.check-amenity .check-form'); 575 if (!form) return; 576 var select = form.querySelector('select'); 577 if (!select) return; 578 var panel = document.querySelector('.amenity-club-result'); 579 if (!panel) { 580 panel = document.createElement('div'); 581 panel.className = 'amenity-club-result'; 582 form.parentElement.appendChild(panel); 583 } 584 function render() { 585 var club = select.value || ''; 586 if (!club || /select your location/i.test(club)) { 587 panel.classList.remove('is-visible'); 588 panel.innerHTML = ''; 589 return; 590 } 591 panel.classList.add('is-visible'); 592 panel.innerHTML = '<h4>' + club + ' amenity snapshot</h4>' + 593 '<div class="amenity-result-grid">' + 594 '<div><strong>Included at this club</strong><ul><li>Strength training floor</li><li>Large cardio equipment selection</li><li>Functional training space</li><li>Women-only She\'s FIT! area</li><li>Locker rooms, showers, and free Wi-Fi</li></ul></div>' + 595 '<div><strong>VIP amenities to confirm</strong><ul><li>Red light therapy</li><li>Hydromassage</li><li>Tanning</li><li>Infrared sauna at Club16 locations</li><li>All-location access across the network</li></ul></div>' + 596 '</div><p>Exact service hours and equipment availability can vary by club. The front desk can confirm the current setup before your visit.</p>'; 597 } 598 form.addEventListener('submit', function (event) { 599 event.preventDefault(); 600 event.stopImmediatePropagation(); 601 render(); 602 }, true); 603 select.addEventListener('change', render); 604 } 605 606 function setupAmenitiesClubChecker() { 607 if (!/amenities\.html/.test(window.location.pathname) && !document.body.classList.contains('amenities-page')) return; 608 var form = document.querySelector('.check-amenity .check-form'); 609 if (!form) return; 610 var select = form.querySelector('select'); 611 if (!select) return; 612 var panel = document.querySelector('.amenity-club-result'); 613 if (!panel) { 614 panel = document.createElement('div'); 615 panel.className = 'amenity-club-result'; 616 form.parentElement.appendChild(panel); 617 } 618 function render() { 619 var club = select.value || ''; 620 if (!club || /select your location/i.test(club)) { 621 panel.classList.remove('is-visible'); 622 panel.innerHTML = ''; 623 return; 624 } 625 panel.classList.add('is-visible'); 626 panel.innerHTML = '<h4>' + club + ' amenity snapshot</h4>' + 627 '<div class="amenity-result-grid">' + 628 '<div><strong>Included at this club</strong><ul><li>Strength training floor</li><li>Large cardio equipment selection</li><li>Functional training space</li><li>Women-only She\'s FIT! area</li><li>Locker rooms, showers, and free Wi-Fi</li></ul></div>' + 629 '<div><strong>VIP amenities to confirm</strong><ul><li>Red light therapy</li><li>Hydromassage</li><li>Tanning</li><li>Infrared sauna at Club16 locations</li><li>All-location access across the network</li></ul></div>' + 630 '</div><p>Exact service hours and equipment availability can vary by club. The front desk can confirm the current setup before your visit.</p>'; 631 } 632 form.addEventListener('submit', function (event) { 633 event.preventDefault(); 634 event.stopImmediatePropagation(); 635 render(); 636 }, true); 637 select.addEventListener('change', render); 638 } 639
640 function fixMobileStickyGuestPassLinks() { 641 document.querySelectorAll('.sticky-cta a, .sticky-cta-btn, .sticky-cta-right').forEach(function (link) { 642 var text = (link.textContent || '').toLowerCase(); 643 if (text.indexOf('3-day') !== -1 || text.indexOf('free pass') !== -1 || text.indexOf('start free') !== -1) { 644 link.setAttribute('href', 'guest-pass.html'); 645 } 646 }); 647 } 648 649 function run() { 650 setupAttributionCapture(); 651 setupHomeHeroPlaylist(); 652 hidePhaseTwoLinks(); 653 cleanupCopy(); 654 setupGuestPassAutofill(); 655 setupGuestPassSubmit(); 656 fixMembershipLanguage(); 657 removeUnverifiedShesFitReviews(); 658 setupAmenitiesClubChecker(); 659 fixMobileStickyGuestPassLinks(); 660 } 661 662 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run); 663 else run(); 664})(); 665 666/* ============================================================ 667 V1 engagement layer â scroll reveals, stagger, float, count-up. 668 Progressive enhancement, no FOUC, reduced-motion aware. 669 ============================================================ */ 670(function () { 671 if (!('IntersectionObserver' in window) || !document.querySelectorAll) return; 672 var docEl = document.documentElement; 673 var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; 674 docEl.classList.add('js-motion'); 675 676 function ready(fn) { 677 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn); 678 else fn(); 679 } 680 681 ready(function () { 682 document.querySelectorAll('.hero-pill').forEach(function (el) { el.classList.add('c16-float'); }); 683 684 if (reduce) return; 685 686 var revealSel = [ 687 '.section > .eyebrow', '.section .h2', '.section > .sub', 688 '.section-paper .h2', '.section-dark .h2', 689 '.programs-header-left', '.programs-header-right', '.mission-fullbleed-content', 690 '.program-card-wide', '.am-card', '.sig-card', '.service-card', '.cat-card', 691 '.post-card', '.blog-card', '.leader-card', '.value-card', '.sf-pillar', 692 '.feature-card', '.timeline-item', '.press-item', '.next-step', 693 '.virtual-tour', '.google-reviews', '.welcome-workout-grid', '.packages-grid' 694 ]; 695 var staggerSel = [ 696 '.trust-strip', '.ig-feed', '.included-grid', '.expect-grid', 697 '.test-grid', '.sf-pillars-grid', '.cat-grid', '.next-steps-grid' 698 ]; 699 700 var vh = function () { return window.innerHeight || docEl.clientHeight; }; 701 var inView = function (el) { var r = el.getBoundingClientRect(); return r.top < vh() * 0.96 && r.bottom > 0; }; 702 703 var io = new IntersectionObserver(function (entries) { 704 entries.forEach(function (e) { 705 if (e.isIntersecting) { e.target.classList.add('c16-in'); io.unobserve(e.target); } 706 }); 707 }, { rootMargin: '0px 0px -7% 0px', threshold: 0.1 }); 708 709 var seen = []; 710 function skip(el) { 711 return el.closest('.hero') || el.closest('.page-hero') || 712 el.closest('.sticky-cta') || el.closest('footer') || 713 el.closest('.nav') || el.closest('.exit-intent') || el.__c16; 714 } 715 document.querySelectorAll(revealSel.join(',')).forEach(function (el) { 716 if (skip(el)) return; el.__c16 = 1; el.classList.add('c16-reveal'); seen.push(el); 717 }); 718 document.querySelectorAll(staggerSel.join(',')).forEach(function (el) { 719 if (skip(el)) return; el.__c16 = 1; el.classList.add('c16-stagger'); seen.push(el); 720 }); 721 722 // Synchronously reveal anything already in view (prevents flash), observe the rest. 723 seen.forEach(function (el) { if (inView(el)) el.classList.add('c16-in'); else io.observe(el); }); 724 725 // Animated count-up for stat numbers when they scroll into view. 726 function countUp(el) { 727 if (el.__counted) return; 728 var raw = (el.textContent || '').trim(); 729 var m = raw.match(/^(\D*)(\d[\d,]*(?:\.\d+)?)(.*)$/); 730 if (!m) return; 731 el.__counted = 1; 732 var prefix = m[1], rawNum = m[2], suffix = m[3]; 733 var hasComma = rawNum.indexOf(',') !== -1; 734 var decimals = (rawNum.split('.')[1] || '').length; 735 var target = parseFloat(rawNum.replace(/,/g, '')); 736 if (isNaN(target)) return; 737 el.classList.add('c16-count'); 738 var dur = 1200, t0 = null; 739 function fmt(n) { 740 var v = decimals ? n.toFixed(decimals) : String(Math.round(n)); 741 if (hasComma) { var p = v.split('.');
741 p[0] = Number(p[0]).toLocaleString('en-US'); v = p.join('.'); } 742 return v; 743 } 744 function tick(ts) { 745 if (!t0) t0 = ts; 746 var p = Math.min((ts - t0) / dur, 1), e = 1 - Math.pow(1 - p, 3); 747 el.textContent = prefix + fmt(target * e) + suffix; 748 if (p < 1) requestAnimationFrame(tick); else el.textContent = prefix + fmt(target) + suffix; 749 } 750 requestAnimationFrame(tick); 751 } 752 var cio = new IntersectionObserver(function (entries) { 753 entries.forEach(function (e) { if (e.isIntersecting) { countUp(e.target); cio.unobserve(e.target); } }); 754 }, { threshold: 0.6 }); 755 document.querySelectorAll('.trust-stat-num, .programs-header-stat-num, .impact-num, .stat-num').forEach(function (el) { 756 cio.observe(el); 757 }); 758 759 // Scroll-cinematic hero (home, desktop/tablet only): subtle media zoom + 760 // content parallax-out via a CSS custom property, plus a scroll cue. 761 var hero = document.querySelector('body.home-page .hero'); 762 if (hero && window.matchMedia('(min-width: 768px)').matches) { 763 if (getComputedStyle(hero).position === 'static') hero.style.position = 'relative'; 764 var heroTicking = false; 765 function heroParallax() { 766 var rect = hero.getBoundingClientRect(); 767 var h = rect.height || 1; 768 var p = Math.min(Math.max(-rect.top / h, 0), 1); 769 docEl.style.setProperty('--c16-hero-p', p.toFixed(3)); 770 heroTicking = false; 771 } 772 window.addEventListener('scroll', function () { 773 if (!heroTicking) { heroTicking = true; requestAnimationFrame(heroParallax); } 774 }, { passive: true }); 775 heroParallax(); 776 777 var cue = document.createElement('div'); 778 cue.className = 'c16-scrollcue'; 779 cue.setAttribute('aria-hidden', 'true'); 780 hero.appendChild(cue); 781 window.addEventListener('scroll', function () { 782 if (window.scrollY > 60) cue.classList.add('is-hidden'); 783 else cue.classList.remove('is-hidden'); 784 }, { passive: true }); 785 } 786 }); 787})(); 788 789/* About page â play Trevor's Club16 message inline instead of opening YouTube. 790 Graceful fallback: with no JS, the link still opens the video on YouTube. */ 791(function () { 792 function ready(fn) { 793 if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn); 794 else fn(); 795 } 796 ready(function () { 797 document.querySelectorAll('a.trevor-video-frame[href*="youtu"]').forEach(function (el) { 798 el.addEventListener('click', function (e) { 799 var m = (el.getAttribute('href') || '').match(/(?:youtu\.be\/|[?&]v=)([\w-]{6,})/); 800 if (!m) return; 801 e.preventDefault(); 802 if (el.dataset.embedded) return; 803 el.dataset.embedded = '1'; 804 var f = document.createElement('iframe'); 805 f.src = 'https://www.youtube.com/embed/' + m[1] + '?autoplay=1&rel=0'; 806 f.title = "Trevor Linden's Club16 message"; 807 f.setAttribute('allow', 'autoplay; encrypted-media; fullscreen; picture-in-picture'); 808 f.setAttribute('allowfullscreen', ''); 809 f.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;border:0;border-radius:inherit;z-index:5'; 810 el.appendChild(f); 811 }); 812 }); 813 }); 814})();
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.