PageSourceSearch

https://www.hmns.org/wp-content/themes/hmns-theme/js/hmns-footer.js?ver=1785979925

js hmns.org collected 2026-10-01 10:23:52 UTC 55,090 bytes, 1,410 lines download raw bytes

1/**
2 * HMNS Footer Front-End Logic
3 *
4 * Extracted verbatim from the former inline <script> in footer.php so it can be
5 * enqueued the WordPress way (versioned via filemtime, cacheable) instead of
6 * being re-parsed inline on every page load.
7 *
8 * All values that were previously injected via PHP (SVG icons, the Tessitura
9 * server URL, the current permalink, a URL promo code, notification-bar session
10 * flags, and the search query) are now passed in through window.hmnsFooterData,
11 * localized just before this file by hmns_enqueue_footer_script().
12 *
13 * Depends on jQuery and js/hmns-tnew-helpers.js (hmnsSetAjaxHeaders,
14 * hmnsCreateTNEWSession) and on the hmnsCookieDomain global (set inline in
15 * header.php) and window.hmns_ajax_router (set in the footer at priority 1).
16 */
17var hmnsFooterData = window.hmnsFooterData || {};
18hmnsFooterData.icons = hmnsFooterData.icons || {};
19jQuery(document).ready(function ($) {
20
21	/**
22	 * Keeps #container padding in sync with the real height of .top-nav + header#header (including 0–2 notification bars).
23	 * When no notification bars are visible, restores header top padding to 12px.
24	 */
25	var hmnsLastHeaderStackBottom = 0;
26	window.hmnsApplyHeaderStackOffset = function () {
27		var header = document.querySelector('header#header');
28		if (!header) {
29			return;
30		}
31		var $vis = $('.hmns-notifications').filter(function () {
32			return !$(this).hasClass('hide');
33		});
34		$vis.css('margin-top', '');
35		if ($vis.length >= 2) {
36			$vis.eq(1).css('margin-top', '-21px');
37		}
38		if ($vis.length === 0) {
39			header.style.paddingTop = '12px';
40		} else {
41			header.style.paddingTop = '0px';
42		}
43		var topNav = document.querySelector('.top-nav');
44		var tnRect = topNav ? topNav.getBoundingClientRect() : null;
45		var tnBottom = tnRect ? Math.ceil(tnRect.bottom) : 0;
46		var hRect = header.getBoundingClientRect();
47		var hBottom = Math.ceil(hRect.bottom);
48		var hH = header.offsetHeight;
49		// If header#header shares the same top origin as the viewport (or sits above .top-nav's bottom),
50		// max(tnBottom, hBottom) under-counts (e.g. 122 instead of 53 + 122). Stack: top-nav + full header height.
51		var stackBottom;
52		if (topNav && hRect.top < tnBottom - 2) {
53			stackBottom = tnBottom + hH;
54		} else {
55			stackBottom = Math.max(tnBottom, hBottom);
56		}
57		stackBottom = Math.ceil(stackBottom);
58		// Logged-in WP admin bar: extra clearance (toolbar not fully reflected in fixed header measurements).
59		if (document.body.classList.contains('admin-bar')) {
60			stackBottom += window.innerWidth < 787 ? 46 : 32;
61		}
62		if (stackBottom > 48) {
63			hmnsLastHeaderStackBottom = stackBottom;
64		}
65		var px = hmnsLastHeaderStackBottom + 'px';
66		var rootStyle = document.documentElement.style;
67		if (rootStyle.getPropertyValue('--hmns-header-stack-height').trim() === px) {
68			return;
69		}
70		rootStyle.setProperty('--hmns-header-stack-height', px);
71	};
72	
73	const setAjaxHeaders = window.hmnsSetAjaxHeaders;
74	
75	// Override jQuery.ajax to automatically queue router calls until nonce is ready
76	var _origAjax = $.ajax;
77	$.ajax = function(options) {
78		var url = options && options.url;
79		var isRouter = url && (url === window.hmns_ajax_router?.url ||
80			(typeof url === 'string' && url.indexOf('/wp-json/hmns-api/v1/router') !== -1));
81		if (isRouter && !(window.hmns_ajax_router && window.hmns_ajax_router.ready)) {
82			var deferred = $.Deferred();
83			$(document).one('hmns_nonce_ready', function() {
84				_origAjax.call($, options)
85					.done(function() { deferred.resolve.apply(deferred, arguments); })
86					.fail(function() { deferred.reject.apply(deferred, arguments); });
87			});
88			return deferred.promise();
89		}
90		return _origAjax.call(this, options);
91	};	
92	
93	$('.cookies-menu').on('click tap',function () {
94		$('.cookies-modal').removeClass('hide');
95		$(this).addClass('open');
96	});
97	var clearcookiesreset = 0;
98	$('.cookie-button-flex .clear-cookies').on('click tap',function () {
99		if (clearcookiesreset == 0) {
100			$(this).text('Please Click Again To Confirm');	
101			clearcookiesreset++;	
102		} else {
103		eat_cookies(true);
104		if (window.sessionTimerInterval) { clearInterval(window.sessionTimerInterval); }
105		window.sessionTimerReady = false;
106		$('.cookies-modal .top-bar h4').text('Cookies Cleared!');
107		$('.cookies-modal .cookie-button-flex').addClass('hide');
108		$('.cookies-modal .cookie-clear-success').removeClass('hide');
109		$('.tessitura-cell.cart .cart-counter span').text('0');		
110		$('.tessitura-cell.cart').removeClass('populated');
111		$('.sessionTimer').addClass('hide').html('');
112			window.setTimeout(function () {
113				$('.cookies-modal').addClass('hide');
114				$('.cookies-menu').removeClass('open');
115				$('.cookies-modal .top-bar h4').text('Clear Cookies?');
116				$('.cookies-modal .cookie-button-flex').removeClass('hide');
117				$('.cookies-modal .cookie-clear-success').addClass('hide');
118			},3000);
119			clearcookiesreset = 0;
120			$(this).text("Yes, Clear All Tessitura Cookies.");
121		}
122	});
123	$('.cookie-button-flex .close-cookie-modal').on('click tap',function () {
124		$('.cookies-modal').addClass('hide');
125		$('.cookies-menu').removeClass('open');
126	});
127	var sessionKey = getCookie('sessionKey');
128	// console.log(sessionKey);
129	var mmtoggle = hmnsFooterData.icons.mobileToggle;
130	var cartclosed = 0;
131	
132	var createTNEWSession = window.hmnsCreateTNEWSession;
133
134	function clearSessionTimerCookie() {
135		document.cookie = "sessionTimer=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=" + hmnsCookieDomain + "; path=/;";
136	}
137
138	function isLoginFlowActive() {
139		return window.hmnsLoginInProgress === true;
140	}
141
142	$('.cart-exp-close button').on('click tap',function (e) {
143		e.preventDefault();
144		e.stopImmediatePropagation();
145		$('.cart-expired').addClass('hide');
146		cartclosed = 1;
147	});
148	$('#mega-menu-wrap-main-menu .mega-menu-toggle .mega-toggle-animated-box').html(mmtoggle);
149	var tickets = hmnsFooterData.icons.tickets;
150	var trex = hmnsFooterData.icons.trex;
151	var chevdownwhite = hmnsFooterData.icons.chevronDownWhite;
152	$('#mega-menu-main-menu').prepend('<li><div class="menu-search-box"><form role="search" method="get" id="searchform-m" class="searchform" action="/"><div class="mobile-search-flex"><input type="text" name="s" placeholder="Search" value="' + hmnsFooterData.searchQuery + '"/><div class="search-icon"><svg width="18" height="19" viewBox="0 0 18 19" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M8.1579 2.5C6.52472 2.5 4.95844 3.14878 3.80361 4.30361C2.64878 5.45844 2 7.02472 2 8.6579C2 10.2911 2.64878 11.8574 3.80361 13.0122C4.95844 14.167 6.52472 14.8158 8.1579 14.8158C9.78273 14.8158 11.3414 14.1736 12.4945 13.0298C12.5015 13.0224 12.5086 13.0151 12.5158 13.0078C12.5224 13.0013 12.5289 12.995 12.5356 12.9887C13.6758 11.8361 14.3158 10.28 14.3158 8.6579C14.3158 7.02472 13.667 5.45844 12.5122 4.30361C11.3574 3.14878 9.79107 2.5 8.1579 2.5ZM14.5925 13.6727C15.7034 12.2472 16.3158 
15210.4841 16.3158 8.6579C16.3158 6.49429 15.4563 4.41929 13.9264 2.88939C12.3965 1.35949 10.3215 0.5 8.1579 0.5C5.99429 0.5 3.91929 1.35949 2.38939 2.88939C0.85949 4.41929 0 6.49429 0 8.6579C0 10.8215 0.85949 12.8965 2.38939 14.4264C3.91929 15.9563 5.99429 16.8158 8.1579 16.8158C9.98679 16.8158 11.7524 16.2017 13.1789 15.0876L16.2923 18.2065C16.6824 18.5973 17.3156 18.5979 17.7065 18.2077C18.0973 17.8176 18.0979 17.1844 17.7077 16.7935L14.5925 13.6727Z" fill="#2B2E2E"></path></svg></div></div><input type="submit" id="searchsubmit" value="Search" class="hide"></form></div></li>');
153	$('.top-nav #mega-menu-wrap-main-menu #mega-menu-main-menu > li.mega-get-tickets > a.mega-menu-link').prepend(tickets);
154	$('.top-nav #mega-menu-wrap-main-menu #mega-menu-main-menu > li.mega-membership > a.mega-menu-link').prepend(trex);	
155	var ticketsmenu = $('.top-nav #mega-menu-wrap-main-menu #mega-menu-main-menu > li.mega-get-tickets');
156	var membershipmenu = $('.top-nav #mega-menu-wrap-main-menu #mega-menu-main-menu > li.mega-membership');
157
158// Check if the items are already in the .mobile-menu-bot-section
159if ($('.mobile-menu-bot-section .mega-get-tickets').length === 0) {
160    if (ticketsmenu.length > 0) {
161        ticketsmenu = ticketsmenu.detach();
162    }
163}
164if ($('.mobile-menu-bot-section .mega-membership').length === 0) {
165    if (membershipmenu.length > 0) {
166        membershipmenu = membershipmenu.detach();
167    }
168}
169
170$('#mega-menu-wrap-main-menu .mega-menu-toggle .mega-toggle-block-0 button.mega-toggle-animated').on('click tap',function () {
171	$(this).children('span').children('svg').map(function () {
172		$(this).toggleClass('hide');
173	}).get();
174});
175
176if ($('.mobile-menu-bot-section').length === 0) {
177    $('#mega-menu-main-menu').append('<div class="mobile-menu-bot-section"></div>');
178}
179	$('.mobile-menu-bot-section').append(membershipmenu);
180	$('.mobile-menu-bot-section').append(ticketsmenu);
181	if (typeof window.hmnsApplyHeaderStackOffset === 'function') {
182		window.hmnsApplyHeaderStackOffset();
183	}
184	$('.search-icon').click(function (e) {
185		$('form#searchform-m').submit();
186	});
187	$('.search-icon.search').click(function (e) {
188		$('form#searchform').submit();
189	});
190	//open search modal for desktop
191	$('.search-menu').on('click tap',function (e) {
192		e.preventDefault();
193		e.stopImmediatePropagation;
194		$('#search').toggleClass('hide');
195	});
196	//change gttranslate svg
197	$('.gt_float_switcher .gt-selected .gt-current-lang span.gt_float_switcher-arrow').html(chevdownwhite);
198	// Notification-bar dismissals: a session cookie plus an <html> class replace the
199	// old PHP-session AJAX round-trip (page-cache safe; state survives navigation for
200	// the browser session, mirroring the previous PHP-session lifetime).
201	$('.close-nb2').on('click tap',function () {
202		document.cookie = 'hmnsNb2Closed=1; path=/';
203		document.documentElement.className += ' hmns-nb2-closed';
204		$(this).parent('.hmns-notifications').addClass('hide');
205		if (typeof window.hmnsApplyHeaderStackOffset === 'function') {
206			window.hmnsApplyHeaderStackOffset();
207		}
208		return false;
209	});
210
211	$('.close-nb').on('click tap',function () {
212		document.cookie = 'hmnsNbClosed=1; path=/';
213		document.documentElement.className += ' hmns-nb-closed';
214		$(this).parent('.hmns-notifications').addClass('hide');
215		if (typeof window.hmnsApplyHeaderStackOffset === 'function') {
216			window.hmnsApplyHeaderStackOffset();
217		}
218		return false;
219	});
220	// Sync #container offset after layout settles (debounced: load + fonts + ResizeObserver fire in a tight burst).
221	(function () {
222		var debounceMs = 80;
223		var timer = null;
224		function schedule() {
225			if (timer) {
226				clearTimeout(timer);
227			}
228			timer = setTimeout(function () {
229				timer = null;
230				if (typeof window.hmnsApplyHeaderStackOffset === 'function') {
231					window.hmnsApplyHeaderStackOffset();
232				}
233			}, debounceMs);
234		}
235		$(window).on('resize', schedule);
236		$(window).on('load', schedule);
237		if (document.fonts && document.fonts.ready) {
238			document.fonts.ready.then(schedule);
239		}
240		if (typeof ResizeObserver !== 'undefined') {
241			var hdr = document.querySelector('header#header');
242			if (hdr) {
243				new ResizeObserver(schedule).observe(hdr);
244			}
245			var tn = document.querySelector('.top-nav');
246			if (tn) {
247				new ResizeObserver(schedule).observe(tn);
248			}
249			$('.hmns-notifications').each(function () {
250				new ResizeObserver(schedule).observe(this);
251			});
252		}
253		// One pass after this tick (catches reflow from code above); merges with observers via debounce.
254		schedule();
255	})();
256	//Tessitura Cookies & TessituraBar Interactions
257	var tessituraStatus = getCookie('tessituraLogin') || false;
258	var promoCode = getCookie('promoCode') || '';
259	//console.log(promoCode);
260	var returnURL = hmnsFooterData.returnURL;
261	// CACHE-COMPATIBLE: Initialize tessitura bar from static skeleton
262	function initializeTessituraBar() {
263		// Initialize sessionTimer ready flag - will be set to true if timer initializes successfully
264		window.sessionTimerReady = false;
265		
266		// Get current cookies
267		sessionKey = getCookie('sessionKey');
268		var currentPromoCode = getCookie('promoCode') || '';
269		var modeOfSale = getCookie('modeOfSale') || '2';
270		var sessionTimer = getCookie('sessionTimer');
271		
272		// Update promo fields (both desktop and mobile) if cookie exists
273		var promoHasValue = false;
274		if (currentPromoCode) {
275			if (currentPromoCode !== '') {
276				promoHasValue = true;
277			}
278		}
279		if (promoHasValue) {
280			// Desktop promo - show promo applied state, but keep X hidden until cart check completes
281			$('#promoCode').val(currentPromoCode).addClass('hide');
282			$('.hmns-tessitura-bar .promo-clear-code').text(currentPromoCode).removeClass('hide');
283			$('.hmns-tessitura-bar .promoSubmit').addClass('hide');
284			// .promo-clear visibility handled by updatePromoClearVisibility after session check
285			
286			// Mobile promo  
287			$('#promoCodeMobile').val(currentPromoCode).addClass('hide');
288			$('.mobile-tessitura-bar .promo-clear-code').text(currentPromoCode).removeClass('hide');
289			$('.mobile-tessitura-bar .promoSubmit').addClass('hide');
290			// .promo-clear visibility handled by updatePromoClearVisibility after session check
291			setCartPromoLink(currentPromoCode);
292		} else {
293			setCartPromoLink(null);
294		}
295		
296		// Update mode of sale for both desktop and mobile
297		$('.tessitura-cell.user-menu').attr('data-mosid', modeOfSale);
298		
299		// Initialize session timer if exists and valid
300		var timerHasValue = false;
301		if (sessionTimer) {
302			if (sessionTimer !== '') {
303				if (sessionTimer !== '0') {
304					timerHasValue = true;
305				}
306			}
307		}
308		if (timerHasValue) {
309			// console.log('initializeTessituraBar: converting sessionTimer to Central Time:', sessionTimer);
310			
311			// Recreate the PHP timezone conversion logic
312			var tte = new Date(sessionTimer);
313			var now = new Date();
314			
315			// Convert both times to Central Time (like PHP code did)
316			var options = { timeZone: 'America/Chicago', hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' };
317			var formatter = new Intl.DateTimeFormat('en-US', options);
318			
319			// Get current time in Central
320			var nowParts = formatter.formatToParts(now);
321			var nowCentral = new Date(
322				nowParts.find(p => p.type === 'year').value + '-' + 
323				nowParts.find(p => p.type === 'month').value + '-' + 
324				nowParts.find(p => p.type === 'day').value + 'T' + 
325				nowParts.find(p => p.type === 'hour').value + ':' + 
326				nowParts.find(p => p.type === 'minute').value + ':' + 
327				nowParts.find(p => p.type === 'second').value
328			);
329			
330			// Get expiration time in Central  
331			var tteParts = formatter.formatToParts(tte);
332			var tteCentral = new Date(
333				tteParts.find(p => p.type === 'year').value + '-' + 
334				tteParts.find(p => p.type === 'month').value + '-' + 
335				tteParts.find(p => p.type === 'day').value + 'T' + 
336				tteParts.find(p => p.type === 'hour').value + ':' + 
337				tteParts.find(p => p.type === 'minute').value + ':' + 
338				tteParts.find(p => p.type === 'second').value
339			);
340			
341			// Calculate difference in Central time (like PHP strtotime comparison)
342			var timetoexpirydiff = tteCentral.getTime() - nowCentral.getTime();
343			
344			// console.log('Central time calculation - now:', nowCentral, 'expires:', tteCentral, 'diff:', timetoexpirydiff);
345			
346			if (timetoexpirydiff > 0) {
347				// Use the original sessionTimer as timetoexpiry (initializeSessionTimer handles the rest)
348				var timetoexpiry = sessionTimer;
349				initializeSessionTimer(timetoexpiry);
350			} else {
351				// console.log('Timer already expired in Central time, not starting');
352				$('.sessionTimer').addClass('hide');
353			}
354		} else {
355			// console.log('initializeTessituraBar: no valid session timer cookie found:', sessionTimer);
356		}
357		
358		// console.log('Tessitura bar initialized with sessionKey:', sessionKey, 'promoCode:', currentPromoCode, 'modeOfSale:', modeOfSale, 'sessionTimer:', sessionTimer);
359	}
360	
361	// CACHE-COMPATIBLE: Session timer initialization
362	function initializeSessionTimer(timetoexpiry) {
363		// console.log('initializeSessionTimer called with:', timetoexpiry);
364		
365		// Update sessionKey from cookie if not set
366		if (!sessionKey || sessionKey == '') {
367			sessionKey = getCookie('sessionKey');
368			if (!sessionKey || sessionKey == '') {
369				// console.log('Timer initialization aborted: no sessionKey');
370				$('.sessionTimer').addClass('hide');
371				return;
372			}
373		}
374		
375		// Validate timetoexpiry more thoroughly
376		if (!timetoexpiry || timetoexpiry == 0 || timetoexpiry == '' || timetoexpiry == '0') {
377			// console.log('Timer initialization aborted: invalid timetoexpiry value:', timetoexpiry);
378			$('.sessionTimer').addClass('hide');
379			return;
380		}
381		
382		// Try to parse the expiration time
383		var expiration = new Date(timetoexpiry).getTime();
384		if (isNaN(expiration)) {
385			// console.log('Timer initialization aborted: invalid date format:', timetoexpiry);
386			$('.sessionTimer').addClass('hide');
387			return;
388		}
389		
390		// Check if timer has already expired (with 5 second buffer)
391		var now = new Date().getTime();
392		if (expiration <= (now + 5000)) {
393			// console.log('Timer initialization aborted: timer already expired or expires too soon:', new Date(expiration), 'vs now:', new Date(now));
394			$('.sessionTimer').addClass('hide');
395			if (isLoginFlowActive()) {
396				clearSessionTimerCookie();
397				return;
398			}
399			eat_cookies(); // Clean up expired session
400			return;
401		}
402		
403		// console.log('Timer initializing with sessionKey:', sessionKey, 'expires:', new Date(expiration));
404		// Don't show timer yet - wait for cart count to come back
405		// Set flag that timer is ready
406		window.sessionTimerReady = true;
407		// Keep timer hidden until cart count is checked
408		$('.sessionTimer').addClass('hide');
409		
410		var dst = getCookie('dst') || (isDaylightSavingTimeCentral() ? '1' : '0');
411					function updateTimer() {
412    // Get the current local time
413		    let now = new Date();
414		
415		    // Convert the current local time to CST
416		    let options = { timeZone: 'America/Chicago', hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' };
417		    let formatter = new Intl.DateTimeFormat('en-US', options);
418		    let parts = formatter.formatToParts(now);
419		    let [month, day, year, hour, minute, second] = parts.filter(part => part.type !== 'literal').map(part => part.value);
420		     let cstTime;
421		    if (dst == '1') {
422		   	cstTime = new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}-05:00`).getTime();
423			} else {
424				cstTime = new Date(`${year}-${month}-${day}T${hour}:${minute}:${second}-06:00`).getTime();
425			}
426
427		
428		    // Calculate the distance between the expiration time and the current CST time
429		    let distance = expiration - cstTime;
430		
431		    // Calculate hours, minutes, and seconds
432		    let hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
433		    let minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
434		    let seconds = Math.floor((distance % (1000 * 60)) / 1000);
435		
436		    // Update the countdown timer on the webpage
437		    if (distance <= 0) {
438		        $('.sessionTimer').html('');
439		        window.setTimeout(function () {
440		            $('.sessionTimer').addClass('hide');
441		            $('.tessitura-cell.cart .cart-counter span').text('0');
442					   $('.tessitura-cell.cart').removeClass('populated');
443					   $('.tessitura-cell.user-menu .logged-in').addClass('hide');
444					   $('.tessitura-cell.user-menu .logged-out').removeClass('hide');
445		            //$('.tessitura-cell.cart').addClass('hide');
446		            //$('.tessitura-cell.tickets').removeClass('hide');
447		            if (cartclosed == 0) { $('.cart-expired').removeClass('hide'); }
448		        }, 1000);
449		        clearInterval(window.sessionTimerInterval);
450		        window.sessionTimerReady = false; // Reset flag when timer expires
451		        // console.log('this is definitely happening, right');
452		        if (isLoginFlowActive()) {
453		        	clearSessionTimerCookie();
454		        	return;
455		        }
456		        eat_cookies();
457		    } else {
458		        let timeLeft = `Time Left: ${minutes}:${seconds < 10 ? '0' + seconds : seconds}`;
459		        $('.sessionTimer').html(timeLeft);
460		    }
461		}
462		
463		// Clear any existing timer and start new one
464		if (window.sessionTimerInterval) {
465			// console.log('Clearing existing timer interval');
466			clearInterval(window.sessionTimerInterval);
467		}
468		// console.log('Starting new timer interval');
469		window.sessionTimerInterval = setInterval(updateTimer, 1000);
470		updateTimer(); // Initial call
471	}
472	
473	// Initialize the bar immediately
474	initializeTessituraBar();
475	
476	if (tessituraStatus == true) {
477		$('.tessitura-cell.user-menu .logged-in').removeClass('hide');
478		$('.tessitura-cell.user-menu .logged-out').addClass('hide');	
479	} 
480	var tserverURL = hmnsFooterData.tserverURL;
481	var ajaxArray = [];
482
483	// TNEW storefront base from the hmnsTnewBase global (header.php, per-environment
484	// HMNS_TNEW_URL constant); fallback keeps dev working standalone.
485	var tnewBase = (typeof hmnsTnewBase === 'string' && hmnsTnewBase !== '') ? hmnsTnewBase : 'https://hmns-tnew-test.tnhs.cloud';
486	var tnewHost = '';
487	try {
488		tnewHost = new URL(tnewBase).hostname;
489	} catch (e) {
490		tnewHost = tnewBase.replace(/^https?:\/\//, '').split('/')[0];
491	}
492
493	// Manage Tessitura cart URL so we can append promo parameters when needed.
494	// Use premove=y ONLY when: promo was just cleared on an empty cart and user is
495	// now going to cart or applying a different promo (so TNEW can clear old promo state).
496	var initialCartLink = $('.tessitura-cell.cart').attr('data-link') || (tnewBase + '/cart/detail
496s');
497	var cartBaseUrl = initialCartLink.split('?')[0];
498	var cartBaseQuery = initialCartLink.indexOf('?') !== -1 ? initialCartLink.substring(initialCartLink.indexOf('?')) : '';
499	var cartPromoUrl = '';
500	var promoJustClearedEmptyCart = false; // true only after user clears promo while cart is empty
501
502	function refreshCartBaseFromAttr() {
503	    var currentLink = $('.tessitura-cell.cart').attr('data-link');
504	    if (!currentLink) {
505	        return;
506	    }
507	    var isPromoLinkMatch = false;
508	    if (cartPromoUrl) {
509	        if (currentLink === cartPromoUrl) {
510	            isPromoLinkMatch = true;
511	        }
512	    }
513	    if (isPromoLinkMatch) {
514	        return;
515	    }
516	    var questionIndex = currentLink.indexOf('?');
517	    if (questionIndex !== -1) {
518	        cartBaseUrl = currentLink.substring(0, questionIndex);
519	        cartBaseQuery = currentLink.substring(questionIndex);
520	    } else {
521	        cartBaseUrl = currentLink;
522	        cartBaseQuery = ''; // No premove by default - avoids child-ticket conversion issue in TNEW
523	    }
524	}
525
526	function setCartPromoLink(promoCode) {
527	    refreshCartBaseFromAttr();
528	    var hasPromoValue = false;
529	    if (promoCode) {
530	        if (promoCode !== '') {
531	            hasPromoValue = true;
532	        }
533	    }
534	    if (hasPromoValue) {
535	        // Only add premove=y when replacing a just-cleared promo (empty cart, user cleared then applied new)
536	        if (promoJustClearedEmptyCart) {
537	            cartPromoUrl = cartBaseUrl + '?premove=y&promo=' + encodeURIComponent(promoCode);
538	            promoJustClearedEmptyCart = false;
539	        } else {
540	            cartPromoUrl = cartBaseUrl + '?promo=' + encodeURIComponent(promoCode);
541	        }
542	        $('.tessitura-cell.cart').attr('data-link', cartPromoUrl);
543	    } else {
544	        cartPromoUrl = '';
545	        $('.tessitura-cell.cart').attr('data-link', cartBaseUrl + cartBaseQuery);
546	    }
547	}
548
549	function getCartDestinationUrl() {
550	    // If user just cleared promo (empty cart) and has no new promo, use premove once so TNEW clears it
551	    if (promoJustClearedEmptyCart && !cartPromoUrl) {
552	        promoJustClearedEmptyCart = false;
553	        return cartBaseUrl + '?premove=y';
554	    }
555	    return cartPromoUrl || (cartBaseUrl + cartBaseQuery);
556	}
557$('.tessitura-cell.user-menu .logged-in').on('click tap',function (e) {
558	e.preventDefault();
559	e.stopPropagation();
560	var $currentMenu = $(this).closest('.tessitura-cell.user-menu');
561	$('.tessitura-cell.user-menu .user-dropdown').not($currentMenu.find('.user-dropdown')).addClass('hide');
562	$currentMenu.find('.user-dropdown').toggleClass('hide');
563});
564
565// CONVERTED: .tessitura-cell.cart - Simplified with New TNEW Method
566$('.tessitura-cell.cart').on('click tap',function () {
567	// Block navigation while an add-to-cart is still in flight so the user can't
568	// land in an empty TNEW cart before the item is committed server-side.
569	if (window.hmnsCartBusy) {
570		return false;
571	}
572	sessionKey = getCookie('sessionKey');
573	var destinationUrl = getCartDestinationUrl();
574	var cartHasItems = !$(this).hasClass('populated');
575	if (cartHasItems) {
576		var sessionMissing = (sessionKey == '' || sessionKey == null);
577		if (sessionMissing) {
578			// Create new TNEW session and redirect to cart
579			createTNEWSession(function(newSessionKey) {
580				window.location.assign(destinationUrl);
581			});
582			return;
583		}
584	}
585	window.location.assign(destinationUrl);
586});
587	
588	// MODIFIED: Main sessionKey check AJAX call - wait for nonce to be ready
589	function checkInitialSession() {
590		sessionKey = getCookie('sessionKey');
591		if (sessionKey != '') {
592			var checkedSessionKey = sessionKey;
593			ajaxArray = [];
594			ajaxArray.push('/api/Web/Session/{sessionkey}');
595			ajaxArray.push(checkedSessionKey);
596			query = JSON.stringify(ajaxArray);
597			$.ajax({
598				type : 'post',
599				url : hmns_ajax_router.url,
600				beforeSend: setAjaxHeaders,
601				data : {
602					action : 'tessitura_api_interactions',
603					query : query			
604				},
605				xhrFields: { withCredentials: true },
606			success : function (response) {
607				// Handle new structured JSON object instead of string
608				$('.tessitura-cell.spinner').addClass('hide');
609				$('.tessitura-cell.user-menu .logged-spinner').addClass('hide');
610				// Share result so login block (and others) can use it without a duplicate API call
611				window.hmns_last_session_check = response;
612				if (isLoginFlowActive()) {
613					return;
614				}
615				if (response.isSessionValid === false) {
616					if (getCookie('sessionKey') === checkedSessionKey) {
617						document.cookie = "sessionKey=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=" + hmnsCookieDomain + "; path=/;";
618						sessionKey = '';
619					} else {
620						return;
621					}
622				}
623				if (response.isLoggedIn) {
624					$('.tessitura-cell.user-menu .logged-in').removeClass('hide');
625					$('.tessitura-cell.user-menu').attr('data-mosid',response.modeOfSaleId);
626					setCookie('modeOfSale', response.modeOfSaleId, 30); // Persist mode of sale for page reloads
627					setCookie('tessituraLogin', true, 30);
628					$('.tessitura-cell.user-menu .logged-out').addClass('hide');
629				} else {
630					$('.tessitura-cell.user-menu .logged-in').addClass('hide');
631					$('.tessitura-cell.user-menu .logged-out').removeClass('hide');
632					$('.tessitura-cell.user-menu').attr('data-mosid',2);
633					setCookie('modeOfSale', 2, 30); // Persist mode of sale for page reloads
634					setCookie('tessituraLogin', false, 30);
635				}
636				// queryDB / hmns_session_checked must run after cookies because listeners read getCookie().
637				$(document).trigger('queryDB');
638				$(document).trigger('hmns_session_checked', [response]);
639			if (response.cartCount >= 1) {
640                if (response.checkoutStatus == 'No Checkout') {
641                    $('.tessitura-cell.cart .cart-counter span').text(response.cartCount);
642                    $('.tessitura-cell.cart').addClass('populated');
643                    $('.tessitura-cell.cart').removeClass('hide');
644                    // Only show sessionTimer if timer is ready
645                    if (window.sessionTimerReady) {
646                        $('.sessionTimer').removeClass('hide');
647                    } else {
648                        $('.sessionTimer').addClass('hide');
649                    }
650                } else {
651                    $('.sessionTimer').addClass('hide');
652                    $('.tessitura-cell.cart .cart-counter span').text(0);
653                    $('.tessitura-cell.cart').removeClass('hide');    
654                    $('.tessitura-cell.cart').removeClass('populated');
655                    // Start new session only if checkout was just completed
656                    if (response.checkoutStatus == 'Completed') {
657                        startNewSession();
658                    }
659                }
660            } else {
661                $('.sessionTimer').addClass('hide');
662                $('.tessitura-cell.cart .cart-counter span').text(0);
663                $('.tessitura-cell.cart').removeClass('hide'); 
664                $('.tessitura-cell.cart').removeClass('populated');
665                // Start new session only if checkout was just completed
666                if (response.checkoutStatus == 'Completed') {
667                    startNewSession();
668                }
669            }
670			},
671			error : function(jqXHR, textStatus, errorThrown) {
672				// Hide spinners and show logged-out state so UI is never stuck
673				$('.tessitura-cell.spinner').addClass('hide');
674				$('.tessitura-cell.user-menu .logged-spinner').addClass('hide');
675				$('.tessitura-cell.user-menu .logged-in').addClass('hide');
676				$('.tessitura-cell.user-menu .logged-out').removeClass('hide');
677				$('.tessitura-cell.cart').removeClass('hide');
678				refreshCartBaseFromAttr();
679				setCartPromoLink(null);
680				// Keep the shared global in sync — gate logic reads it, not the event payload.
681				window.hmns_last_session_check = { isLoggedIn: false };
682				$(document).trigger('queryDB');
683				$(document).trigger('hmns_session_checked', [window.hmns_last_session_check]);
684			}
685		});	
686	} else {
687		$('.tessitura-cell.spinner').addClass('hide');
688		$('.tessitura-cell.user-menu .logged-spinner').addClass('hide');
689		$('.tessitura-cell.user-menu .logged-out').removeClass('hide');
690		$('.tessitura-cell.cart').removeClass('hide');
691		refreshCartBaseFromAttr();
692		setCartPromoLink(null);
693		$(document).trigger('queryDB');
694		window.hmns_last_session_check = { isLoggedIn: false };
695		$(document).trigger('hmns_session_checked', [window.hmns_last_session_check]);
696	}
697	}
698	
699	// Production can safely ask my.hmns.org for a TNEW session on every load because it is same-site.
700	// Dev's TNEW endpoint is cross-site and can mutate the TNEWQA cookie on ordinary WP navigation,
701	// so only sync from TNEW when the user is returning from TNEW.
702	var tnewSyncResult = null; // null = pending | { existingKey: true } | { encrypted: string } | { noKey: true }
703	var syncAndCheckRan = false;
704	function isTnewReturnNavigation() {
705		var referrer = document.referrer || '';
706		if (!referrer) {
707			return false;
708		}
709		try {
710			var referrerHost = new URL(referrer).hostname;
711			return (tnewHost !== '' && referrerHost === tnewHost) || referrerHost.endsWith('.tnhs.cloud');
712		} catch (e) {
713			return (tnewHost !== '' && referrer.indexOf(tnewHost) !== -1) || referrer.indexOf('.tnhs.cloud') !== -1;
714		}
715	}
716	function validateExistingSessionBeforeTnewSwap(currentSessionKey, nextSessionKey) {
717		ajaxArray = [];
718		ajaxArray.push('/api/Web/Session/{sessionkey}');
719		ajaxArray.push(currentSessionKey);
720		query = JSON.stringify(ajaxArray);
721		$.ajax({
722			type: 'post',
723			url: hmns_ajax_router.url,
724			beforeSend: setAjaxHeaders,
725			data: {
726				action: 'tessitura_api_interactions',
727				query: query
728			},
729			xhrFields: { withCredentials: true },
730			success: function(response) {
731				if (response && response.isSessionValid === true && (response.isLoggedIn || response.cartCount > 0 || response.membershipCount > 0)) {
732					sessionKey = currentSessionKey;
733					checkInitialSession();
734					return;
735				}
736				setCookie('sessionKey', nextSessionKey, 30);
737				sessionKey = nextSessionKey;
738				checkInitialSession();
739			},
740			error: function() {
741				setCookie('sessionKey', nextSessionKey, 30);
742				sessionKey = nextSessionKey;
743				checkInitialSession();
744			}
745		});
746	}
747	function runWhenNonceAndTnewReady() {
748		if (!window.hmns_ajax_router || !window.hmns_ajax_router.ready) return;
749		if (syncAndCheckRan) return;
750		if (tnewSyncResult === null) return;
751		syncAndCheckRan = true;
752		var oldSessionKey = getCookie('sessionKey') || '';
753		if (tnewSyncResult.existingKey) {
754			sessionKey = oldSessionKey;
755			checkInitialSession();
756			return;
757		}
758		if (tnewSyncResult.noKey) {
759			if (oldSessionKey) {
760				sessionKey = oldSessionKey;
761				checkInitialSession();
762				return;
763			}
764			if (isLoginFlowActive()) {
765				checkInitialSession();
766				return;
767			}
768			document.cookie = "sessionKey=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=" + hmnsCookieDomain + "; path=/;";
769			setCookie('tessituraLogin', false, 30);
770			sessionKey = '';
771			checkInitialSession();
772			return;
773		}
774		$.ajax({
775			url: hmns_ajax_router.url,
776			type: 'POST',
777			beforeSend: setAjaxHeaders,
778			data: { action: 'decrypt_tnew_string', encrypted_string: tnewSyncResult.encrypted },
779			success: function(decryptResponse) {
780				if (decryptResponse && decryptResponse.decrypted_value) {
781					var sessionKeyParts = decryptResponse.decrypted_value.split('|');
782					var retrievedSessionKey = sessionKeyParts[0];
783					if (retrievedSessionKey !== oldSessionKey) {
784						if (oldSessionKey) {
785							validateExistingSessionBeforeTnewSwap(oldSessionKey, retrievedSessionKey);
786							return;
787						}
788						setCookie('sessionKey', retrievedSessionKey, 30);
789						sessionKey = retrievedSessionKey;
790						// console.log('Session key updated from TNEW (transfer):', oldSessionKey, '->', retrievedSessionKey);
791					}
792				}
793				checkInitialSession();
794			},
795			error: function() { checkInitialSession(); }
796		});
797	}
798	if (getCookie('sessionKey') && !isTnewReturnNavigation()) {
799		tnewSyncResult = { existingKey: true };
800	} else if (isTnewReturnNavigation()) {
801		// Start TNEW GET only when we may be receiving a TNEW transfer.
802		$.ajax({
803			url: tnewBase + '/api/session/sessionkey',
804			type: 'GET',
805			xhrFields: { withCredentials: true },
806			success: function(response) {
807				var encrypted = '';
808				if (typeof response === 'string') encrypted = response;
809				else if (response && response.encryptedSessionKey) encrypted = response.encryptedSessionKey;
810				tnewSyncResult = encrypted ? { encrypted: encrypted } : { noKey: true };
811				runWhenNonceAndTnewReady();
812			},
813			error: function() {
814				tnewSyncResult = { noKey: true };
815				runWhenNonceAndTnewReady();
816			}
817		});
818	} else {
819		tnewSyncResult = { noKey: true };
820	}
821	var nonceReadyNow = false;
822	if (window.hmns_ajax_router) {
823		if (window.hmns_ajax_router.ready) {
824			nonceReadyNow = true;
825		}
826	}
827	if (nonceReadyNow) {
828		runWhenNonceAndTnewReady();
829	} else {
830		$(document).on('hmns_nonce_ready', function() {
831			runWhenNonceAndTnewReady();
832		});
833	}
834	
835	// CONVERTED: startNewSession - Simplified with New TNEW Method
836	// Post-checkout (or post-clear) intentionally wants a brand-new TNEW session,
837	// so force a mint and bypass the reuse/coalesce short-circuit.
838	function startNewSession() {
839		if (window.sessionTimerInterval) { clearInterval(window.sessionTimerInterval); }
840		window.sessionTimerReady = false;
841		$('.sessionTimer').addClass('hide').html('');
842		document.cookie = "sessionKey=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=" + hmnsCookieDomain + "; path=/;";
843		createTNEWSession(function(newSessionKey) {
844			document.cookie = "sessionTimer=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=" + hmnsCookieDomain + "; path=/;";
845			document.cookie = "promoCode=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=" + hmnsCookieDomain + "; path=/;";
846			$('.promo-flex input[type="text"]').val('').removeClass('hide');
847			$('.promo-flex .promoSubmit').removeClass('hide');
848			$('.promo-clear-code').addClass('hide').text('');
849			$('.promo-clear').addClass('hide');
850			setCartPromoLink(null);
851		}, null, { forceNew: true });
852	}
853	
854	// CONVERTED: Promo form submit - New TNEW Method
855	$('.tessitura-cell.promo form').on('submit',function (e) {
856		e.preventDefault();
857		e.stopImmediatePropagation();
858		thisform = $(this);
859		sessionKey = getCookie('sessionKey');
860		if (sessionKey == '' || sessionKey == null) {
861			createTNEWSession(function(newSessionKey) {
862				validate_promo();
863			});
864		} else {	
865			validate_promo();
866		}
867	});
868
869	// CONVERTED: Promo submit button - New TNEW Method
870	$('.tessitura-cell.promo .promo-flex .promoSubmit').on('click tap',function (e) {
871		e.preventDefault();
872		e.stopImmediatePropagation();
873		sessionKey = getCookie('sessionKey');
874		thisform = $('.tessitura-cell.promo form');
875		if (sessionKey == '' || sessionKey == null) {
876			createTNEWSession(function(newSessionKey) {
877				validate_promo();
878			});
879		} else {
880			validate_promo();
881		}
882	});
883	
884	// Handle promo code from URL parameter - wait for nonce to be ready
885	function handleQueryPromoCode() {
886		var queryPromoCode = hmnsFooterData.queryPromoCode;
887		// console.log('QueryPromo: ' + queryPromoCode);
888		if (queryPromoCode != '') {
889			sessionKey = getCookie('sessionKey');
890			thisform = $('.tessitura-cell.promo form');
891			var exCode = $('.tessitura-cell .promo-flex input[type="text"]').val();
892			// console.log('currentPromo: ' + exCode);
893			var exCookie = getCookie('promoCode');
894			if (queryPromoCode != exCode) { 
895				if (queryPromoCode != exCookie) { setCookie('promoCode',queryPromoCode,30); }
896				$('.tessitura-cell .promo-flex input[type="text"]').val(queryPromoCode);
897				if (sessionKey == '' || sessionKey == null) {
898					// CONVERTED: Use TNEW session creation method
899					createTNEWSession(function(newSessionKey) {
900						sessionKey = newSessionKey;
901						validate_promo();
902					}, function(error) {
903						// console.error('Failed to create session for promo code:', error);
904					});
905				} else {	
906					validate_promo();
907				}	
908			}
909		}
910	}
911	
912	// Wait for nonce to be ready before handling promo codes
913	if (window.hmns_ajax_router && window.hmns_ajax_router.ready) {
914		handleQueryPromoCode();
915	} else {
916		$(document).on('hmns_nonce_ready', function() {
917			handleQueryPromoCode();
918		});
919	}
920
921	var promo = '';
922	function validate_promo() {
923		sessionKey = getCookie('sessionKey');
924		var $form = $('.tessitura-cell.promo form');
925		promo = $form.find('input[name="promoCode"]').val();	
926		// console.log('check promo: ' + promo);
927		
928		// If no sessionKey, create one using TNEW method first
929		if (!sessionKey || sessionKey === '') {
930			createTNEWSession(function(newSessionKey) {
931				sessionKey = newSessionKey;
932				performPromoValidation();
933			}, function(error) {
934				// console.error('Failed to create session for promo validation:', error);
935				$('.promo-spinner').addClass('hide');
936				$('.tessitura-cell.promo form').removeClass('greyed-out');
937			});
938			return;
939		}
940		
941		performPromoValidation();
942	}
943	
944	function performPromoValidation() {
945		ajaxArray = [];
946		ajaxArray.push(promo);
947		ajaxArray.push(sessionKey);
948		query = JSON.stringify(ajaxArray);
949		if (promo != '') {
950			// MODIFIED: validate_promo AJAX call
951			$.ajax({
952				type: 'post',
953				url: hmns_ajax_router.url,
954				beforeSend: function(xhr) {
955					setAjaxHeaders(xhr); // Include nonce and security headers
956					$('.promo-spinner').removeClass('hide');
957					$('.tessitura-cell.promo form').addClass('greyed-out');
958				},
959				data: {
960					action: 'tessitura_promo',
961					query: query
962				},
963				xhrFields: { withCredentials: true },
964			success: function(response) {
965				$('.promo-spinner').addClass('hide');
966				promoCode = response.promoCode;
967				if (promoCode) {
968					setCookie('promoCode', promoCode, 30);
969					setCartPromoLink(promoCode);
970					document.cookie = "sessionTimer=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=" + hmnsCookieDomain + "; path=/;";
971					if (window.sessionTimerInterval) { clearInterval(window.sessionTimerInterval); }
972					window.sessionTimerReady = false;
973					$('.tessitura-cell.cart').removeClass('populated');
974					$('.tessitura-cell.cart .cart-counter span').text(0);
975					$('.sessionTimer').addClass('hide').html('');
976					$('.hmns-tessitura-bar .promo-flex input#promoCode').addClass('hide');
977					$('.hmns-tessitura-bar .promo-flex .promoSubmit').addClass('hide');
978					$('.hmns-tessitura-bar .promo-clear-code').text(promoCode).removeClass('hide');
979					var currentCartCount = window.hmns_last_session_check ? (window.hmns_last_session_check.cartCount || 0) : 0;
980					if (currentCartCount === 0) {
981						$('.hmns-tessitura-bar .promo-clear').removeClass('hide');
982					}
983					$('.tessitura-cell.promo form').removeClass('greyed-out');
984				} else {
985					$('.tessitura-cell.promo form').removeClass('greyed-out');
986					$('.hmns-tessitura-bar .promo-fail').removeClass('hide');
987					window.setTimeout(function() {
988						$('.hmns-tessitura-bar .promo-fail').addClass('hide');
989					}, 3000);
990				}
991				},
992				error: function(jqXHR, textStatus, errorThrown) {
993					$('.promo-spinner').addClass('hide');
994					$('.tessitura-cell.promo form').removeClass('greyed-out');
995					$('.hmns-tessitura-bar .promo-fail').removeClass('hide');
996					window.setTimeout(function() {
997						$('.hmns-tessitura-bar .promo-fail').addClass('hide');
998					}, 3000);
999				},
1000				complete: function() {
1001					$('.promo-spinner').addClass('hide');
1002					$('.tessitura-cell.promo form').removeClass('greyed-out');
1003				}
1004			});
1005		}
1006	}
1007	
1008	// Control visibility of promo clear button based on cart contents
1009	// Promo codes cannot be removed when there are items in the cart
1010	function updatePromoClearVisibility(cartCount) {
1011		var hasPromo = getCookie('promoCode');
1012		if (hasPromo && cartCount > 0) {
1013			// Cart has items - hide the X button (can't remove promo)
1014			$('.promo-clear').addClass('hide');
1015		} else if (hasPromo && cartCount === 0) {
1016			// Cart is empty - show the X button (can remove promo)
1017			$('.promo-clear').removeClass('hide');
1018		}
1019	}
1020	
1021	// Check on initial session load
1022	if (window.hmns_last_session_check) {
1023		updatePromoClearVisibility(window.hmns_last_session_check.cartCount || 0);
1024	}
1025	$(document).on('hmns_session_checked', function(e, response) {
1026		updatePromoClearVisibility(response.cartCount || 0);
1027		// If cart has items, no need for premove on next visit
1028		if ((response.cartCount || 0) >= 1) {
1029			promoJustClearedEmptyCart = false;
1030		}
1031	});
1032	
1033	// MODIFIED: .promo-clear - Clear promo; use premove only when user next goes to cart or applies new promo
1034	$('.tessitura-cell.promo .promo-flex .promo-clear').on('click tap',function (e) {
1035		e.preventDefault();
1036		e.stopImmediatePropagation();
1037		// Mark that promo was cleared on empty cart so we use premove=y only on next cart visit or new promo
1038		promoJustClearedEmptyCart = true;
1039		// Update UI immediately; cart link stays base URL (no premove) until they go to cart or apply new promo
1040		$('.promo-flex input#promoCode').val('');
1041		$('.promo-flex input#promoCode').removeClass('hide');		
1042		$('.promo-flex .promoSubmit').removeClass('hide');
1043		$(this).addClass('hide');
1044		$('.promo-clear-code').addClass('hide');
1045		document.cookie = "promoCode=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=" + hmnsCookieDomain + "; path=/;";
1046		setCartPromoLink(0);
1047		
1048		// Also clear from TNEW cart via API
1049		var currentSessionKey = getCookie('sessionKey');
1050		if (currentSessionKey && window.hmns_ajax_router && window.hmns_ajax_router.ready) {
1051			var clearPromoArray = [currentSessionKey];
1052			var clearPromoQuery = JSON.stringify(clearPromoArray);
1053			$.ajax({
1054				type: 'post',
1055				url: hmns_ajax_router.url,
1056				beforeSend: setAjaxHeaders,
1057				data: {
1058					action: 'clear_tessitura_promo',
1059					query: clearPromoQuery
1060				},
1061				success: function(response) {
1062					// console.log('Promo cleared from TNEW:', response);
1063				},
1064				error: function(jqXHR, textStatus, errorThrown) {
1065					// console.error('Failed to clear promo from TNEW:', textStatus, errorThrown);
1066				}
1067			});
1068			startNewSession(function(newSessionKey) {
1069				// console.log('New session created:', newSessionKey);
1070			});
1071		}
1072	});
1073	
1074	// Logout handler - Show confirmation modal
1075	$('.logout-link.clear-cookies').on('click', function(e) {
1076		e.preventDefault();
1077		e.stopPropagation();
1078		
1079		// Show logout confirmation modal
1080		$('.logout-modal').removeClass('hide');
1081		$('.logout-confirm-section').removeClass('hide');
1082		$('.logout-success').addClass('hide');
1083	});
1084	
1085	// Cancel logout - close modal
1086	$('.cancel-logout').on('click', function() {
1087		$('.logout-modal').addClass('hide');
1088	});
1089	
1090	// Confirm logout - execute logout and show countdown
1091	$('.confirm-logout').on('click', function() {
1092		// Hide confirmation, show success message
1093		$('.logout-confirm-section').addClass('hide');
1094		$('.logout-success').removeClass('hide');
1095		
1096		// Get sessionKey before clearing cookies
1097		var sessionKey = getCookie('sessionKey');
1098		
1099		// Call Tessitura logout API if we have a sessionKey and nonce is ready
1100		var canCallLogout = false;
1101		if (sessionKey) {
1102			if (window.hmns_ajax_router) {
1103				if (window.hmns_ajax_router.ready) {
1104					canCallLogout = true;
1105				}
1106			}
1107		}
1108		if (canCallLogout) {
1109			var ajaxArray = [];
1110			ajaxArray.push('/api/Web/Session/{sessionKey}/Logout');
1111			ajaxArray.push(sessionKey);
1112			var query = JSON.stringify(ajaxArray);
1113			
1114			$.ajax({
1115				type: 'post',
1116				url: hmns_ajax_router.url,
1117				beforeSend: setAjaxHeaders,
1118				data: {
1119					action: 'tessitura_api_interactions',
1120					query: query
1121				},
1122				success: function(response) {
1123					// console.log('Logout API call successful:', response);
1124				},
1125				error: function(xhr, status, error) {
1126					// console.error('Logout API call failed:', status, error);
1127					// Continue with logout even if API call fails
1128				},
1129				complete: function() {
1130					// Clear cookies and reset UI after API call (success or failure)
1131					performLogoutCleanup();
1132				}
1133			});
1134		} else {
1135			// No sessionKey or nonce not ready - just clear cookies and reset UI
1136			performLogoutCleanup();
1137		}
1138	});
1139	
1140	// Function to perform logout cleanup (clear cookies then full reload so gates/blocks match server state)
1141	function performLogoutCleanup() {
1142		eat_cookies(true);
1143
1144		// Clear mode-of-sale reload flags from sessionStorage (allows reload on next page with different MOS)
1145		for (var key in sessionStorage) {
1146			if (key.indexOf('mosReload_') === 0) {
1147				sessionStorage.removeItem(key);
1148			}
1149		}
1150
1151		if (window.sessionTimerInterval) {
1152			clearInterval(window.sessionTimerInterval);
1153		}
1154		window.sessionTimerReady = false;
1155
1156		window.location.reload();
1157	}
1158	
1159	// MODIFIED: update_cart_totals AJAX call - wait for nonce to be ready
1160	function updateCartTotalsOnLoad() {
1161    sessionKey = getCookie('sessionKey');
1162    var hasSessionKey = false;
1163    if (sessionKey != null) {
1164        if (sessionKey !== '') {
1165            hasSessionKey = true;
1166        }
1167    }
1168    if (hasSessionKey) {
1169        ajaxArray = [];
1170        ajaxArray.push('/api/Web/Session/{sessionkey}');
1171        ajaxArray.push(sessionKey);
1172        query = JSON.stringify(ajaxArray);
1173        $.ajax({
1174            type : 'post',
1175            url : hmns_ajax_router.url,
1176            beforeSend: setAjaxHeaders,
1177            data : {
1178                action : 'update_cart_totals',
1179                query : query
1180            },
1181            xhrFields: { withCredentials: true },
1182            success : function (response) {
1183                // Handle new structured response
1184                $('.tessitura-cell.cart .cart-counter span').text(response.cartCount);
1185                // Only show sessionTimer if cart has items AND timer is ready
1186                if (response.cartCount >= 1) {
1187                    if (window.sessionTimerReady) {
1188                        $('.sessionTimer').removeClass('hide');
1189                    } else {
1190                        $('.sessionTimer').addClass('hide');
1191                    }
1192                } else {
1193                    $('.sessionTimer').addClass('hide');
1194                }
1195                $('.tessitura-cell.cart').removeClass('hide');
1196            },
1197            error : function(jqXHR,textStatus,errorThrown) { /* ... */ }
1198        });
1199    } else {
1200        setCartPromoLink(null);
1201    }
1202}
1203	
1204	// Cart totals on initial load are updated by checkInitialSession (no separate Session API call)
1205	// CONVERTED: Mobile promo form handlers - New TNEW Method
1206	$('#hmns-promo-form-mobile').on('submit',function (e) {
1207		e.preventDefault();
1208		e.stopImmediatePropagation();
1209		thisform = $(this);
1210		sessionKey = getCookie('sessionKey');
1211		if (sessionKey == '') {
1212			createTNEWSession(function(newSessionKey) {
1213				validate_promo_mobile();
1214			});
1215		} else {	
1216			validate_promo_mobile();
1217		}
1218	});
1219
1220	$('.mobile-tessitura-bar .promo-flex .promoSubmit').on('click tap',function (e) {
1221		e.preventDefault();
1222		e.stopImmediatePropagation();
1223		thisform = $('#hmns-promo-form-mobile');
1224		sessionKey = getCookie('sessionKey');
1225		if (sessionKey == '') {
1226			createTNEWSession(function(newSessionKey) {
1227				validate_promo_mobile();
1228			});
1229		} else {
1230			validate_promo_mobile();
1231		}
1232	});
1233	
1234	// CACHE-COMPATIBLE: Mobile promo validation function
1235	function validate_promo_mobile() {
1236		sessionKey = getCookie('sessionKey');
1237		var $form = $('#hmns-promo-form-mobile');
1238		promo = $form.find('input[name="promoCode"]').val();	
1239		// console.log('check mobile promo: ' + promo);
1240		
1241		// If no sessionKey, create one using TNEW method first
1242		if (!sessionKey || sessionKey === '') {
1243			createTNEWSession(function(newSessionKey) {
1244				sessionKey = newSessionKey;
1245				performPromoValidationMobile();
1246			}, function(error) {
1247				// console.error('Failed to create session for mobile promo validation:', error);
1248				$('.mobile-tessitura-bar .promo-spinner').addClass('hide');
1249				$('#hmns-promo-form-mobile').removeClass('greyed-out');
1250			});
1251			return;
1252		}
1253		
1254		performPromoValidationMobile();
1255	}
1256	
1257	function performPromoValidationMobile() {
1258		ajaxArray = [];
1259		ajaxArray.push(promo);
1260		ajaxArray.push(sessionKey);
1261		query = JSON.stringify(ajaxArray);
1262		if (promo != '') {
1263			$.ajax({
1264				type : 'post',
1265				url : hmns_ajax_router.url,
1266				data : {
1267					action : 'tessitura_promo',
1268					query : query		
1269				},
1270				xhrFields: { withCredentials: true },
1271				beforeSend : function (xhr) {
1272					setAjaxHeaders(xhr);
1273					$('.mobile-tessitura-bar .promo-spinner').removeClass('hide');
1274					$('#hmns-promo-form-mobile').addClass('greyed-out');
1275				},
1276				success : function (response) {
1277					$('.mobile-tessitura-bar .promo-spinner').addClass('hide');
1278					promoCode = response.promoCode;
1279					if (promoCode) {
1280						setCookie('promoCode', promoCode, 30);
1281						setCartPromoLink(promoCode);
1282						$('.mobile-tessitura-bar .promo-flex input#promoCodeMobile').addClass('hide');
1283						$('.mobile-tessitura-bar .promo-flex .promoSubmit').addClass('hide');
1284						$('.mobile-tessitura-bar .promo-clear-code').text(promoCode).removeClass('hide');
1285						var currentCartCount = window.hmns_last_session_check ? (window.hmns_last_session_check.cartCount || 0) : 0;
1286						if (currentCartCount === 0) {
1287							$('.mobile-tessitura-bar .promo-clear').removeClass('hide');
1288						}
1289						$('#hmns-promo-form-mobile').removeClass('greyed-out');
1290					} else {
1291						$('.mobile-tessitura-bar .promo-fail').removeClass('hide');
1292						window.setTimeout(function () {
1293							$('#hmns-promo-form-mobile').removeClass('greyed-out');
1294							$('.mobile-tessitura-bar .promo-fail').addClass('hide');
1295						}, 3000);
1296					}
1297				},
1298				error : function (jqXHR, textStatus, errorThrown) {
1299					$('.mobile-tessitura-bar .promo-spinner').addClass('hide');
1300					$('#hmns-promo-form-mobile').removeClass('greyed-out');
1301					$('.mobile-tessitura-bar .promo-fail').removeClass('hide');
1302					window.setTimeout(function () {
1303						$('.mobile-tessitura-bar .promo-fail').addClass('hide');
1304					}, 3000);
1305				}
1306			});
1307		}
1308	}
1309	
1310	// CACHE-COMPATIBLE: Mobile promo clear handler - Now also clears promo from TNEW cart
1311	$('.mobile-tessitura-bar .promo-flex .promo-clear').on('click tap',function (e) {
1312		e.preventDefault();
1313		e.stopImmediatePropagation();
1314		
1315		promoJustClearedEmptyCart = true;
1316		// Update UI immediately
1317		$('.mobile-tessitura-bar .promo-flex input#promoCodeMobile').val('');
1318		$('.mobile-tessitura-bar .promo-flex input#promoCodeMobile').removeClass('hide');		
1319		$('.mobile-tessitura-bar .promo-flex .promoSubmit').removeClass('hide');
1320		$(this).addClass('hide');
1321		$('.mobile-tessitura-bar .promo-clear-code').addClass('hide');
1322		document.cookie = "promoCode=; expires=Thu, 01 Jan 1970 00:00:00 UTC; domain=" + hmnsCookieDomain + "; path=/;";
1323		setCartPromoLink(null);
1324		
1325		// Also clear from TNEW cart via API
1326		var currentSessionKey = getCookie('sessionKey');
1327		if (currentSessionKey && window.hmns_ajax_router && window.hmns_ajax_router.ready) {
1328			var clearPromoArray = [currentSessionKey];
1329			var clearPromoQuery = JSON.stringify(clearPromoArray);
1330			$.ajax({
1331				type: 'post',
1332				url: hmns_ajax_router.url,
1333				beforeSend: setAjaxHeaders,
1334				data: {
1335					action: 'clear_tessitura_promo',
1336					query: clearPromoQuery
1337				},
1338				success: function(response) {
1339					// console.log('Promo cleared from TNEW (mobile):', response);
1340				},
1341				error: function(jqXHR, textStatus, errorThrown) {
1342					// console.error('Failed to clear promo from TNEW (mobile):', textStatus, errorThrown);
1343				}
1344			});
1345		}
1346	});
1347	function eat_cookies(forceSessionClear) {
1348		// Preserve the session key only while a login is mid-flight (unless forced).
1349		if (forceSessionClear || !isLoginFlowActive()) {
1350			window.hmnsDeleteCookieEverywhere('sessionKey');
1351		}
1352		window.hmnsDeleteCookieEverywhere('sessionTimer');
1353		window.hmnsDeleteCookieEverywhere('promoCode');
1354		window.hmnsDeleteCookieEverywhere('tessituraLogin');
1355		window.hmnsDeleteCookieEverywhere('modeOfSale');
1356		// Reset mode of sale to the public default so product pages behave predictably.
1357		document.cookie = "modeOfSale=2; path=/; domain=" + hmnsCookieDomain + ";";
1358	} 
1359	function getCookie(name) {
1360	    var dc = document.cookie;
1361	    var prefix = name + "=";
1362	    var begin = dc.indexOf("; " + prefix);
1363	    if (begin == -1) {
1364	        begin = dc.indexOf(prefix);
1365	        if (begin != 0) return null;
1366	    } else {
1367	        begin += 2;
1368	    }
1369	    var end = dc.indexOf(";", begin);
1370	    if (end == -1) {
1371	        end = dc.length;
1372	    }
1373	    // because unescape has been deprecated, replaced with decodeURI
1374	    //return unescape(dc.substring(begin + prefix.length, end));
1375	    return decodeURI(dc.substring(begin + prefix.length, end));
1376	}
1377	function setCookie(name, value, minutes) {
1378	   let expires = "";
1379		if (minutes) {
1380	   	const date = new Date();
1381			date.setTime(date.getTime() + (minutes * 60 * 1000)); // Convert minutes to milliseconds
1382			expires = "; expires=" + date.toUTCString();
1383		}
1384		let cookieValue = value;
1385		if (typeof cookieValue === 'boolean') {
1386			cookieValue = cookieValue ? 'true' : 'false';
1387		}
1388		document.cookie = name + "=" + (cookieValue == null ? "" : cookieValue) + expires + "; path=/; domain=" + hmnsCookieDomain + ";";
1389	} 
1390	// CONVERTED: Add isDaylightSavingTimeCentral function (from functions.php)
1391	function isDaylightSavingTimeCentral() {
1392		const today = new Date();
1393		const year = today.getFullYear();
1394		
1395		// DST starts on the second Sunday in March
1396		const march = new Date(year, 2, 1); // March 1st
1397		const dstStart = new Date(year, 2, 14 - march.getDay()); // Second Sunday
1398		
1399		// DST ends on the first Sunday in November
1400		const november = new Date(year, 10, 1); // November 1st
1401		const dstEnd = new Date(year, 10, 7 - november.getDay()); // First Sunday
1402		
1403		if (today >= dstStart) {
1404        if (today < dstEnd) {
1405            return true;
1406        }
1407    }
1408    return false;
1409}
1410});

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.