PageSourceSearch

https://trevorlindenfitness.com/production-final.js?v=9

js trevorlindenfitness.com collected 2026-10-03 22:15:01 UTC 35,118 bytes, 814 lines download raw bytes

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.