/* global window, document, navigator, rewardswpOpenPanelOnPageLoad, rewardswpIsPanelVisible, rewardswpMainVars */

// Helper functions
const showPanel = () => {
	if ('#rewardswp-home' === window.location.hash) {
		return true;
	}

	const params = new URLSearchParams(window.location.search);
	const rewardswpParam = params.get('rewardswp');
	const validPanels = ['home', 'rewards', 'earn', 'redeem', 'join', 'login'];

	if (validPanels.includes(rewardswpParam) || (rewardswpParam && rewardswpParam.startsWith('rewards-'))) {
		return true;
	}

	return typeof rewardswpOpenPanelOnPageLoad !== 'undefined' && rewardswpOpenPanelOnPageLoad !== null
		? rewardswpOpenPanelOnPageLoad ?? false
		: false;
};

const isPanelVisible = () => {
	return typeof rewardswpIsPanelVisible !== 'undefined' && rewardswpIsPanelVisible !== null
		? rewardswpIsPanelVisible ?? true
		: true;
};

/**
 * Set a cookie with a given name, value, and expiration in days.
 *
 * Uses COOKIEPATH and COOKIE_DOMAIN from WordPress when available
 * (passed via rewardswpCookieConfig), falling back to sensible defaults.
 *
 * @param {string} name  Cookie name.
 * @param {string} value Cookie value.
 * @param {number} days  Expiration in days.
 */
const setNamedCookie = ( name, value, days ) => {
	const expirationDate = new Date();
	expirationDate.setTime( expirationDate.getTime() + ( days * 24 * 60 * 60 * 1000 ) );

	const cookiePath = window.rewardswpCookieConfig?.path || '/';
	const cookieDomain = window.rewardswpCookieConfig?.domain || window.location.hostname;

	document.cookie = name + '=' + value + ';expires=' + expirationDate.toUTCString() + ';path=' + cookiePath + ';domain=' + cookieDomain;
};

const setCookie = ( cookieVal ) => {
	setNamedCookie( 'rewardswp_member_access_token', cookieVal, 400 );
};

const getCookie = (cookieName) => {
	const name = cookieName + '=';
	const decodedCookie = decodeURIComponent(document.cookie);
	const cookieArray = decodedCookie.split(';');

	for (let i = 0; i < cookieArray.length; i++) {
		const cookie = cookieArray[ i ].trim();
		if (cookie.indexOf(name) === 0) {
			return cookie.substring(name.length, cookie.length);
		}
	}

	return null;
};

// Helper function from utils.js
const toKebabCase = (str) => {
	return str
		.replace(/([a-z])([A-Z])/g, '$1-$2')
		.replace(/[\s_]+/g, '-')
		.toLowerCase();
};

// Initialize member access token cookie
document.addEventListener('DOMContentLoaded', function () {
	const memberAccessToken = localStorage?.getItem('rewardswp_member_access_token');
	const tokenCookie = getCookie('rewardswp_member_access_token');

	if (memberAccessToken && (!tokenCookie || '' === tokenCookie)) {
		setCookie(memberAccessToken);
	}

	// Sync referral code from localStorage to cookie if cookie is missing.
	const referralCode = localStorage?.getItem('rewardswp_referral_code');
	const referralCookie = getCookie('rewardswp_referral_code');

	if (referralCode && (!referralCookie || '' === referralCookie)) {
		setNamedCookie( 'rewardswp_referral_code', referralCode, 30 );
	}
});

// Export function to register all rewards panel components
export function registerRewardsPanelComponents(Alpine) {
	Alpine.data('copyHandler', () => ({
		copied: false,
		copyContents(value) {
			if (!value) {
				return;
			}

			navigator.clipboard
				.writeText(value)
				.then(() => {
					this.copied = true;
					setTimeout(() => {
						this.copied = false;
					}, 2000);
				})
				.catch(() => {
					console.error('Failed to copy');
				});
		},
	}));

	Alpine.data('rewardsPanel', (props = {}) => ({
		show: showPanel(),
		sharing: [ 'x', 'facebook', 'email' ],
		isVisible: showPanel() || isPanelVisible(),
		launcherPosition: 'right',
		launcherClass: '',
		styleData: {},
		...props,
		isLoading: false,
		error: null,
		memberAccessToken: localStorage?.getItem('rewardswp_member_access_token'),
		toggle() {
			this.show = !this.show;
		},
		hide() {
			this.show = false;

			if (window.location.hash) {
				history.replaceState(null, '', window.location.href.split('#')[ 0 ]);
			}
		},
		updateUI(args = {}) {
			Object.keys(args).forEach((key) => {
				if (typeof this[ key ] !== 'undefined') {
					this[ key ] = args[ key ];
				}
			});

			this.calculateStyles();
		},
		calculateStyles() {
			this.styleData = {
				position: document.body.classList.contains('wp-admin') ? 'absolute' : 'fixed',
				zIndex: 9999,
				bottom: '24px',
				right: this.launcherPosition === 'right' ? '24px' : 'auto',
				left: this.launcherPosition === 'left' ? '24px' : 'auto',
				marginLeft: this.launcherPosition === 'right' ? '24px' : 'auto',
				marginRight: this.launcherPosition === 'left' ? '24px' : 'auto',
			};

			this.launcherClass = this.launcherPosition === 'left' ? 'self-start' : '';
		},
		async maybeLoadAdvocateByToken() {
			if (!this.memberAccessToken) {
				return;
			}

			this.isLoading = true;
			this.error = null;

			try {
				const response = await fetch(rewardswpMainVars.ajaxUrl, {
					method: 'POST',
					headers: {
						'Content-Type': 'application/x-www-form-urlencoded',
					},
					body: new URLSearchParams({
						action: 'rewardswp_get_advocate_data_by_token',
						nonce: rewardswpMainVars.nonce,
						token: this.memberAccessToken,
					}),
				});

				const result = await response.json();

				if (!result.success) {
					throw new Error(result.data.message || 'Failed to load advocate data');
				}

				this.advocate = result.data.advocate;
			} catch (error) {
				console.error('Error loading advocate data:', error);
				this.error = error.message;
				this.advocate = null;
				this.token = null;
			} finally {
				this.isLoading = false;
			}
		},
		init() {
			this.calculateStyles();

			window.addEventListener('hashchange', () => {
				this.$dispatch('show-launcher');
				this.show = showPanel();
				this.isVisible = showPanel();
			});
		},
		get styles() {
			return Object.entries(this.styleData)
				.map(([ key, value ]) => `${toKebabCase(key)}: ${value}`)
				.join('; ');
		},
	}));

	Alpine.data('launcher', (props = {}) => ({
		isVisible: showPanel() || isPanelVisible(),
		...props,
		hide() {
			this.isVisible = false;
		},
		show() {
			this.isVisible = true;
		},
		toggleRewardsPanel() {
			this.$dispatch('toggle-rewards-panel');
		},
	}));

	Alpine.data('rewardsPanelSteps', (steps) => ({
		currentStep: 'invite',
		steps,
		direction: 'right',
		isSharing: false,

		getTransition(direction) {
			return direction === 'right'
				? {
					'x-transition:enter': 'transition ease-out',
					'x-transition:enter-start': 'transform translate-x-64 opacity-0',
					'x-transition:enter-end': 'transform translate-x-0 opacity-100',
				}
				: {
					'x-transition:enter': 'transition ease-out',
					'x-transition:enter-start': 'transform -translate-x-64 opacity-0',
					'x-transition:enter-end': 'transform translate-x-0 opacity-100',
				};
		},

		initWatcher() {
			this.$watch('currentStep', (value, old) => {
				const keys = Object.keys(this.steps);
				this.direction = keys.indexOf(value) > keys.indexOf(old) ? 'right' : 'left';
			});
		},
	}));

	Alpine.data('couponUtils', (props = {}) => ({
		isApplying: false,
		isApplied: false,
		isPending: false, // New: reward is pending (waiting for minimum spend).
		pendingMessage: '', // New: message to show in pending state.
		pendingRewardId: 0, // ID of the currently pending reward from session.
		dismissedRewardId: 0, // ID of the dismissed reward from session.
		isDismissed: false, // True if this reward was dismissed by user removing coupon from cart.
		buttonText: 'Apply coupon',
		appliedButtonText: 'Coupon applied',
		hasErrors: false,
		errorMessage: '',
		// State for "product added but coupon failed" scenario.
		productAddedButCouponFailed: false,
		productWasNewlyAdded: false,
		couponErrorMessage: '',
		remainingToUnlock: 0, // Amount remaining to meet minimum spend.
		freeProductCouponFailed: false, // Synced from parent via event.
		nonce: '',
		rewardId: 0,
		couponCode: '', // The coupon code for this reward (used for WooCommerce Blocks detection).
		// Cart-aware properties.
		cartItemCount: 0,
		cartProductIds: [],
		cartSubtotal: 0, // Current cart subtotal for calculating remaining to unlock.
		freeProductId: null,
		minimumOrderAmount: 0,
		integrationSource: 'woocommerce',
		checkoutUrl: '',
		cartUrl: '',
		storeUrl: '',
		currencySymbol: '$', // Currency symbol for display.
		// Button text properties for free_product rewards.
		addToCartText: 'Activate free product',
		applyToCartText: 'Claim free product',
		isDemo: false,
		...props,


		// Computed property for cart state
		get isCartEmpty() {
			return this.cartItemCount === 0;
		},

		// Computed property to check if using WooCommerce
		get isWooCommerce() {
			return this.integrationSource === 'woocommerce';
		},

		/**
		 * Get the appropriate button text for free_product rewards based on cart state and minimum spend.
		 *
		 * Logic:
		 * - If product is already in cart → "Apply to cart"
		 * - If minimum spend not met → "Claim reward" (activates but doesn't add to cart)
		 * - If minimum spend met (or no minimum) → "Add to cart"
		 *
		 * @param {number|string} freeProductId - The ID of the free product.
		 * @param {number} minOrderAmount - The minimum order amount (optional, falls back to this.minimumOrderAmount).
		 * @returns {string} The button text to display.
		 */
		getFreeProductButtonText(freeProductId, minOrderAmount = null) {
			// Use passed minimum or fall back to component's minimumOrderAmount
			const minimum = minOrderAmount !== null ? minOrderAmount : this.minimumOrderAmount;

			// Check if product is in cart
			if (this.isProductInCart(freeProductId)) {
				return this.applyToCartText;
			}

			// Check if minimum spend requirement is not met
			if (minimum > 0 && this.cartSubtotal < minimum) {
				return this.claimRewardText || 'Claim reward';
			}

			return this.addToCartText;
		},

		/**
		 * Check if a product is in the cart.
		 * Supports both exact ID match and parent product ID match for variants.
		 *
		 * @param {number|string} productId - The product ID to check.
		 * @returns {boolean} True if product is in cart.
		 */
		isProductInCart(productId) {
			if (!productId || !this.cartProductIds || this.cartProductIds.length === 0) {
				return false;
			}
			// Convert product ID to number for consistent comparison.
			const numericId = parseInt(productId, 10);
			if (isNaN(numericId)) {
				return false;
			}
			// Check if any cart product ID matches (convert all to numbers for comparison).
			return this.cartProductIds.some(cartId => {
				const cartNumericId = parseInt(cartId, 10);
				return !isNaN(cartNumericId) && cartNumericId === numericId;
			});
		},

	applyCoupon(couponCode, overrideRewardId = null) {
		this.isApplying = true;
		this.hasErrors = false;
		this.errorMessage = '';
		this.isPending = false;
		this.pendingMessage = '';

		// Mock apply for demo/admin preview — simulate success without AJAX.
		if (this.isAdminPreview || this.isDemo) {
			setTimeout(() => {
				this.$dispatch('reset-buttons', { except: this });

				// Check if minimum spend requirement is met.
				// Read from parent scope's redeemSuccessData as it has the current value
				// (props were evaluated at mount time when redeemSuccessData was empty).
				// In Alpine's merged scope, parent data is accessible via this.
				const parentMinAmount = this.redeemSuccessData?.minimum_order_amount;
				const effectiveMinAmount = parentMinAmount || this.minimumOrderAmount || 0;
				const hasMinSpend = effectiveMinAmount > 0;
				const minSpendMet = this.cartSubtotal >= effectiveMinAmount;

				if (hasMinSpend && !minSpendMet) {
					// Minimum spend not met — set to pending state.
					this.isPending = true;
					this.pendingMessage = '';
					this.remainingToUnlock = effectiveMinAmount - this.cartSubtotal;
				} else {
					// No minimum spend or already met — set to applied state.
					this.isApplied = true;
					window.dispatchEvent(new CustomEvent('coupon-applied'));
					if (this.cartItemCount === 0) {
						this.cartItemCount = 1;
					}
					if (this.freeProductId && !this.cartProductIds.includes(parseInt(this.freeProductId, 10))) {
						this.cartProductIds.push(parseInt(this.freeProductId, 10));
					}
				}
				this.isApplying = false;
			}, 400);
			return;
		}

		// Read fresh nonce from web component attribute (updated after registration).
		const webComponent = document.querySelector('rewards-panel');
		const freshNonce = webComponent ? webComponent.getAttribute('data-claim-nonce') : this.nonce;

		// Clear dismissed state locally - server handles clearing it in the same request.
		this.isDismissed = false;

		const formData = new FormData();

		// Use override rewardId if provided (for redeem success panel where x-data is evaluated once).
		const effectiveRewardId = overrideRewardId !== null ? overrideRewardId : this.rewardId;

		formData.append('action', 'rewardswp_apply_coupon_to_cart');
		formData.append('_ajax_nonce', freshNonce);
		formData.append('coupon_code', couponCode);
		formData.append('reward_id', effectiveRewardId);

			fetch(rewardswpMainVars.ajaxUrl, {
				method: 'POST',
				body: formData,
			})
				.then((response) => response.json())
				.then((result) => {
					if (result.success) {
						const state = result.data?.state || 'applied';

						// Handle pending state (minimum spend not met, product NOT added).
						if (state === 'pending') {
							this.isPending = true;
							this.pendingMessage = result.data?.message || '';
							this.remainingToUnlock = parseFloat(result.data?.remaining) || 0;
							// Product is NOT added in pending state.
							this.productAddedButCouponFailed = false;
							this.productWasNewlyAdded = false;

							// Notify parent component.
							window.dispatchEvent(new CustomEvent('reward-set-pending', {
								detail: {
									rewardId: effectiveRewardId,
									remaining: this.remainingToUnlock,
									message: this.pendingMessage
								}
							}));

							return;
						}

						// Handle applied state (success).
						// Reset other buttons.
						this.$dispatch('reset-buttons', { except: this });

						this.buttonText = this.appliedButtonText;
						this.isApplied = true;

						// Notify other components that coupon was applied (hides copy code drawer).
						window.dispatchEvent(new CustomEvent('coupon-applied'));

						// Update cart count - the product was added to cart by the backend.
						// Ensure at least 1 item in cart so "Go to checkout" buttons appear.
						if (this.cartItemCount === 0) {
							this.cartItemCount = 1;
						}

						// Update cartProductIds to include the newly added product.
						// This ensures isProductInCart() works correctly on subsequent checks.
						if (this.freeProductId && !this.cartProductIds.includes(parseInt(this.freeProductId, 10))) {
							this.cartProductIds.push(parseInt(this.freeProductId, 10));
						}
					} else {
						// Coupon failed to apply.
						const productWasAdded = result.data?.product_was_added || false;

						if (productWasAdded) {
							// Check if product was already in cart before this action.
							const productWasAlreadyInCart = this.freeProductId && this.cartProductIds.includes(parseInt(this.freeProductId, 10));

							// Product added (or was already there) but coupon failed.
							this.productAddedButCouponFailed = true;
							this.productWasNewlyAdded = !productWasAlreadyInCart;
							this.couponErrorMessage = result.data?.message || 'Could not apply coupon.';

							// Calculate remaining amount to unlock (minimum - cart total).
							const cartTotal = parseFloat(result.data?.cart_total) || 0;
							const minimumAmount = parseFloat(this.minimumOrderAmount) || 0;
							this.remainingToUnlock = Math.max(0, Math.ceil(minimumAmount - cartTotal));

							// Notify parent component that free product coupon failed.
							window.dispatchEvent(new CustomEvent('free-product-coupon-failed', {
								detail: { remainingToUnlock: this.remainingToUnlock }
							}));

							// Update cart state since product was added.
							if (this.cartItemCount === 0) {
								this.cartItemCount = 1;
							}
							if (this.freeProductId && !productWasAlreadyInCart) {
								this.cartProductIds.push(parseInt(this.freeProductId, 10));
							}
						} else {
							// Regular error.
							this.hasErrors = true;
							this.errorMessage = result.data?.message || 'Failed to apply coupon.';
						}
					}
				})
				.catch(() => {
					this.hasErrors = true;
					this.errorMessage = 'Network error. Please try again.';
				})
				.finally(() => {
					this.isApplying = false;
				});
		},

		init() {
			// Convert both to integers for reliable comparison (handles string/number mismatch).
			const pendingId = parseInt(this.pendingRewardId, 10);
			const currentId = parseInt(this.rewardId, 10);

			if (pendingId > 0 && currentId > 0 && pendingId === currentId) {
				this.isPending = true;
			}

			// Check if this reward is dismissed (user removed coupon from cart).
			// Dismissed takes precedence over pending - if dismissed, clear pending state.
			const dismissedId = parseInt(this.dismissedRewardId, 10);
			if (dismissedId > 0 && currentId > 0 && dismissedId === currentId) {
				this.isDismissed = true;
				this.isPending = false;
			}

			// Listen for free product coupon failed event to sync state.
			window.addEventListener('free-product-coupon-failed', () => {
				this.freeProductCouponFailed = true;
			});

			// Listen for show-redeem-success to reset state.
			window.addEventListener('show-redeem-success', (e) => {
				this.isApplied = false;
				this.isApplying = false;
				this.freeProductCouponFailed = false;
				this.isPending = false;
				this.isDismissed = false;
				this.pendingMessage = '';
				this.productAddedButCouponFailed = false;
				this.hasErrors = false;
				this.errorMessage = '';

				// In demo mode, update cart/checkout URLs to include the redeemed action ID.
				if (this.isDemo && e.detail?.actionId) {
					const rid = e.detail.actionId;
					this.cartUrl = this.cartUrl.replace(/([?&])redeemed=[^&]*/g, '').replace(/[?&]$/, '') + '&redeemed=' + rid;
					this.checkoutUrl = this.checkoutUrl.replace(/([?&])redeemed=[^&]*/g, '').replace(/[?&]$/, '') + '&redeemed=' + rid;
				}
			});

			// Listen for reward auto-applied event (from cart changes).
			window.addEventListener('reward-auto-applied', (e) => {
				if (e.detail?.rewardId === this.rewardId) {
					this.isPending = false;
					this.isDismissed = false;
					this.pendingMessage = '';
					this.productAddedButCouponFailed = false;
					this.isApplied = true;
					this.buttonText = this.appliedButtonText;

					// Notify other components that coupon was applied (hides copy code drawer).
					window.dispatchEvent(new CustomEvent('coupon-applied'));
				}
			});

			// Listen for reward-dismissed event (when coupon is removed from cart).
			window.addEventListener('reward-dismissed', (e) => {
				if (e.detail?.rewardId === this.rewardId) {
					this.isApplied = false;
					this.isPending = false;
					this.isDismissed = true;
					this.productAddedButCouponFailed = false;
				}
			});

			// Listen for WooCommerce Blocks Store API cart updates.
			// When the cart changes, check if our coupon was removed.
			if (typeof wp !== 'undefined' && wp.data && wp.data.subscribe) {
				let previousCoupons = [];

				// Get initial coupon state.
				const store = wp.data.select('wc/store/cart');
				if (store && typeof store.getCartCoupons === 'function') {
					previousCoupons = (store.getCartCoupons() || []).map(c => c.code.toLowerCase());
				}

				wp.data.subscribe(() => {
					const store = wp.data.select('wc/store/cart');
					if (!store || typeof store.getCartCoupons !== 'function') return;

					const currentCoupons = (store.getCartCoupons() || []).map(c => c.code.toLowerCase());

					// Check if this reward's coupon was just removed.
					if (this.isApplied && this.couponCode) {
						const couponLower = this.couponCode.toLowerCase();
						const wasApplied = previousCoupons.includes(couponLower);
						const isNowApplied = currentCoupons.includes(couponLower);

						if (wasApplied && !isNowApplied) {
							// Coupon was removed - mark as dismissed.
							this.isApplied = false;
							this.isDismissed = true;
							this.isPending = false;
							this.productAddedButCouponFailed = false;
						}
					}

					previousCoupons = currentCoupons;
				});
			}
		},
	}));

	Alpine.data('memberCenter', () => ({
		currentTab: 'home',
		direction: 'right', // For transitions.
		init() {
			this.$watch('currentTab', (newValue, oldValue) => {
				const tabs = [ 'home', 'earn', 'refer', 'redeem', 'more' ];
				this.direction = tabs.indexOf(newValue) > tabs.indexOf(oldValue) ? 'right' : 'left';
			});
		},
		getTransition() {
			// Replicate original panel's slide + fade transition.
			return this.direction === 'right'
				? {
					  'x-transition:enter': 'transition ease-out duration-300', // Added duration
					  'x-transition:enter-start': 'transform translate-x-64 opacity-0',
					  'x-transition:enter-end': 'transform translate-x-0 opacity-100',
					  // No explicit leave defined, similar to original
				  }
				: {
					  'x-transition:enter': 'transition ease-out duration-300', // Added duration
					  'x-transition:enter-start': 'transform -translate-x-64 opacity-0',
					  'x-transition:enter-end': 'transform translate-x-0 opacity-100',
					  // No explicit leave defined, similar to original
				  };
		},
	}));

	// Main rewards panel widget component
	Alpine.data('rewardsPanelWidget', (config = {}) => {
	// Check localStorage for just-joined flag to set initial panel state
	let initialPanel = 'main';
	let initialShowWelcomeBanner = false;
	let initialOpen = false; // Always start closed to trigger open animation
	const memberId = config.memberId || 0;

	// Don't process localStorage flag on confirmation pages (it will be processed when they navigate away)
	if (memberId > 0 && !config.isConfirmationPage) {
		const justJoinedKey = 'rewardswp_just_joined_' + memberId;
		const justJoined = localStorage.getItem(justJoinedKey);
		if (justJoined === 'true') {
			localStorage.removeItem(justJoinedKey);
			initialPanel = 'ways-to-earn';
			initialShowWelcomeBanner = true;
			initialOpen = true; // Auto-open when navigating away from confirmation page
		}
	}

	// Check if guest member should see welcome flow (set by server after checkout)
	if (config.showWelcomeFlow && memberId > 0) {
		const justJoinedKey = 'rewardswp_just_joined_' + memberId;
		localStorage.setItem(justJoinedKey, 'true');
		initialPanel = 'ways-to-earn';
		initialShowWelcomeBanner = true;
		// Only auto-open if NOT on confirmation page
		initialOpen = !config.isConfirmationPage;
	}

	// Apply explicit initialPanel config
	const hasExplicitInitialPanel = !!config.initialPanel;
	if (hasExplicitInitialPanel) {
		initialPanel = config.initialPanel;
	}

	// Simple mobile detection before return (will be refined in init)
	const checkIsMobile = () => {
		return window.innerWidth < 768 || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
	};
	const initialIsMobile = checkIsMobile();

	return {
	open: initialOpen, // Use calculated value instead of config.isAdminPreview
	isMobile: initialIsMobile, // Set immediately for x-if templates
	currentPanel: initialPanel,
	hasExplicitInitialPanel: hasExplicitInitialPanel, // Track if initial panel was explicitly set
		isAdminPreview: config.isAdminPreview || false,
		isDemo: config.isDemo || false,
		memberId: config.memberId || 0,
		isGuestMember: config.isGuestMember || false,
		redeemSuccessData: {
			couponCode: '',
			pointsSpent: 0,
			newBalance: 0,
			message: ''
		}, // Data for redeem success screen
		freeProductCouponFailed: false, // Track if free product coupon failed due to minimum spend
		hasNativeShare: false,
		sharing: config.sharing ?? {
			x: true,
			facebook: true,
			email: true,
		},
		// Reactive rewards data
	availableRewards: config.availableRewards || [],
	availableRewardsCount: config.availableRewardsCount || 0,
	sharePointsNonce: config.sharePointsNonce || '',
	advocateNonce: config.advocateNonce || '',
	claimNonce: config.claimNonce || '',
	navigationDirection: "initial", // 'initial', 'forward', or 'back'
	headerCollapsed: false,
	scrollProgress: 0, // 0 = fully expanded, 1 = fully collapsed
	scrollTop: 0, // Current scroll position in pixels
	savedMainScrollPosition: 0, // Saved scroll position for restoring when navigating back to main
	isOpening: false, // Flag to prevent transitions during opening
	hasScrollAnimation: true, // Whether to enable scroll animation based on content height
	hasScrolled: false, // Track if user has scrolled at all
	headerScrollThreshold: 120, // Delay fixed header reveal slightly
	showFixedHeader: false, // Controls fixed header visibility with animation support
	isScrolledToBottom: false, // Track if user has scrolled to the bottom of the main panel
	showWelcomeBanner: initialShowWelcomeBanner, // Controls welcome banner visibility on ways-to-earn panel
	shouldClearWelcomeFlowOnClose: false, // Track if we need to clear the welcome flow meta
	loading: false, // Loading state for guest upgrade
	errorMessage: '', // Error message for guest upgrade (shown if upgrade fails)
	lockedReward: null, // The reward the guest was trying to redeem (stored for create-account-prompt panel)
		dynamicBackgroundHeight: 145, // Measured height of banner content (will be updated dynamically)

		// Activity Panel State
		activityTab: 'points', // Current tab: 'points', 'referrals'
		activityItems: [], // Activity items for current tab
		activityLoading: false, // Loading state for initial load
		activityLoadingMore: false, // Loading state for "load more"
		activityHasMore: false, // Whether more items are available
		activityPage: 1, // Current page for pagination
		activityPerPage: 10, // Items per page
		activityTotal: 0, // Total count
		activityStatusCounts: null, // Status counts for referrals tab (total, completed, pending)
		activityCache: {}, // Cache loaded data per tab to avoid re-fetching
		activityData: config.activity || null, // Config-provided activity data
		activityShowFade: true, // Default visible; updateActivityFade() hides if no overflow
		activityShowTopFade: false, // Top fade shown when scrolled down
		needsActivityRefresh: false, // Flag to indicate activity needs refresh after redeem

		// Birthday Picker State
		birthdayMonth: config.birthdayMonth || '',
		birthdayDay: config.birthdayDay || '',
		birthdayOriginalMonth: config.birthdayMonth || '',
		birthdayOriginalDay: config.birthdayDay || '',
		birthdaySubmitting: false,
		birthdaySuccess: false,
		birthdayError: '',
		birthdaySuccessMessage: '',
		birthdayNotice: '',
		birthdayIsEditing: !!(config.birthdayMonth && config.birthdayDay),
		birthdayCanEdit: config.birthdayCanEdit || false,
		birthdayPoints: config.birthdayPoints || 0,
		birthdayMinDays: config.birthdayMinDays || 30,
		birthdayDayFirst: config.birthdayDayFirst || false,
		birthdayRewardDate: '', // Set after successful save from server response

		get birthdayChanged() {
			return this.birthdayMonth !== this.birthdayOriginalMonth || this.birthdayDay !== this.birthdayOriginalDay;
		},

		get birthdayDaysInMonth() {
			if (!this.birthdayMonth) return Array.from({ length: 31 }, (_, i) => i + 1);
			const month = parseInt(this.birthdayMonth, 10);
			const daysInMonth = new Date(2000, month, 0).getDate();
			return Array.from({ length: daysInMonth }, (_, i) => i + 1);
		},

		/**
		 * Live reward preview computed from selected month/day.
		 *
		 * Returns null if month/day not selected, otherwise an object with:
		 * - rewardDate: formatted date string (e.g., "March 3, 2027")
		 * - isNextYear: boolean
		 */
		get birthdayRewardPreview() {
			if (!this.birthdayMonth || !this.birthdayDay) return null;
			if (!this.birthdayPoints) return null;

			const month = parseInt(this.birthdayMonth, 10);
			const day = parseInt(this.birthdayDay, 10);
			const today = new Date();
			today.setHours(0, 0, 0, 0);

			const currentYear = today.getFullYear();
			const birthdayThisYear = new Date(currentYear, month - 1, day);
			const birthdayNextYear = new Date(currentYear + 1, month - 1, day);

			// Find the upcoming birthday (this year if not passed, next year if passed).
			const upcoming = birthdayThisYear >= today ? birthdayThisYear : birthdayNextYear;
			const daysUntil = Math.floor((upcoming - today) / (1000 * 60 * 60 * 24));

			let rewardDate;
			let isNextYear;
			let dueToMinDays = false;

			if (daysUntil >= this.birthdayMinDays) {
				rewardDate = upcoming;
				isNextYear = upcoming.getFullYear() > currentYear;
			} else {
				// Too soon — reward on next occurrence.
				rewardDate = upcoming === birthdayThisYear ? birthdayNextYear : new Date(currentYear + 2, month - 1, day);
				isNextYear = true;
				dueToMinDays = true;
			}

			// Format respecting WP date order: day-first (en-GB) or month-first (en-US).
			const locale = this.birthdayDayFirst ? 'en-GB' : 'en-US';
			const formatted = rewardDate.toLocaleDateString(locale, {
				month: 'short',
				day: 'numeric',
				year: 'numeric',
			});

			return { rewardDate: formatted, isNextYear, dueToMinDays };
		},

		birthdayBanner: config.birthdayBanner || null, // Non-null on member's birthday (all day)

		// Scroll animation configuration
		textFadeDistance: 100, // Extend fade range to keep header visible longer
		textFadeStartDelay: 0, // Pixels to scroll before fade starts
		parallaxMultiplier: 0.6, // Background moves slower than scroll for parallax effect
		parallaxEnabled: false, // Master toggle for all parallax effects (background + text)

		backgroundImageUrl: '',
		showFadeOverlay: false,

		// Background Image System (Research-Based)
		backgroundImage: {
			enabled: false, // Toggle for background image
			url: '',
		},

		// Text Readability Overlay System (Only when background image exists)
		textOverlay: {
			mode: 'auto', // 'auto', 'manual', 'disabled'
			opacity: 0.6, // 0-0.8 range
			style: 'translucent', // 'translucent', 'gradient-scrim', 'floor-fade'
			blendMode: 'normal', // CSS blend modes
			color: '#000000', // Overlay color
		},

		logoUrl: '',
		showLogo: false,
		programName: config.programName || 'Rewards',
		welcomeGreeting: config.welcomeGreeting || '',
		programTagline: 'Earn perks on every order',
		currencySingular: config.currencySingular || 'point',
		currencyPlural: config.currencyPlural || 'points',
		currencySymbol: config.currencySymbol || '$',
		currencyPosition: config.currencyPosition || 'before', // 'before' or 'after', resolved server-side from the store setting.
		currencySpace: config.currencySpace || false,
		yourLabel: config.yourLabel || 'Your',
		currencyAbbreviation: 'PTS',
		currencyIcon: '',
		globalTier: config.globalTier || 'lite', // Get current tier from test controls

		// Friend claim data for friend claim mode
		friendClaimData: config.friendClaimData || {
			referralCode: '',
			advocateName: '',
			rewardInfo: null
		},

		// Mock member data for admin preview (allows switching between logged_in/logged_out states)
		mockMemberData: config.mockMemberData || null,

		userData: {
			points: config.userPoints || 0,
			name: config.userName || "",
			email: config.userEmail || "",
			referralLink: config.referralLink || "",
		}, // Will be set by PHP
		welcomeBannerData: config.welcomeBannerData || {
			memberName: '',
			signupPoints: 0,
		}, // Welcome banner data for new members (initialized from PHP, updated on join)
		// Friend claim mode is separate from the rewards panel. Disable it when
		// navigating explicitly via #rewardswp-* hash (hashes don't reach the server).
		isFriendClaimMode: (config.isFriendClaimMode || false) && !window.location.hash.startsWith('#rewardswp-'),

		// Panel Service configuration - controls what sections should be displayed
		pointsEnabled: config.pointsEnabled !== undefined ? config.pointsEnabled : true,
		referralsEnabled: config.referralsEnabled !== undefined ? config.referralsEnabled : true,
		friendsReferred: config.friendsReferred || 0,
		referralTotal: config.referralTotal || 0, // Preloaded referral total (completed+pending) for header

		// Referral rewards for admin preview reactivity
		advocateReward: config.advocateReward || null,
		friendReward: config.friendReward || null,
		rewardIconMap: config.rewardIconMap || {},

		// Brand colors object for theming
		rwpColors: {
			// Core Brand Color (shared across all tiers)
			accent: '#3b82f6',

			// Lite Version Tokens (Free) - Use PHP variables as defaults
			background: '#ffffff',
			text: '#374151',
			headerBg: '#1f2937',
			headerText: '#ffffff',

			// Text Contrast System (Lite/Pro)
			textContrast: 'auto', // 'auto', 'light', 'dark'

			// Pro Version Tokens (Paid) - Use PHP variables as defaults
			success: '#10b981',
			buttonText: '#ffffff',
			icon: '#3b82f6',
			border: '#e5e7eb',
			progress: '#3b82f6',
			emailHeader: '#1f2937',
			darkVariant: '#2563eb',
		},

		// Launcher dimensions constants
		ICON_SIZE: 24,
		ICON_CONTAINER_SIZE: 48, // Icon + its padding - must match height for perfect circle
		TEXT_HORIZONTAL_PADDING: 12, // Space between icon and text

		// Core launcher properties
		measuredWidth: null, // Store the measured natural width
		launcherText: config.launcherText || "Rewards",
		originalLauncherText: config.originalLauncherText || config.launcherText || "Rewards",
		friendClaimLauncherText: config.friendClaimLauncherText || "Claim gift",
		friendClaimedLauncherText: config.friendClaimedLauncherText || "Your gift",
		launcherDisplayMode: (() => {
			// Map WordPress values to Alpine.js values
			const wpToAlpineMap = {
				'icon_with_text': 'both',
				'text_only': 'text',
				'icon_only': 'icon'
			};
			return wpToAlpineMap[config.launcherDisplayMode] || config.launcherDisplayMode || "both";
		})(), // 'icon', 'text', 'both'
		launcherStyle: "floating-circle", // 'floating-circle', 'side-tab', 'corner-pill'

		// Demo/test control properties (separate from core)
		demo: config.demo ? {
			style: config.demo.style || "floating-circle",
			horizontalOffset: config.demo.horizontalOffset || 24,
			verticalOffset: config.demo.verticalOffset || 24,
		} : {
			style: "floating-circle",
			horizontalOffset: 24,
			verticalOffset: 24,
		},

		// Computed properties for different states
		get isLoggedIn() {
			return this.memberId > 0;
		},
		get showPoints() {
			// Points visible when enabled and not in friend claim mode
			return this.pointsEnabled && !this.isFriendClaimMode;
		},
		get showReferrals() {
			// Referrals visible when enabled, points disabled, and not in friend claim mode
			if (!this.referralsEnabled || this.isFriendClaimMode) {
				return false;
			}
			// When points program is active, don't show referrals in header
			return !this.pointsEnabled;
		},
	get availableTabs() {
		const tabs = [];
		if (this.showPoints) {
			tabs.push("ways-to-earn", "ways-to-redeem");
		}
		if (this.showReferrals) {
			tabs.push("referral");
		}
		// Always allow your-rewards panel if user has rewards
		tabs.push("your-rewards");
		// Always allow activity panel for members
		tabs.push("activity");
		// Always allow birthday picker panel
		tabs.push("birthday-picker");
		return tabs;
	},

		// Helper method to safely get status count
		getStatusCount(key) {
			return this.activityStatusCounts && this.activityStatusCounts[key] !== undefined ? this.activityStatusCounts[key] : 0;
		},

		// Helper function to check if a Pro feature is accessible in current tier
		isProFeatureAccessible(requiredTier = "pro") {
			const tierLevels = { lite: 1, basic: 2, plus: 3, pro: 4 };
			const currentLevel = tierLevels[this.globalTier] || 1;
			const requiredLevel = tierLevels[requiredTier] || 1;
			return currentLevel >= requiredLevel;
		},

	navigateToPanel(panel, options = {}) {
		const resetScroll = options.resetScroll || false;

		// Special handling for redeem-success panel
		if (panel === "ways-to-redeem" && this.currentPanel === "redeem-success") {
			// Coming back from redeem success to ways to redeem
			this.redeemSuccessData = {
				couponCode: '',
				pointsSpent: 0,
				newBalance: 0,
				message: ''
			}; // Clear success data
			this.navigationDirection = "back";
			this.currentPanel = panel;
			return;
		}

		// Check if the panel is available for current test state
		if (!this.availableTabs.includes(panel) && panel !== "main" && panel !== "redeem-success" && panel !== "create-account-prompt" && !panel.startsWith("reward-detail-")) {
			return; // Don't navigate to unavailable panels
		}

		// Open panel if navigating to create-account-prompt from closed state
		if (panel === "create-account-prompt" && !this.open) {
			this.open = true;
		}

		// Set navigation direction for transitions
		// "Back" = navigating up the panel hierarchy (child → parent)
		const isBackNavigation =
			panel === "main" || // Going back to main from anywhere
			(panel === "ways-to-redeem" && (this.currentPanel === "redeem-success" || this.currentPanel === "create-account-prompt")) ||
			(panel === "your-rewards" && this.currentPanel.startsWith("reward-detail-")) ||
			(panel === "ways-to-earn" && this.currentPanel === "birthday-picker");

		this.navigationDirection = isBackNavigation ? "back" : "forward";

		// Save main panel scroll position before navigating away (must be before currentPanel changes)
		if (this.currentPanel === 'main' && panel !== 'main' && this.$refs.scrollContainer) {
			this.savedMainScrollPosition = this.$refs.scrollContainer.scrollTop;
		}

		this.currentPanel = panel;

		// Reset navigation direction after animation completes to prevent
		// re-animation when returning to widget tab
		setTimeout(() => {
			this.navigationDirection = "none";
		}, 300);
		// Manage welcome banner state
		if (panel === "ways-to-earn" && this.showWelcomeBanner) {
			// Keep welcome banner visible on first visit
		} else {
			// Hide welcome banner when navigating away or manually navigating back
			this.showWelcomeBanner = false;
		}
		// When navigating away from main, show collapsed header immediately
		if (panel !== "main") {
			this.scrollProgress = 1; // Fully collapsed
			this.headerCollapsed = true;
			this.showFixedHeader = true; // Show fixed header immediately for sub-panels
		} else {
			// When returning to main
			// Hide welcome banner when returning to main
			this.showWelcomeBanner = false;

			if (isBackNavigation && this.savedMainScrollPosition > 0 && !resetScroll) {
				// Restore scroll position and header state after content renders
				this.$nextTick(() => {
					if (this.$refs.scrollContainer) {
						this.$refs.scrollContainer.scrollTop = this.savedMainScrollPosition;
						this.scrollTop = this.savedMainScrollPosition;
						// Trigger handleScroll to update header state based on restored position
						this.handleScroll({ target: this.$refs.scrollContainer });
					}
					this.checkContentHeight();
				});
			} else {
				// Reset to expanded state
				this.scrollProgress = 0;
				this.headerCollapsed = false;
				this.showFixedHeader = false;
			// Check content height after a short delay to let content render
			this.$nextTick(() => {
				setTimeout(() => this.checkContentHeight(), 100);
			});
		}
		}
		// Reset scroll for forward navigation (not back to main)
		if (this.$refs.scrollContainer && !(isBackNavigation && panel === 'main')) {
			this.$refs.scrollContainer.scrollTop = 0;
			this.scrollTop = 0;
		}

		},

		// Helper method to scroll to an element
		scrollToElement(selector) {
			const element = this.$root.querySelector( selector );
			if (!element || !this.$refs.scrollContainer) {
				return;
			}

			// Check if element is visible (not hidden by x-show or CSS)
			const computedStyle = window.getComputedStyle(element);
			if (computedStyle.display === 'none' || computedStyle.visibility === 'hidden') {
				// Element is hidden, try again after a short delay
				setTimeout(() => {
					this.scrollToElement(selector);
				}, 100);
				return;
			}

			// Use offsetTop relative to scroll container for more reliable calculation
			let scrollTop = 0;
			let currentElement = element;

			// Calculate offset from scroll container
			while (currentElement && currentElement !== this.$refs.scrollContainer) {
				scrollTop += currentElement.offsetTop;
				currentElement = currentElement.offsetParent;
			}

			// Subtract container's scrollTop if it has one, and add offset
			scrollTop = scrollTop - (this.$refs.scrollContainer.offsetTop || 0) - 20; // 20px offset

			// Ensure scrollTop is not negative
			scrollTop = Math.max(0, scrollTop);

			// Perform scroll
			this.$refs.scrollContainer.scrollTo({
				top: scrollTop,
				behavior: 'smooth'
			});

			// Update scrollTop state
			this.scrollTop = scrollTop;
		},

		handleScroll(event) {
			const scrollTop = event.target.scrollTop;
			this.scrollTop = scrollTop; // Store for use in templates

			// Set hasScrolled based on scroll threshold
			if (scrollTop >= this.headerScrollThreshold) {
				this.hasScrolled = true;
			} else {
				this.hasScrolled = false;
			}

			// Calculate scroll progress based on dynamic background height
			const parallaxRange = this.dynamicBackgroundHeight;
			this.scrollProgress = Math.min(scrollTop / parallaxRange, 1);

			// Only apply header collapse animation if scroll animation is enabled
			if (this.hasScrollAnimation) {
				// Header collapse uses original 76px range
				const headerRange = 76;
				const headerProgress = Math.min(scrollTop / headerRange, 1);
				this.headerCollapsed = headerProgress > 0.5;
			}

		// Control fixed header visibility for main panel
		if (this.currentPanel === "main") {
			// Show fixed header when scrolling content reaches the X button area
			// X button bottom ~56px + buffer for card rounded corners
			const xButtonBottomPosition = 72;
			const fixedHeaderThreshold = this.dynamicBackgroundHeight - xButtonBottomPosition;

			if (
				scrollTop >= fixedHeaderThreshold &&
				!this.showFixedHeader
			) {
				this.showFixedHeader = true;
			} else if (
				scrollTop < fixedHeaderThreshold &&
				this.showFixedHeader
			) {
				this.showFixedHeader = false;
			}
		}

		// Track if scrolled to bottom for main panel fade
		this.isScrolledToBottom = event.target.scrollTop + event.target.clientHeight >= event.target.scrollHeight - 5;
		},

		// Check if content is long enough to warrant scroll animation
		checkContentHeight() {
			if (!this.$refs.contentArea) return;

			// Disable scroll animation for friend claim mode
			if (this.isFriendClaimMode) {
				this.hasScrollAnimation = false;
				this.scrollProgress = 0;
				this.headerCollapsed = false;
				return;
			}

			// Enable scroll animation only if both programs are enabled
			// When only one program is active, disable parallax for a cleaner experience
			this.hasScrollAnimation = this.pointsEnabled && this.referralsEnabled;

			// If scroll animation is disabled, ensure header stays expanded
			if (!this.hasScrollAnimation) {
				this.scrollProgress = 0;
				this.headerCollapsed = false;
			}
		},

		// Toggle body scroll for mobile
		toggleBodyScroll(shouldPreventScroll) {
			if (this.isMobile && shouldPreventScroll) {
				// Prevent body scroll on mobile when panel is open
				document.body.style.overflow = 'hidden';
				document.body.style.position = 'fixed';
				document.body.style.width = '100%';
			} else {
				// Restore body scroll
				document.body.style.overflow = '';
				document.body.style.position = '';
				document.body.style.width = '';
			}
		},

		detectMobile() {
			// Check multiple conditions for mobile detection
			const conditions = {
				// Touch capability (most mobile devices have touch)
				hasTouch: 'ontouchstart' in window || navigator.maxTouchPoints > 0,

				// Small viewport (either dimension)
				smallViewport: window.innerWidth < 768 || window.innerHeight < 600,

				// Mobile aspect ratios (portrait or landscape on mobile)
				mobileAspectRatio: (window.innerWidth < 1024 && window.innerHeight < 768) ||
								  (window.innerHeight < 1024 && window.innerWidth < 768),

				// User agent detection as fallback
				mobileUserAgent: /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent),

				// CSS media query check for actual mobile devices (not just touch)
				mobileMediaQuery: window.matchMedia('(hover: none) and (pointer: coarse)').matches,

				// Check if we're in Chrome DevTools responsive mode
				isDevTools: navigator.userAgent.includes('Chrome') && window.outerWidth - window.innerWidth > 100
			};

			// More strict mobile detection to avoid false positives in dev tools
			// Require mobile user agent OR (touch + mobile media query + small viewport)
			const isMobile = conditions.mobileUserAgent ||
							(conditions.hasTouch && conditions.mobileMediaQuery && conditions.smallViewport);

			// Also check orientation for tablets in landscape (but not in dev tools)
			const isTabletLandscape = !conditions.isDevTools && conditions.hasTouch &&
									 conditions.mobileMediaQuery &&
									 window.innerWidth <= 1024 && window.innerHeight <= 768;

			return isMobile || isTabletLandscape;
		},

		// Measure the actual banner content height
		measureBannerHeight() {
			if (!this.$refs.bannerContent) return;

			const bannerRect = this.$refs.bannerContent.getBoundingClientRect();
			// Get the parent panel's rect to calculate relative position
			const panelRect = this.$refs.bannerContent
				.closest(".panel-expanded-header")
				.getBoundingClientRect();

			// Calculate the height from top of panel to bottom of banner content
			// This includes all padding and dynamic content
			const measuredHeight = bannerRect.bottom - panelRect.top;

			// Add extra height to extend below the first card (same as before)
			const extraHeight = this.showLogo ? 45 : 40;


			// Update the reactive property with a safe minimum so the
			// brand background persists long enough to match the demo pacing
			const computedHeight = Math.round(measuredHeight + extraHeight);
			const MIN_HEADER_BG_HEIGHT = 145; // align with demo baseline
			this.dynamicBackgroundHeight = Math.max(MIN_HEADER_BG_HEIGHT, computedHeight);
		},

	// Calculate text opacity based on pixel distance
	getTextOpacity(scrollTop) {
		// No fade when scroll animation is disabled
		if (!this.hasScrollAnimation) return 1;
		// Before fade start delay, full opacity
		if (scrollTop < this.textFadeStartDelay) return 1;

			// During fade, calculate based on fade distance
			const fadeProgress =
				(scrollTop - this.textFadeStartDelay) / this.textFadeDistance;

			// After fade distance, fully transparent
			if (fadeProgress >= 1) return 0;

			// Return inverse of progress (1 = visible, 0 = transparent)
			return 1 - fadeProgress;
		},

	// Calculate parallax offset based on scroll position
	getParallaxOffset(scrollTop) {
		if (this.currentPanel !== "main")
			return 0;

		// When parallax or scroll animation disabled, scroll at 1x (normal)
		if (!this.parallaxEnabled || !this.hasScrollAnimation)
			return -scrollTop;

		// Apply multiplier to scroll position for parallax effect
		return -scrollTop * this.parallaxMultiplier;
	},

	// Calculate text parallax offset (faster than background for depth effect)
	getTextParallaxOffset(scrollTop) {
		// No parallax when disabled or scroll animation is disabled
		if (!this.parallaxEnabled || !this.hasScrollAnimation)
			return 0;

		// Text moves at 0.45x speed (faster than background's 0.6x)
		return -scrollTop * 0.45;
	},

		// Helper to copy text to clipboard
		handleCopyClick(inputElement, alpineData = null, showAnimation = true) {
			// Consolidated method for copying with optional animation
			if (!inputElement) return;

			// Select the input text
			inputElement.select();
			inputElement.setSelectionRange(0, 99999); // For mobile devices

			// Copy to clipboard
			const textToCopy = inputElement.value;

			// Modern clipboard API with fallback
			if (navigator.clipboard && window.isSecureContext) {
				navigator.clipboard
					.writeText(textToCopy)
									.then(() => {
					// Text copied successfully
				})
				.catch(() => {
					// Fallback if clipboard API fails
					try {
						document.execCommand("copy");
					} catch (err) {
						console.error("Failed to copy text: ", err);
					}
				});
		} else {
			// Fallback for older browsers or non-secure contexts
			try {
				document.execCommand("copy");
			} catch (err) {
				console.error("Failed to copy text: ", err);
			}
		}

			// Show checkmark animation only if requested and alpineData is available
			if (showAnimation && alpineData) {
				alpineData.isCheckmark = true;

				// Reset after 2 seconds
				setTimeout(() => {
					alpineData.isCheckmark = false;
					// Deselect the input
					window.getSelection().removeAllRanges();
				}, 2000);
			}
		},

		measureLauncherWidth() {
			// Create a temporary span to measure the text width without affecting the DOM
			const measureSpan = document.createElement('span');

			// Use the known classes from the launcher label for accurate styling
			measureSpan.className = 'whitespace-nowrap font-medium text-sm';

			// Style it to be invisible and off-screen
			measureSpan.style.position = 'absolute';
			measureSpan.style.left = '-9999px';
			measureSpan.style.top = '-9999px';
			measureSpan.style.visibility = 'hidden';

			// Set the text content
			measureSpan.textContent = this.launcherText;

			// Add to Shadow DOM (not document.body) so Tailwind classes apply
			const shadowRoot = this.$root.getRootNode();
			shadowRoot.appendChild(measureSpan);
			const labelWidth = measureSpan.offsetWidth;
			measureSpan.remove();

			// --- Calculate Total Width ---
			// All values in pixels, based on Tailwind classes.
			const PADDING_X = 48; // from `px-6` on the button when closed (1.5rem * 2)
			const ICON_WIDTH = 24; // from `w-6` on the icon span (1.5rem)
			const ICON_MARGIN = 8; // from `mr-2` on the icon span (0.5rem)

			let totalWidth = PADDING_X;

			if (this.launcherDisplayMode === 'both') {
				totalWidth += labelWidth + ICON_WIDTH + ICON_MARGIN;
			} else if (this.launcherDisplayMode === 'text') {
				totalWidth += labelWidth;
			} else { // icon_only
				// For the "icon_only" case, the expanded width isn't relevant. The button width is fixed.
				// However, we still need a sane value for measuredWidth in case the mode changes.
				// So we calculate the 'both' case as a fallback.
				totalWidth += labelWidth + ICON_WIDTH + ICON_MARGIN;
			}

			// Set the measuredWidth. Add a small buffer for safety.
			this.measuredWidth = Math.ceil(totalWidth) + 2;
		},

	// Hash routing methods
	handleHashChange() {
		// Skip hash-based control in admin preview mode
		if (this.isAdminPreview) {
			return;
		}

		const hash = window.location.hash;

		// Disable friend claim mode when navigating via any #rewardswp-* hash.
		// The friend claim panel is separate from the rewards panel.
		if (hash.startsWith('#rewardswp-') && this.isFriendClaimMode) {
			this.isFriendClaimMode = false;
		}

		// Handle #rewardswp-home to show main panel
		if (hash === "#rewardswp-home") {
			const wasOpen = this.open;
			if (!this.open) {
				this.open = true;
				// Check content height when panel opens via hash
				this.$nextTick(() => {
					this.checkContentHeight();
				});
			}
			// Always navigate to main panel when home hash is set
			// If panel was just opened, wait for next tick to ensure DOM is ready
			if (!wasOpen) {
				this.$nextTick(() => {
					this.navigateToPanel("main");
				});
			} else {
				this.navigateToPanel("main");
			}
		}
		// Handle #rewardswp-rewards to show your-rewards panel
		else if (hash === "#rewardswp-rewards") {
			const wasOpen = this.open;
			if (!this.open) {
				this.open = true;
				// Check content height when panel opens via hash
				this.$nextTick(() => {
					this.checkContentHeight();
				});
			}
			// Always navigate to the your-rewards panel when rewards hash is set
			// If panel was just opened, wait for next tick to ensure DOM is ready
			if (!wasOpen) {
				this.$nextTick(() => {
					this.navigateToPanel("your-rewards");
				});
			} else {
				this.navigateToPanel("your-rewards");
			}
		}
		// Handle #rewardswp-earn to show ways-to-earn panel
		else if (hash === "#rewardswp-earn") {
			const wasOpen = this.open;
			if (!this.open) {
				this.open = true;
				// Check content height when panel opens via hash
				this.$nextTick(() => {
					this.checkContentHeight();
				});
			}
			// Always navigate to the ways-to-earn panel when earn hash is set
			// If panel was just opened, wait for next tick to ensure DOM is ready
			if (!wasOpen) {
				this.$nextTick(() => {
					this.navigateToPanel("ways-to-earn");
				});
			} else {
				this.navigateToPanel("ways-to-earn");
			}
		}
		// Handle #rewardswp-redeem to show ways-to-redeem panel
		else if (hash === "#rewardswp-redeem") {
			const wasOpen = this.open;
			if (!this.open) {
				this.open = true;
				// Check content height when panel opens via hash
				this.$nextTick(() => {
					this.checkContentHeight();
				});
			}
			// Always navigate to the ways-to-redeem panel when redeem hash is set
			// If panel was just opened, wait for next tick to ensure DOM is ready
			if (!wasOpen) {
				this.$nextTick(() => {
					this.navigateToPanel("ways-to-redeem");
				});
			} else {
				this.navigateToPanel("ways-to-redeem");
			}
		}
		// Handle #rewardswp-join to show register form
	else if (hash === "#rewardswp-join") {
		const wasOpen = this.open;
		if (!this.open) {
			this.open = true;
			// Check content height when panel opens via hash
			this.$nextTick(() => {
				this.checkContentHeight();
			});
		}

	// Pre-fill email/name for guest members
	if (this.isGuestMember && this.userData.email) {
		window.dispatchEvent(new CustomEvent('rewardswp-join-with-email', {
			detail: {
				email: this.userData.email,
				name: this.userData.name
			}
		}));
	}

		// Always navigate to main panel and trigger register form
		// If panel was just opened, wait for next tick to ensure DOM is ready
		if (!wasOpen) {
			this.$nextTick(() => {
				this.navigateToPanel("main");
				// Trigger the form to show via event
				this.$nextTick(() => {
					window.dispatchEvent(new CustomEvent('show-register-form'));
				});
			});
		} else {
			this.navigateToPanel("main");
			this.$nextTick(() => {
				window.dispatchEvent(new CustomEvent('show-register-form'));
			});
		}
	}
		// Handle #rewardswp-login to show login form
		else if (hash === "#rewardswp-login") {
			const wasOpen = this.open;
			if (!this.open) {
				this.open = true;
				// Check content height when panel opens via hash
				this.$nextTick(() => {
					this.checkContentHeight();
				});
			}
			// Always navigate to main panel and trigger login form
			// If panel was just opened, wait for next tick to ensure DOM is ready
			if (!wasOpen) {
				this.$nextTick(() => {
					this.navigateToPanel("main");
					// Trigger the form to show via event
					this.$nextTick(() => {
						window.dispatchEvent(new CustomEvent('show-login-form'));
					});
				});
			} else {
				this.navigateToPanel("main");
				this.$nextTick(() => {
					window.dispatchEvent(new CustomEvent('show-login-form'));
				});
			}
		}
		// Handle #rewardswp-reward-{id} to show specific reward detail by ID (singular - for deep links)
		else if (hash.startsWith("#rewardswp-reward-") && !hash.startsWith("#rewardswp-rewards-")) {
			const rewardId = parseInt(hash.replace("#rewardswp-reward-", ""), 10);

			// Find reward by ID in availableRewards
			if (!isNaN(rewardId) && this.availableRewards) {
				const reward = this.availableRewards.find(r => r.id === rewardId);

				if (reward) {
					const wasOpen = this.open;
					if (!this.open) {
						this.open = true;
						// Check content height when panel opens via hash
						this.$nextTick(() => {
							this.checkContentHeight();
						});
					}
					// Navigate to the specific reward detail
					// If panel was just opened, wait for next tick to ensure DOM is ready
					if (!wasOpen) {
						this.$nextTick(() => {
							this.navigateToPanel("reward-detail-" + reward.id);
						});
					} else {
						this.navigateToPanel("reward-detail-" + reward.id);
					}
				}
			}
		}
		// Handle #rewardswp-rewards-{number} to show specific reward detail by index (plural - legacy)
		else if (hash.startsWith("#rewardswp-rewards-")) {
			const rewardIndex = parseInt(hash.replace("#rewardswp-rewards-", ""), 10);

			// Validate that we have a valid number and the reward exists
			if (!isNaN(rewardIndex) && rewardIndex > 0 && this.availableRewards && this.availableRewards.length >= rewardIndex) {
				const reward = this.availableRewards[rewardIndex - 1]; // Convert 1-based to 0-based index

				if (reward && reward.id) {
					const wasOpen = this.open;
					if (!this.open) {
						this.open = true;
						// Check content height when panel opens via hash
						this.$nextTick(() => {
							this.checkContentHeight();
						});
					}
					// Navigate to the specific reward detail
					// If panel was just opened, wait for next tick to ensure DOM is ready
					if (!wasOpen) {
						this.$nextTick(() => {
							this.navigateToPanel("reward-detail-" + reward.id);
						});
					} else {
						this.navigateToPanel("reward-detail-" + reward.id);
					}
				}
			}
		}
		// Handle #rewardswp-activity to open the activity panel
		else if (hash === '#rewardswp-activity') {
			const wasOpen = this.open;
			if (!this.open) {
				this.open = true;
				this.$nextTick(() => {
					this.checkContentHeight();
				});
			}
			if (!wasOpen) {
				this.$nextTick(() => {
					this.navigateToPanel('activity');
				});
			} else {
				this.navigateToPanel('activity');
			}
		}
		// Handle #rewardswp-activity-points / #rewardswp-activity-referrals
		else if (hash === '#rewardswp-activity-points' || hash === '#rewardswp-activity-referrals') {
			const wasOpen = this.open;
			const tab = hash === '#rewardswp-activity-points' ? 'points' : 'referrals';
			if (!this.open) {
				this.open = true;
				this.$nextTick(() => {
					this.checkContentHeight();
				});
			}
			if (!wasOpen) {
				this.activityTab = tab;
				this.$nextTick(() => {
					this.navigateToPanel('activity');
				});
			} else {
				this.navigateToActivityTab(tab);
			}
		}
		// Close panel if hash doesn't match any rewardswp hash.
		else {
			if (this.open) {
				this.open = false;
			}
		}
	},

	// Query string routing - handles ?rewardswp={panel} URLs
	handleQueryParam(param) {
		// Handle rewards-{number} pattern for specific reward detail
		if (param.startsWith('rewards-')) {
			const rewardIndex = parseInt(param.replace('rewards-', ''), 10);

			// Validate that we have a valid number and the reward exists
			if (!isNaN(rewardIndex) && rewardIndex > 0 && this.availableRewards && this.availableRewards.length >= rewardIndex) {
				const reward = this.availableRewards[rewardIndex - 1]; // Convert 1-based to 0-based index

				if (reward && reward.id) {
					this.navigateToPanel('reward-detail-' + reward.id);
				}
			}
			return;
		}

		switch (param) {
			case 'home':
				this.navigateToPanel('main');
				break;
			case 'rewards':
				this.navigateToPanel('your-rewards');
				break;
			case 'earn':
				this.navigateToPanel('ways-to-earn');
				break;
			case 'redeem':
				this.navigateToPanel('ways-to-redeem');
				break;
			case 'join':
				this.navigateToPanel('main');
				this.$nextTick(() => {
					window.dispatchEvent(new CustomEvent('show-register-form'));
				});
				break;
			case 'login':
				this.navigateToPanel('main');
				this.$nextTick(() => {
					window.dispatchEvent(new CustomEvent('show-login-form'));
				});
				break;
			case 'activity':
				this.navigateToPanel('activity');
				break;
			case 'activity-points':
				this.navigateToActivityTab('points');
				break;
			case 'activity-referrals':
				this.navigateToActivityTab('referrals');
				break;
		}
	},

		updateHash(isOpen) {
			// Skip hash updates in admin preview mode
			if (this.isAdminPreview) {
				return;
			}

			if (isOpen) {
				window.location.hash = "rewardswp-home";
			} else {
				// Remove hash without triggering hashchange event
				history.replaceState(
					null,
					null,
					window.location.pathname + window.location.search
				);
			}
		},

		handleSettingUpdate(detail) {
			if (!this.isAdminPreview) return;

			const { key, value } = detail;

			// Map WordPress values to Alpine.js values
			const wpToAlpineMap = {
				'icon_with_text': 'both',
				'text_only': 'text',
				'icon_only': 'icon'
			};

			// For launcher settings, close the panel so users can see the launcher
			const launcherSettings = ['launcher_display', 'launcher_text', 'launcher_position'];
			if (launcherSettings.includes(key)) {
				this.open = false;
			} else {
				this.open = true; // Keep panel open for other settings
			}

			switch (key) {
				case 'brand_color':
					this.rwpColors.headerBg = value;
					this.rwpColors.accent = value;
					// this.dynamicHeaderTextColor = this.getContrastingTextColor(value);
					break;
				case 'launcher_display':
					this.launcherDisplayMode = wpToAlpineMap[value] || value;
					break;
				case 'launcher_text':
					this.launcherText = value;
					this.$nextTick(() => this.measureLauncherWidth());
					break;
				case 'sharing_x':
					this.sharing.x = value;
					break;
				case 'sharing_facebook':
					this.sharing.facebook = value;
					break;
				case 'sharing_email':
					this.sharing.email = value;
					break;
				case 'sharing_x_message':
				case 'sharing_email_subject':
				case 'sharing_email_message':
					// These are text-based and don't have a direct visual representation
					// in the preview at the moment.
					break;
			}
		},

		// Panel control methods
		toggle() {
			this.open = !this.open;
		},

		openPanel() {
			this.open = true;
		},

		closePanel() {
			this.open = false;
		},

		/**
		 * Switch preview member state (admin preview only).
		 *
		 * Allows toggling between "logged_out" and "logged_in" states without
		 * requiring an AJAX request. Uses mock member data passed from PHP.
		 *
		 * @param {string} newState - Either 'logged_in', 'logged_out', or 'friend'.
		 */
		switchPreviewMemberState(newState) {
			if (!this.isAdminPreview) return;

			if (newState === 'logged_in' && this.mockMemberData) {
				// Switch to "logged in" state with mock member data
				this.memberId = this.mockMemberData.member_id || 1;
				this.userData.points = this.mockMemberData.points_balance || 100;
				this.userData.name = this.mockMemberData.user_name || '';
				this.userData.email = this.mockMemberData.user_email || '';
				this.userData.referralLink = this.mockMemberData.referral_link || '';
				this.availableRewards = this.mockMemberData.available_rewards || [];
				this.availableRewardsCount = this.mockMemberData.available_rewards_count || 0;
				this.isGuestMember = false;
				this.isFriendClaimMode = false;
				// Sync points to redeemManager's separate userData
				window.dispatchEvent(new CustomEvent('member-joined', {
					detail: { pointsBalance: this.userData.points }
				}));
			} else if (newState === 'friend') {
				// Switch to "friend claim" state using actual config friendClaimData
				this.memberId = 0;
				this.isFriendClaimMode = true;
				// Use actual claim data from PHP config (has real friend reward info)
				const configFriendClaim = config.friendClaimData || {};
				this.friendClaimData = {
					referralCode: configFriendClaim.referralCode || 'preview123',
					advocateName: configFriendClaim.advocateName || 'Jane Doe',
					rewardInfo: configFriendClaim.rewardInfo || null
				};
				this.isGuestMember = false;
				// Tell friendClaimForm to show the claim form (not success)
				window.dispatchEvent(new CustomEvent('preview-friend-claim-state', { detail: { showSuccess: false } }));
			} else if (newState === 'friend-claimed') {
				// Switch to "friend claimed" state showing success/coupon
				this.memberId = 0;
				this.isFriendClaimMode = true;
				// Use actual claim data from PHP config (has real friend reward info)
				const configFriendClaimClaimed = config.friendClaimData || {};
				this.friendClaimData = {
					referralCode: configFriendClaimClaimed.referralCode || 'preview123',
					advocateName: configFriendClaimClaimed.advocateName || 'Jane Doe',
					rewardInfo: configFriendClaimClaimed.rewardInfo || null
				};
				this.isGuestMember = false;
				// Tell friendClaimForm to show the success state with mock coupon
				window.dispatchEvent(new CustomEvent('preview-friend-claim-state', {
					detail: {
						showSuccess: true,
						couponCode: 'friend-abc123',
						successMessage: 'You have received your gift!'
					}
				}));
			} else {
				// Switch to "logged out" state
				this.memberId = 0;
				this.userData.points = 0;
				this.userData.name = '';
				this.userData.email = '';
				this.userData.referralLink = '';
				this.availableRewards = [];
				this.availableRewardsCount = 0;
				this.isGuestMember = false;
				this.isFriendClaimMode = false;
				// Sync points to redeemManager's separate userData
				window.dispatchEvent(new CustomEvent('member-joined', {
					detail: { pointsBalance: 0 }
				}));
			}

			// Navigate back to main panel when switching states
			this.navigateToPanel('main');

			// Re-measure banner height after state change
			this.$nextTick(() => {
				this.measureBannerHeight();
				this.checkContentHeight();
			});
		},

		/**
		 * Get formatted reward description for display.
		 * Used by admin preview to show reactive reward info.
		 */
		getRewardDescription(reward) {
			if (!reward || !reward.type) return '';

			// Use pre-computed formatted_amount from PHP if available (matches frontend formatting exactly).
			if (reward.formatted_amount) {
				return reward.formatted_amount;
			}

			// Fallback formatting for legacy data or edge cases.
			const amount = parseFloat(reward.reward_amount || 0);
			const type = reward.type;

			switch (type) {
				case 'points':
					return `${amount.toLocaleString()} ${amount === 1 ? this.currencySingular : this.currencyPlural}`;
				case 'percentage_discount':
					return `${amount}% off coupon`;
				case 'fixed_discount': {
					// Mirror the server-side symbol placement so a euro store shows "5 € off coupon", not "€5".
					const sep = this.currencySpace ? ' ' : '';
					const money = this.currencyPosition === 'after'
						? `${amount}${sep}${this.currencySymbol}`
						: `${this.currencySymbol}${sep}${amount}`;
					return `${money} off coupon`;
				}
				case 'free_shipping':
					return 'Free shipping';
				case 'free_product':
					return 'Free product';
				default:
					return reward.name || 'Reward';
			}
		},

		/**
		 * Get icon HTML for a reward type.
		 * Falls back to gift icon if not found in map.
		 */
		getRewardIconHtml(reward) {
			if (!reward || !reward.type) return '';
			return this.rewardIconMap[reward.type] || this.rewardIconMap['gift'] || '';
		},

		// Getter for external access to panel state
		get isOpen() {
			return this.open;
		},

		// Initialization
	init() {

		// Save the initial flag state before we clear it
		const shouldOpenFromFlag = window.rewardswpOpenPanelOnPageLoad === true && !this.isAdminPreview;

		// Check for query string parameter ?rewardswp={panel}
		const params = new URLSearchParams(window.location.search);
		const rewardswpQueryParam = params.get('rewardswp');
		const validQueryPanels = ['home', 'rewards', 'earn', 'redeem', 'join', 'login', 'activity', 'activity-points', 'activity-referrals'];
		const isValidQueryParam = validQueryPanels.includes(rewardswpQueryParam) || (rewardswpQueryParam && rewardswpQueryParam.startsWith('rewards-'));
		const shouldOpenFromQuery = isValidQueryParam && !this.isAdminPreview;

		// Check for #rewardswp-* hash
		const hash = window.location.hash;
		const shouldOpenFromHash = hash.startsWith('#rewardswp-') && !this.isAdminPreview;

		// Check if panel was opened by the welcome flow
		const shouldOpenFromWelcomeFlow = config.showWelcomeFlow && this.memberId > 0;

		// Check for window.rewardswpOpenPanelOnPageLoad compatibility
		if (shouldOpenFromFlag) {
			this.open = true;
			// Clear the flag to prevent it from opening again on subsequent page loads
			window.rewardswpOpenPanelOnPageLoad = false;
		}

		// Open panel if query string parameter or hash is present
		if ((shouldOpenFromQuery || shouldOpenFromHash) && !shouldOpenFromFlag) {
			this.open = true;
		}

		this.hasNativeShare = navigator.share !== undefined;

		// Reset navigationDirection to 'initial' when panel opens to trigger stagger animations
		this.$watch('open', (isOpen) => {
			if (isOpen) {
				this.navigationDirection = 'initial';
				document.body.classList.add('rwp-panel-open');
			} else {
				document.body.classList.remove('rwp-panel-open');
			}
		});

		// Apply initial state (for when panel opens via URL param before watcher is set up)
		if (this.open) {
			document.body.classList.add('rwp-panel-open');
		}

		// Reset birthday day if it exceeds days in newly selected month
		this.$watch('birthdayMonth', () => {
			const maxDay = this.birthdayDaysInMonth.length;
			if (this.birthdayDay && parseInt(this.birthdayDay, 10) > maxDay) {
				this.birthdayDay = '';
			}
		});

		// Admin preview: Listen for state toggle event (logged_in/logged_out)
		if (this.isAdminPreview) {
			// Open panel after browser paint to trigger the CSS animation
			requestAnimationFrame(() => {
				this.open = true;
			});

			// Reset navigation direction after initial animations complete
			// This prevents re-animation when returning to widget tab
			setTimeout(() => {
				this.navigationDirection = "none";
			}, 500);

			window.addEventListener('switch-preview-member-state', (e) => {
				this.switchPreviewMemberState(e.detail);
			});

			// Listen for program toggle changes to update widget preview reactively
			window.addEventListener('points-program-toggled', (e) => {
				this.pointsEnabled = e.detail.enabled;
				this.checkContentHeight(); // Re-evaluate parallax
			});

			window.addEventListener('referrals-program-toggled', (e) => {
				this.referralsEnabled = e.detail.enabled;
				this.checkContentHeight(); // Re-evaluate parallax
			});

			// Listen for referral reward updates to update widget preview reactively
			window.addEventListener('referral-reward-updated', (e) => {
				const { context, reward } = e.detail;
				if (context === 'advocate') {
					this.advocateReward = reward;
				} else if (context === 'friend') {
					this.friendReward = reward;
				}
			});
		}

		// Listen for friend claim success to update launcher text (works on frontend and admin)
		window.addEventListener('friend-claim-success', () => {
			if (this.isFriendClaimMode) {
				this.launcherText = this.friendClaimedLauncherText;
			}
		});

		// Set initial launcher text if starting in friend-claim mode
		if (this.isFriendClaimMode) {
			this.launcherText = this.friendClaimLauncherText;
		}

		// DON'T clear the meta immediately - wait until panel is closed
		// Store flag to clear it later
		this.shouldClearWelcomeFlowOnClose = config.showWelcomeFlow && this.memberId > 0;

		// Initialize mobile detection
		this.isMobile = this.detectMobile();

			// Ensure viewport meta tag includes safe area support
			let viewport = document.querySelector('meta[name="viewport"]');
			if (!viewport) {
				viewport = document.createElement('meta');
				viewport.name = 'viewport';
				document.head.appendChild(viewport);
			}
			// Add viewport-fit=cover if not already present
			if (!viewport.content.includes('viewport-fit=cover')) {
				viewport.content = viewport.content + ', viewport-fit=cover';
			}

			// Initialize demo/test controls from defaults (only if not set by config)
			if (!config.demo) {
				this.demo.style = "floating-circle";
				this.demo.horizontalOffset = 24;
				this.demo.verticalOffset = 24;
			}

			// Initialize core launcher content settings (only if not already set by config)
			if (!config.launcherText) {
				this.launcherText = "Rewards";
			}
			this.launcherStyle = "floating-circle";

			// Add resize and orientation listeners for mobile detection
			const handleViewportChange = () => {
				this.isMobile = this.detectMobile();

				// Re-apply body scroll lock if panel is open on mobile
				if (this.open) {
					this.toggleBodyScroll(this.isMobile);
				}
			};

			window.addEventListener("resize", handleViewportChange);
			window.addEventListener("orientationchange", handleViewportChange);

			// Core initialization: measure button width after DOM is ready
			this.$nextTick(() => {
				this.measureLauncherWidth();
				this.measureBannerHeight();

				// Set up ResizeObserver to track banner content changes
				if (window.ResizeObserver && this.$refs.bannerContent) {
					this._bannerObserver = new ResizeObserver(() => {
						this.measureBannerHeight();
					});
					this._bannerObserver.observe(this.$refs.bannerContent);
				}

				// Set up ResizeObserver on contentArea to recompute isScrolledToBottom
				// when content changes (panel opens, AJAX loads, dynamic content)
				if (window.ResizeObserver && this.$refs.contentArea) {
					this._contentObserver = new ResizeObserver(() => {
						if (this.currentPanel === 'main') {
							const scrollEl = this.$refs.scrollContainer;
							if (scrollEl && scrollEl.scrollHeight > 0) {
								this.isScrolledToBottom = scrollEl.scrollTop + scrollEl.clientHeight >= scrollEl.scrollHeight - 5;
							}
						}
					});
					this._contentObserver.observe(this.$refs.contentArea);
				}

				// Check content height on initial load
				setTimeout(() => this.checkContentHeight(), 200);
			});

		// Set up hash routing
		window.addEventListener("hashchange", () =>
			this.handleHashChange()
		);

	// Handle clicks on hash links when hash is already set
	// This allows clicking the same hash link multiple times (e.g., clicking "Sign in" when already on #rewardswp-login)
	document.addEventListener("click", (e) => {
		const link = e.target.closest('a[href^="#rewardswp-"]');
		if (link) {
			const targetHash = link.getAttribute("href");
			// If clicking a hash link that matches current hash, manually trigger handler
			if (targetHash === window.location.hash) {
				e.preventDefault();
				this.handleHashChange();
			}
		}
	}, true); // Add capture phase here too

	// Handle initial hash on load (matching the query param pattern: open sync, navigate in $nextTick)
	if (shouldOpenFromHash && !shouldOpenFromFlag && !shouldOpenFromQuery && !shouldOpenFromWelcomeFlow) {
		this.$nextTick(() => {
			this.checkContentHeight();
			this.$nextTick(() => {
				this.handleHashChange();
			});
		});
	} else if (!shouldOpenFromFlag && !shouldOpenFromQuery && !shouldOpenFromWelcomeFlow) {
		// No hash to handle — check for close/other hashes
		this.handleHashChange();
	}

	// Handle query string panel navigation (after all init is complete)
	if (shouldOpenFromQuery && rewardswpQueryParam !== 'home') {
		this.$nextTick(() => {
			this.checkContentHeight();
			this.$nextTick(() => {
				this.handleQueryParam(rewardswpQueryParam);
			});
		});
	}

	// Watch for panel close to clear welcome flow meta
	this.$watch('open', (isOpen) => {
		if (!isOpen && this.shouldClearWelcomeFlowOnClose) {
			this.clearWelcomeFlowMeta();
			// Also clear localStorage flag so panel doesn't auto-open when navigating away
			if (this.memberId > 0) {
				const justJoinedKey = 'rewardswp_just_joined_' + this.memberId;
				localStorage.removeItem(justJoinedKey);
			}
			this.shouldClearWelcomeFlowOnClose = false; // Only clear once
		}
	});

	// Note: localStorage check for just-joined flag is now handled during initialization
	// to prevent flicker (see initialPanel and initialShowWelcomeBanner above)

// Listen for member-joined event to update data
	window.addEventListener('member-joined', (event) => {
	if (event.detail) {
		// Update memberId if it was 0 (new member just joined)
		if (this.memberId === 0 && event.detail.memberId > 0) {
		this.memberId = event.detail.memberId;
		}

		// Update isGuestMember to false when a full member account is created
		// This runs for ALL member-joined events, whether memberId was 0 or already assigned
		if (this.isGuestMember && event.detail.memberId > 0) {
			this.isGuestMember = false;
			// Invalidate activity cache and flag for refresh (fresh nonce is already set above)
			this.activityCache = {};
			this.needsActivityRefresh = true;
		}

		// Update welcome banner data
		if (event.detail.memberName) {
			this.welcomeBannerData.memberName = event.detail.memberName;
		}
		if (event.detail.signupPoints !== undefined) {
			this.welcomeBannerData.signupPoints = event.detail.signupPoints || 0;
		}
	// Update points balance - direct mutation for proper reactivity
	if (event.detail.pointsBalance !== undefined) {
		this.userData.points = event.detail.pointsBalance || 0;

		// Also update the web component's data-user-points attribute
		// This ensures redeemManager's getter picks up the new value
		const webComponent = document.querySelector('rewards-panel');
		if (webComponent) {
			webComponent.setAttribute('data-user-points', event.detail.pointsBalance || 0);
		}

	}
	// Update referral link
	if (event.detail.referralLink) {
		this.userData.referralLink = event.detail.referralLink;
	}

	// Update fresh nonces after member creation (nonces are user-specific)
	if (event.detail.freshNonce) {
		// Update Alpine component's advocate nonce
		this.advocateNonce = event.detail.freshNonce;
		// Update web component attribute for redeemManager
		const webComponent = document.querySelector('rewards-panel');
		if (webComponent) {
			webComponent.setAttribute('data-share-points-nonce', event.detail.freshNonce);
		}
	}

	if (event.detail.freshClaimNonce) {
		// Update Alpine component's claim nonce
		this.claimNonce = event.detail.freshClaimNonce;
		// Update web component attribute for couponUtils
		const webComponent = document.querySelector('rewards-panel');
		if (webComponent) {
			webComponent.setAttribute('data-claim-nonce', event.detail.freshClaimNonce);
		}
	}
	// No need to refresh - we already have all the data from the join response
}
});

	// Listen for upgrade-guest-to-member event
	window.addEventListener('upgrade-guest-to-member', async (event) => {

		// Show loading state
		this.loading = true;

		try {
			const formData = new FormData();
			formData.append('action', 'rewardswp_create_member');
			formData.append('_ajax_nonce', this.advocateNonce);
			formData.append('is_guest_upgrade', '1');

			const response = await fetch(rewardswpMainVars.ajaxUrl, {
				method: 'POST',
				body: formData
			});

			const result = await response.json();

			if (result.success) {

				// Update member data
				this.memberId = result.data.member_id || this.memberId;
				this.userData.points = result.data.points_balance || 0;
				this.userData.referralLink = result.data.referral_link || '';
				this.userPoints = result.data.points_balance || 0;

			// Update welcome banner data
			this.welcomeBannerData.memberName = result.data.member_name || '';
			this.welcomeBannerData.signupPoints = result.data.signup_points || 0;

			// Show welcome banner with points
			this.showWelcomeBanner = true;

			// Force Alpine to update watchers
			await this.$nextTick();

			// Dispatch member-joined event so signup action marks as completed
			window.dispatchEvent(new CustomEvent('member-joined', {
				detail: {
					memberId: result.data.member_id,
					memberName: result.data.member_name,
					pointsBalance: result.data.points_balance,
					signupPoints: result.data.signup_points,
					referralLink: result.data.referral_link,
					freshNonce: result.data.fresh_nonce,
					freshClaimNonce: result.data.fresh_claim_nonce
				}
			}));

			// Update nonces
				if (result.data.fresh_nonce) {
					this.advocateNonce = result.data.fresh_nonce;
					const webComponent = document.querySelector('rewards-panel');
					if (webComponent) {
						webComponent.setAttribute('data-share-points-nonce', result.data.fresh_nonce);
						webComponent.setAttribute('data-user-points', result.data.points_balance || 0);
					}
				}
				if (result.data.fresh_claim_nonce) {
					this.claimNonce = result.data.fresh_claim_nonce;
					const webComponent = document.querySelector('rewards-panel');
					if (webComponent) {
						webComponent.setAttribute('data-claim-nonce', result.data.fresh_claim_nonce);
					}
				}

				// Update panel state
			} else {
				this.errorMessage = result.data?.message || 'Failed to create account. Please try again.';
			}
		} catch (error) {
			this.errorMessage = 'An error occurred. Please try again.';
		} finally {
			this.loading = false;
		}
	});

	// Listen for navigate-to-create-account-prompt event (for guest members clicking redeem)
	window.addEventListener('navigate-to-create-account-prompt', (event) => {
		// Store the reward they were trying to redeem
		if (event.detail && event.detail.reward) {
			this.lockedReward = event.detail.reward;
		}
		if (typeof this.navigateToPanel === 'function') {
			this.navigateToPanel('create-account-prompt');
		}
	});

	// Listen for navigate-to-earn event
	window.addEventListener('navigate-to-earn', (event) => {

		if (event.detail && event.detail.memberId) {
			// Update memberId if it was 0 (new member just joined)
			if (this.memberId === 0 && event.detail.memberId > 0) {
				this.memberId = event.detail.memberId;
			}
			}
		// Update welcome banner data from event
		if (event.detail.memberName) {
			this.welcomeBannerData.memberName = event.detail.memberName;
		}
		if (event.detail.signupPoints !== undefined) {
			this.welcomeBannerData.signupPoints = event.detail.signupPoints || 0;
		}
	// Update points balance - direct mutation for proper reactivity
	if (event.detail.pointsBalance !== undefined) {
		this.userData.points = event.detail.pointsBalance || 0;

		// Also update the web component's data-user-points attribute
		// This ensures redeemManager's getter picks up the new value
		const webComponent = document.querySelector('rewards-panel');
		if (webComponent) {
			webComponent.setAttribute('data-user-points', event.detail.pointsBalance || 0);
		}

	}
	// Update referral link
	if (event.detail.referralLink) {
		this.userData.referralLink = event.detail.referralLink;
	}
	// Navigate if member ID matches or if current memberId is 0 (new member)
		if (event.detail.memberId === this.memberId || this.memberId === 0) {
			this.showWelcomeBanner = true;
			// Ensure panel is open
			if (!this.open) {
				this.open = true;
			}
			// Navigate immediately
			this.$nextTick(() => {
				this.navigateToPanel('ways-to-earn');
			});
			}
		});

	// Listen for open-panel-to-join event (from confirmation card "Create account" button)
	window.addEventListener('rewardswp-open-panel-to-join', (event) => {
		// Open the panel if closed
		if (!this.open) {
			this.open = true;
		}
		// Use navigateToPanel for smooth transition
		this.$nextTick(() => {
			this.navigateToPanel('main');
		});
	});

			// Handle escape key (skip in admin preview)
			if (!this.isAdminPreview) {
				document.addEventListener("keydown", (e) => {
					if (e.key === "Escape" && this.open) {
						this.open = false;
					}
				});
			}

		// Update launcher and reset header state when opening/closing
		this.$watch("open", (value) => {
			// Toggle body scroll for mobile (skip in admin preview)
			if (!this.isAdminPreview) {
				this.toggleBodyScroll(value);
			}

			if (value) {
				// Set opening flag to prevent transitions
				this.isOpening = true;
				// Reset all state immediately and synchronously before widget shows
				this.scrollProgress = 0;
				this.headerCollapsed = false;
				// Only reset to main if not showing welcome banner and no explicit initial panel was set
				// (preserve ways-to-earn for welcome flow, and preserve any explicitly configured panel)
				if (!this.showWelcomeBanner && !this.hasExplicitInitialPanel) {
					this.currentPanel = "main";
				}
				this.showFixedHeader = false;
					// Reset scroll immediately and synchronously
					this.$nextTick(() => {
						if (this.$refs.scrollContainer) {
							this.$refs.scrollContainer.scrollTop = 0;
							// Force scrollProgress to 0 after scroll reset to prevent any race conditions
							this.scrollProgress = 0;
						}
						// Clear opening flag after DOM updates to allow normal transitions again
						this.$nextTick(() => {
							this.isOpening = false;
							// Check content height and measure banner height after panel opens and content renders
							// Use requestAnimationFrame to wait for next paint cycle, then $nextTick for Alpine reactivity
							requestAnimationFrame(() => {
								this.$nextTick(() => {
									this.checkContentHeight();
									this.measureBannerHeight(); // Re-measure background height after resize
								});
							});
						});
					});
				} else {
					// When closing, re-measure the launcher width to ensure it's correct
					this.$nextTick(() => {
						this.measureLauncherWidth();
					});
				}
			});

			// Reset to main panel when friend claim mode changes (unless explicit initial panel was set)
			this.$watch("isFriendClaimMode", (isClaim, wasClaim) => {
				if (!this.hasExplicitInitialPanel) {
					this.currentPanel = "main";
				}
				this.scrollProgress = 0;
				this.headerCollapsed = false;
				if (this.$refs.scrollContainer) {
					this.$refs.scrollContainer.scrollTop = 0;
				}
				// Check content height after state change and content re-renders
				this.$nextTick(() => {
					setTimeout(() => this.checkContentHeight(), 100);
				});

				// Update launcher text for friend-claim mode
				if (isClaim) {
					// Set to "Claim gift" initially (will be updated if already claimed)
					this.launcherText = this.friendClaimLauncherText;
				} else if (wasClaim) {
					// Restore original text when leaving friend-claim mode
					this.launcherText = this.originalLauncherText;
				}
			});

			// Core watchers (affect functionality)
			this.$watch("launcherText", () => {
				this.$nextTick(() => this.measureLauncherWidth());
			});

		this.$watch("launcherDisplayMode", () => {
			this.$nextTick(() => this.measureLauncherWidth());
		});
	},

	destroy() {
		this._bannerObserver?.disconnect();
		this._contentObserver?.disconnect();
	},

		// Helper function for currency pluralization
		getCurrencyName(amount) {
			const num = parseInt(amount) || 0;
			return num === 1 ? this.currencySingular : this.currencyPlural;
		},

		// Handle event tracking
	async handleShareClick(network, event) {
		// Get original href (contains admin settings text)
		const originalHref = event.currentTarget.href;
		const isEmailLink = network === 'email';

		// Prevent default to replace URL dynamically
		event.preventDefault();

		// Get current dynamic referral link
		const newReferralLink = this.userData.referralLink || 'https://yoursite.com/?share=df04c90';

		// Map sharing option IDs to event network names
		const eventsNetworkMap = {
			'twitter': 'x',
			'facebook': 'facebook',
			'email': 'email'
		};

		const eventsNetwork = eventsNetworkMap[network];

		if (!eventsNetwork) {
			// Unknown network, use shareChannel as fallback
			this.shareChannel(network);
			return;
		}

		// Replace the URL parameter in the original href with the new referralLink
		let updatedHref = originalHref;

	if (network === 'email') {
		// For mailto links, replace the referral URL in the body
		// In mailto body, URLs are URL-encoded: https://site.com/path becomes https%3A%2F%2Fsite.com%2Fpath
		// Note: dots (.) and alphanumeric chars in domain are NOT encoded
		const encodedNewLink = encodeURIComponent(newReferralLink);

		// Simple pattern: match https%3A%2F%2F followed by any characters until whitespace or &
		// This captures the full encoded URL including path and query string
		const encodedPattern = /https?%3A%2F%2F[^\s&]+/gi;
		updatedHref = originalHref.replace(encodedPattern, encodedNewLink);

		// Track points and events in background (non-blocking)
		this.trackShareInBackground(network);
		// Navigate to updated mailto link
		window.location.href = updatedHref;
		return;
	}

		if (network === 'twitter') {
			// For Twitter, replace the url parameter
			updatedHref = originalHref.replace(/([&?])url=([^&]*)/gi, '$1url=' + encodeURIComponent(newReferralLink));
		} else if (network === 'facebook') {
			// For Facebook, replace the u parameter
			updatedHref = originalHref.replace(/([&?])u=([^&]*)/gi, '$1u=' + encodeURIComponent(newReferralLink));
		}

		// Track sharing event first (using clean nomenclature)
		await this.trackEvent('share', eventsNetwork, window.location.href);

		// Open the updated URL with original text but new referral link
		window.open(updatedHref, '_blank');
	},

		// Track sharing in background for email (non-blocking, events only)
		async trackShareInBackground(network) {
			const eventsNetworkMap = {
				'email': 'email'
			};
			const eventsNetwork = eventsNetworkMap[network];

			// Track event
			try {
				await this.trackEvent('share', eventsNetwork, window.location.href);
			} catch (error) {
				console.warn('Background event tracking failed:', error);
			}


		},

		// Generic event tracking function
		async trackEvent(eventType, eventContext = null, referrerUrl = null) {
			try {
				const body = {
					action: 'rewardswp_track_event',
					type: eventType,
					nonce: this.sharePointsNonce
				};

				// Add context if provided
				if (eventContext) {
					body.context = eventContext;
				}

				// Add referrer URL if provided
				if (referrerUrl) {
					body.referrer_url = referrerUrl;
				}

				const response = await fetch(rewardswpMainVars.ajaxUrl, {
					method: 'POST',
					headers: {
						'Content-Type': 'application/x-www-form-urlencoded',
					},
					body: new URLSearchParams(body)
				});

				const result = await response.json();

				if (!result.success) {
					// Silently log error - don't interfere with user experience
					console.warn('Event tracking failed:', result.data?.message || 'Unknown error');
				}

				return result;
			} catch (error) {
				// Silently log error - don't interfere with user experience
				console.warn('Event tracking error:', error);
				return null;
			}
		},



		// Get text overlay styles for different overlay patterns
		getTextOverlayStyles() {
			const overlay = this.optimalTextOverlay;
			const rgb = this.hexToRgb(overlay.color);

			if (!rgb) return {};

			const baseStyles = {
				translucent: {
					background: `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${overlay.opacity})`,
					mixBlendMode: overlay.blendMode
				},
				'gradient-scrim': {
					background: `radial-gradient(ellipse at center, rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${overlay.opacity}) 0%, transparent 70%)`,
					mixBlendMode: overlay.blendMode
				},
				'floor-fade': {
					background: `linear-gradient(to top, rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${overlay.opacity}) 0%, transparent 60%)`,
					mixBlendMode: overlay.blendMode
				}
			};

			return baseStyles[this.textOverlay.style] || baseStyles.translucent;
		},

		// Helper method to convert overlay styles to CSS string
		getOverlayStylesAsString() {
			const styles = this.getTextOverlayStyles();
			const cssString = Object.entries(styles).map(([key, value]) => {
				const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase();
				return `${cssKey}: ${value}`;
			}).join('; ');

			return cssString;
		},

		// Get optimal text overlay based on current mode and image
		get optimalTextOverlay() {
			if (
				!this.backgroundImage.enabled ||
				!this.backgroundImage.url ||
				this.textOverlay.mode === "disabled"
			) {
				return {
					opacity: 0,
					color: "transparent",
					blendMode: "normal",
				};
			}

			if (this.textOverlay.mode === "manual") {
				return {
					opacity: this.textOverlay.opacity,
					color: this.textOverlay.color,
					blendMode: this.textOverlay.blendMode,
				};
			}

			// Auto mode - research-backed optimal settings
			return this.getAutoOptimalTextOverlay();
		},

		// Auto-detection of optimal text overlay based on research
		getAutoOptimalTextOverlay() {
			// Default to research-backed 40% black translucent overlay (increased to 60% for testing visibility)
			const optimalOpacity = 0.6;
			const optimalColor = "#000000";
			const optimalBlendMode = "normal";

			return {
				opacity: optimalOpacity,
				color: optimalColor,
				blendMode: optimalBlendMode,
			};
		},

		// Utility function to convert hex to RGB
		hexToRgb(hex) {
			const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(
				hex
			);
			return result
				? {
						r: parseInt(result[1], 16),
						g: parseInt(result[2], 16),
						b: parseInt(result[3], 16),
				  }
				: null;
		},

	// Refresh rewards data via AJAX for reactivity
	async refreshRewardsData() {
		// Skip refresh in demo or admin preview mode — no real member data to fetch.
		if (this.isDemo || this.isAdminPreview) {
			return;
		}

		// Only refresh if user is a member
		if (!this.memberId || this.memberId === 0) {
			return;
		}

		// Check if required globals are available
		if (typeof rewardswpMainVars === 'undefined' || !rewardswpMainVars.ajaxUrl) {
			console.warn('RewardsWP: Ajax URL not available, skipping rewards refresh');
			return;
		}

		try {
			const response = await fetch(rewardswpMainVars.ajaxUrl, {
				method: 'POST',
				headers: {
					'Content-Type': 'application/x-www-form-urlencoded',
				},
				body: new URLSearchParams({
					action: 'rewardswp_refresh_rewards',
					nonce: this.advocateNonce
				})
			});

			const result = await response.json();

			if (result.success) {
				this.availableRewards = result.data.rewards || [];
				this.availableRewardsCount = result.data.rewards_count || 0;
				this.userData.points = result.data.points_balance || this.userData.points;
			} else {
				console.warn('RewardsWP: AJAX request failed:', result);
			}
		} catch (error) {
			console.error('RewardsWP: Failed to refresh rewards data:', error);
		}
	},

	/**
	 * Clear the show_welcome_flow meta flag after it's been triggered.
	 * This ensures the welcome flow only shows once for guest members.
	 */
	async clearWelcomeFlowMeta() {
		if (!this.memberId || this.memberId === 0) {
			return;
		}

		if (typeof rewardswpMainVars === 'undefined' || !rewardswpMainVars.ajaxUrl) {
			console.warn('RewardsWP: Ajax URL not available, cannot clear welcome flow meta');
			return;
		}

		try {
			const response = await fetch(rewardswpMainVars.ajaxUrl, {
				method: 'POST',
				headers: {
					'Content-Type': 'application/x-www-form-urlencoded',
				},
				body: new URLSearchParams({
					action: 'rewardswp_clear_welcome_flow',
					member_id: this.memberId,
					nonce: this.advocateNonce
				})
			});

			const result = await response.json();

		} catch (error) {
			console.error('RewardsWP: Error clearing welcome flow meta:', error);
		}
	},

	/**
	 * Navigate to the activity panel on a specific tab.
	 *
	 * Resets activity state and loads fresh data. Used by hash routing
	 * and postMessage handling where we need to jump directly to a tab
	 * without the cache-swap behavior of switchActivityTab().
	 *
	 * @param {string} tab - Tab to navigate to: 'points' or 'referrals'
	 */
	navigateToActivityTab(tab) {
		this.activityTab = tab;
		this.activityItems = [];
		this.activityPage = 1;
		this.activityHasMore = false;
		this.activityStatusCounts = null;
		this.activityCache = {};

		if (this.currentPanel === 'activity') {
			this.loadActivity();
		} else {
			this.navigateToPanel('activity');
		}
	},

	/**
	 * Switch activity tab and reload data.
	 *
	 * @param {string} tab - Tab to switch to: 'points' or 'referrals'
	 */
	switchActivityTab(tab) {
		if (this.activityTab === tab) {
			return;
		}

		// Cache current tab data before switching
		this.activityCache[this.activityTab] = {
			items: this.activityItems,
			total: this.activityTotal,
			hasMore: this.activityHasMore,
			page: this.activityPage,
			statusCounts: this.activityStatusCounts,
		};

		this.activityTab = tab;

		// Restore from cache if available
		if (this.activityCache[tab]) {
			const cached = this.activityCache[tab];
			this.activityItems = cached.items;
			this.activityTotal = cached.total;
			this.activityHasMore = cached.hasMore;
			this.activityPage = cached.page;
			this.activityStatusCounts = cached.statusCounts;
		} else {
			// Otherwise fetch fresh
			this.activityItems = [];
			this.activityPage = 1;
			this.activityHasMore = false;
			this.activityStatusCounts = null;
			this.loadActivity();
		}

		// Reset scroll to top for the new tab.
		const scrollEl = this.$refs.activityScroll;
		if ( scrollEl ) {
			scrollEl.scrollTop = 0;
		}
		this.activityShowTopFade = false;

		this.updateActivityFade();
	},

	/**
	 * Submit birthday (set or update).
	 */
	async submitBirthday() {
		this.birthdaySubmitting = true;
		this.birthdayError = '';
		this.birthdayNotice = '';

		try {
			const formData = new FormData();
			formData.append('action', 'rewardswp_set_birthday');
			formData.append('nonce', this.advocateNonce);
			formData.append('month', this.birthdayMonth);
			formData.append('day', this.birthdayDay);

			const response = await fetch(rewardswpMainVars.ajaxUrl, {
				method: 'POST',
				body: formData,
				credentials: 'same-origin'
			});

			const result = await response.json();

			if (result.success) {
				this.birthdaySuccess = true;
				this.birthdayRewardDate = result.data.reward_date || '';
				this.birthdaySuccessMessage = result.data.birthday;
				this.birthdayIsEditing = true;
				this.birthdayOriginalMonth = this.birthdayMonth;
				this.birthdayOriginalDay = this.birthdayDay;

				// Dispatch event to reactively update the birthday earn action row.
				window.dispatchEvent(new CustomEvent('birthday-saved', {
					detail: { display: result.data.birthday }
				}));
			} else {
				this.birthdayError = result.data?.message || 'Failed to save birthday. Please try again.';
			}
		} catch (error) {
			console.error('Birthday save error:', error);
			this.birthdayError = 'Network error. Please try again.';
		} finally {
			this.birthdaySubmitting = false;
		}
	},

	/**
	 * Reset birthday picker state when navigating away.
	 */
	resetBirthdayPicker() {
		this.birthdaySuccess = false;
		this.birthdayError = '';
		this.birthdaySuccessMessage = '';
		this.birthdayNotice = '';
	},

	/**
	 * Load activity data from server.
	 */
	async loadActivity() {
		// Use config-provided activity data.
		if (this.activityData?.[this.activityTab]) {
			const d = this.activityData[this.activityTab];
			this.activityItems = d.items || [];
			this.activityTotal = d.total || 0;
			this.activityHasMore = false;
			this.activityStatusCounts = d.status_counts || null;
			this.updateActivityFade();
			return;
		}

		if (this.memberId <= 0) {
			return;
		}

		if (typeof rewardswpMainVars === 'undefined' || !rewardswpMainVars.ajaxUrl) {
			console.warn('RewardsWP: Ajax URL not available for activity loading');
			return;
		}

		this.activityLoading = true;

		try {
			const formData = new FormData();
			formData.append('action', 'rewardswp_get_member_activity');
			formData.append('nonce', this.advocateNonce);
			formData.append('tab', this.activityTab);
			formData.append('page', this.activityPage);
			formData.append('per_page', this.activityPerPage);

			const response = await fetch(rewardswpMainVars.ajaxUrl, {
				method: 'POST',
				body: formData
			});

			if (!response.ok) {
				throw new Error(`HTTP error! status: ${response.status}`);
			}

			const result = await response.json();

			if (result.success) {
				this.activityItems = result.data.items || [];
				this.activityTotal = result.data.total || 0;
				this.activityHasMore = result.data.has_more || false;

				// Store status counts for points and referrals tabs
				if (result.data.status_counts && (this.activityTab === 'points' || this.activityTab === 'referrals')) {
					this.activityStatusCounts = result.data.status_counts;

					// Keep referralTotal in sync with latest server data
					if (this.activityTab === 'referrals' && result.data.status_counts.total !== undefined) {
						this.referralTotal = result.data.status_counts.total;
					}
				}
			} else {
				console.error('Activity load error:', result.data?.message);
			}
		} catch (error) {
			console.error('Activity fetch error:', error);
		} finally {
			this.activityLoading = false;
			this.updateActivityFade();
		}
	},

	/**
	 * Load more activity items for pagination.
	 */
	async loadMoreActivity() {
		if (this.activityLoadingMore || !this.activityHasMore) {
			return;
		}

		if (typeof rewardswpMainVars === 'undefined' || !rewardswpMainVars.ajaxUrl) {
			return;
		}

		this.activityLoadingMore = true;
		this.activityPage++;

		try {
			const formData = new FormData();
			formData.append('action', 'rewardswp_get_member_activity');
			formData.append('nonce', this.advocateNonce);
			formData.append('tab', this.activityTab);
			formData.append('page', this.activityPage);
			formData.append('per_page', this.activityPerPage);

			const response = await fetch(rewardswpMainVars.ajaxUrl, {
				method: 'POST',
				body: formData
			});

			if (!response.ok) {
				throw new Error(`HTTP error! status: ${response.status}`);
			}

			const result = await response.json();

			if (result.success) {
				this.activityItems = [...this.activityItems, ...(result.data.items || [])];
				this.activityHasMore = result.data.has_more || false;

				// Update status counts if available (for points and referrals tabs)
				if (result.data.status_counts && (this.activityTab === 'points' || this.activityTab === 'referrals')) {
					this.activityStatusCounts = result.data.status_counts;
				}
			}
		} catch (error) {
			console.error('Load more error:', error);
			this.activityPage--; // Revert page increment on error
		} finally {
			this.activityLoadingMore = false;
			this.updateActivityFade();
		}
	},

	/**
	 * Synchronously compute activity scroll fade states.
	 *
	 * Bottom fade: visible when content overflows and not scrolled to bottom.
	 * Top fade: visible when content overflows and scrolled away from top.
	 */
	syncActivityFade() {
		const el = this.$refs.activityScroll;
		if ( ! el ) {
			this.activityShowFade = false;
			this.activityShowTopFade = false;
			return;
		}
		const canScroll = el.scrollHeight > el.clientHeight;
		const atBottom = ( el.scrollTop + el.clientHeight ) >= ( el.scrollHeight - 10 );
		const atTop = el.scrollTop <= 1;
		this.activityShowFade = canScroll && ! atBottom;
		this.activityShowTopFade = canScroll && ! atTop;
	},

	/**
	 * Update activity fades after AJAX responses.
	 *
	 * Optimistically shows the bottom fade when items exist, then defers
	 * to syncActivityFade() after Alpine renders and the browser lays out.
	 */
	updateActivityFade() {
		if ( this.activityItems.length > 0 ) {
			this.activityShowFade = true;
		}
		window.setTimeout( () => this.syncActivityFade(), 50 );
	},

	}; // End of returned object
	}); // End of rewardsPanelWidget

	// Add the sharingOptions component for handling social sharing
	Alpine.data('sharingOptions', (config = {}) => ({
		showMore: false,
		hasNativeShare: false,

		init() {
			this.hasNativeShare = navigator.share !== undefined;
		},

	shareNative() {
		if (navigator.share) {
			const url = this.userData.referralLink || 'https://yoursite.com/?share=df04c90';
			navigator.share({
				title: 'Join our rewards program',
				text: 'Join me and we both get rewards!',
				url: url
			});
		}
	},

	shareChannel(channel) {
		const url = this.userData.referralLink || 'https://yoursite.com/?share=df04c90';
		const programName = window.rwpDefaults?.['program-name'] || 'our rewards program';
		const text = 'Join me on ' + programName + ' and we both get rewards!';
		const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;

			const shareUrls = {
				twitter: 'https://twitter.com/intent/tweet?text=' + encodeURIComponent(text) + '&url=' + encodeURIComponent(url),
				facebook: 'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(url),
				whatsapp: isIOS ?
					'whatsapp://send?text=' + encodeURIComponent(text + ' ' + url) :
					'https://wa.me/?text=' + encodeURIComponent(text + ' ' + url),
				email: 'mailto:?subject=' + encodeURIComponent('Join me on ' + programName) + '&body=' + encodeURIComponent(text + '\n\n' + url),
				messenger: 'fb-messenger://share?link=' + encodeURIComponent(url),
				linkedin: 'https://www.linkedin.com/sharing/share-offsite/?url=' + encodeURIComponent(url),
				telegram: 'tg://msg?text=' + encodeURIComponent(text + ' ' + url),
				pinterest: 'https://pinterest.com/pin/create/button/?url=' + encodeURIComponent(url) + '&description=' + encodeURIComponent(text),
				reddit: 'https://reddit.com/submit?url=' + encodeURIComponent(url) + '&title=' + encodeURIComponent(text),
				sms: 'sms:&body=' + encodeURIComponent(text + ' ' + url)
			};

			if (channel === 'email' || channel === 'sms' || channel === 'whatsapp' || (isIOS && (channel === 'messenger' || channel === 'telegram'))) {
				window.location.href = shareUrls[channel];
			} else if (shareUrls[channel]) {
				const width = 600;
				const height = 400;
				const left = (window.innerWidth - width) / 2;
				const top = (window.innerHeight - height) / 2;
				window.open(shareUrls[channel], 'share-dialog', 'width=' + width + ',height=' + height + ',left=' + left + ',top=' + top + ',toolbar=no,menubar=no');
			}
		}
	}));

	// Add the becomeMember component for handling member registration and login
	Alpine.data('becomeMember', (config = {}) => ({
		isSubmitting: false,
		showSuccess: false,
		showError: false,
		errorMessage: '',
		showForm: false, // Always start with form hidden
		currentScreen: 'register', // 'register' or 'login'
		justJoined: false, // Track if user just became a member
		showGuestConfirmation: ! ! ( config.isGuestMember && config.userEmail ), // Show one-click confirmation for guest members
		prefillEmail: ( config.isGuestMember && config.userEmail ) ? config.userEmail : '', // Pre-filled email for guest members
		autoShowForm: false, // Auto-show form when triggered from external link
		formData: {
			name: config.userName || '',
			email: config.userEmail || '',
			password: ''
		},
		nonce: config.nonce || '',
		ajaxUrl: config.ajaxUrl || rewardswpMainVars.ajaxUrl,
		isLoggedIn: config.isLoggedIn || false,
		isGuestMember: config.isGuestMember || false,
		userName: config.userName || '',
		userEmail: config.userEmail || '',

		init() {
			// Listen for join-with-email event from confirmation card
			window.addEventListener('rewardswp-join-with-email', (e) => {
				this.prefillEmail = e.detail.email || '';
				this.formData.email = this.prefillEmail;
				this.formData.name = e.detail.name || '';
				this.currentScreen = 'register';
				this.showForm = true;
				this.autoShowForm = true;

				// Dispatch a separate event for the panel to open itself
				// This way the panel component can handle its own opening
				window.dispatchEvent(new CustomEvent('rewardswp-open-panel-to-join', {
					detail: { email: e.detail.email, name: e.detail.name }
				}));
			});

			// Listen for hashchange to hide form when navigating away from join
			window.addEventListener('hashchange', () => {
				const hash = window.location.hash;
				// If navigating away from #rewardswp-join, smoothly hide the form
				if (hash !== '#rewardswp-join' && this.showForm) {
					// Use Alpine's nextTick to ensure smooth transition
					this.$nextTick(() => {
						this.showForm = false;
						this.currentScreen = 'register'; // Reset to initial state
					});
				}
			});

		// Check if navigating via #rewardswp-join hash
		if (window.location.hash === '#rewardswp-join') {
			this.currentScreen = 'register';
			this.showForm = true;
			this.autoShowForm = true;
		}

		// Listen for direct member creation from sticky join buttons (logged-in users)
		window.addEventListener('create-member-direct', () => {
			this.createMember();
		});

		// Listen for show-register-form event from panel
		window.addEventListener('show-register-form', () => {
			this.currentScreen = 'register';
			this.showForm = true;
			this.autoShowForm = true;
		});

		// Watch for form visibility and auto-focus first field
		this.$watch('showForm', (value) => {
				if (value) {
					// Wait for animation to complete (300ms) then focus
					setTimeout(() => {
						const nameInput = this.$el.querySelector('#member-name');
						if (nameInput) {
							nameInput.focus();
						}
					}, 320); // Animation duration + small buffer
				}
			});
		},

		// Switch between register and login screens
		switchToLogin(preserveEmail = false) {
			const email = preserveEmail ? this.formData.email : '';
			this.currentScreen = 'login';
			this.showForm = true;
			this.showError = false;
			this.errorMessage = '';
			this.formData = {
				name: '',
				email: email,
				password: ''
			};
			if (preserveEmail) {
				const rootNode = this.$root.getRootNode();
				setTimeout(() => {
					const passwordField = rootNode.getElementById('login-password');
					if (passwordField) {
						passwordField.focus();
					}
				}, 350);
			}
		},

		switchToRegister() {
			this.currentScreen = 'register';
			this.showForm = false;
			this.showError = false;
			this.errorMessage = '';
			// Clear form data
			this.formData = {
				name: '',
				email: '',
				password: ''
			};
		},

		handleJoinClick() {
			if (this.isLoggedIn || this.isGuestMember) {
				// If user is logged in or is a guest member, directly create member
				this.createMember();
			} else if (this.showForm && this.currentScreen === 'register') {
				// If register form is visible, create member with form data
				this.createMember();
			} else {
				// If not logged in and form not visible, show register form first
				this.currentScreen = 'register';
				this.showForm = true;
			}
		},

		async createMember() {
			this.isSubmitting = true;
			this.showError = false;
			this.errorMessage = '';

			try {
				const formData = new FormData();
				formData.append('action', 'rewardswp_create_member');
				formData.append('nonce', this.nonce);

				// Only append form data if user is not logged in and not a guest member
				if ( ! this.isLoggedIn && ! this.isGuestMember ) {
					formData.append( 'name', this.formData.name );
					formData.append( 'email', this.formData.email );
				}

				// Include honeypot field for anti-spam protection
				const honeypotField = this.$root.querySelector('input[name="rwp_website"]');
				if (honeypotField) {
					formData.append('rwp_website', honeypotField.value);
				}

				const response = await fetch(this.ajaxUrl, {
					method: 'POST',
					body: formData
				});

				const result = await response.json();

		if (result.success) {

			// If member already existed as guest, update panel member data
			if (result.data.linked_guest_account) {
				// Update member data in parent component
				const panel = document.querySelector('[x-data*="rewardsPanelWidget"]');
				if (panel && panel.__x) {
					panel.__x.$data.memberId = result.data.member.id;
					panel.__x.$data.isGuestMember = false; // User is now a full member
					panel.__x.$data.userPoints = result.data.member.points_balance;
					if (panel.__x.$data.userData) {
						panel.__x.$data.userData.points = result.data.member.points_balance;
					}
				}
			}

			// Don't set showSuccess to prevent any flicker - we navigate immediately instead
			this.justJoined = true;
			// Set localStorage flag with member ID for auto-navigation
			if (result.data.member_id) {
				localStorage.setItem('rewardswp_just_joined_' + result.data.member_id, 'true');
			}
				// Dispatch events immediately without delay to prevent flicker
				window.dispatchEvent(new CustomEvent('member-joined', {
					detail: {
						memberId: result.data.member_id,
						memberName: result.data.member_name || '',
						pointsBalance: result.data.points_balance || 0,
						signupPoints: result.data.signup_points || 0,
						referralLink: result.data.referral_link || '',
						freshNonce: result.data.fresh_nonce || '',
						freshClaimNonce: result.data.fresh_claim_nonce || '',
						linkedGuestAccount: result.data.linked_guest_account || false,
					}
				}));
				// Navigate immediately without delay
				window.dispatchEvent(new CustomEvent('navigate-to-earn', {
					detail: {
						memberId: result.data.member_id,
						memberName: result.data.member_name || '',
						pointsBalance: result.data.points_balance || 0,
						signupPoints: result.data.signup_points || 0,
						referralLink: result.data.referral_link || '',
						freshNonce: result.data.fresh_nonce || '',
						freshClaimNonce: result.data.fresh_claim_nonce || '',
					}
				}));
				} else {
					this.showError = true;
					this.errorMessage = result.data?.message || 'An error occurred';

					// If user needs to login (email already exists), offer to switch to login form
					if (result.data?.needs_login || result.data?.redirect_to_login) {
						this.errorMessage += ` If you already have one, please <button x-on:click="switchToLogin(true)" class="font-medium underline text-rwp-accent cursor-pointer">Sign In</button>.`;
					}
				}
			} catch (error) {
				this.showError = true;
				this.errorMessage = 'Network error occurred';
			} finally {
				this.isSubmitting = false;
			}
		},

		async handleLogin() {
			this.isSubmitting = true;
			this.showError = false;
			this.errorMessage = '';

			try {
				const formData = new FormData();
				formData.append('action', 'rewardswp_login_user');
				formData.append('nonce', this.nonce);
				formData.append('email', this.formData.email);
				formData.append('password', this.formData.password);

				const response = await fetch(this.ajaxUrl, {
					method: 'POST',
					body: formData
				});

				const result = await response.json();

				if (result.success) {
					// If we have member data, transition in-place (no reload)
					if (result.data.member_id) {
						const memberDetail = {
							memberId: result.data.member_id,
							memberName: result.data.member_name || '',
							pointsBalance: result.data.points_balance || 0,
							signupPoints: 0,
							referralLink: result.data.referral_link || '',
							freshNonce: result.data.fresh_nonce || '',
							freshClaimNonce: result.data.fresh_claim_nonce || '',
						};

						if (result.data.is_new_member) {
							// New member: welcome flow with navigate-to-earn
							this.justJoined = true;
							localStorage.setItem('rewardswp_just_joined_' + result.data.member_id, 'true');
							window.dispatchEvent(new CustomEvent('member-joined', {
								detail: { ...memberDetail, linkedGuestAccount: false }
							}));
							window.dispatchEvent(new CustomEvent('navigate-to-earn', {
								detail: memberDetail
							}));
						} else {
							// Existing member: go straight to main panel
							window.dispatchEvent(new CustomEvent('member-joined', {
								detail: { ...memberDetail, linkedGuestAccount: false }
							}));
							window.location.hash = 'rewardswp-home';
						}
					} else {
						// No member record — fall back to reload
						window.location.href = window.location.pathname + '#rewardswp-home';
						window.location.reload();
					}
				} else {
					this.showError = true;
					this.errorMessage = result.data?.message || 'Login failed';
				}
			} catch (error) {
				this.showError = true;
				this.errorMessage = 'Network error occurred';
				console.error('[RewardsWP] Login error:', error);
			} finally {
				this.isSubmitting = false;
			}
		}
	}));

	// Friend Claim Form Component
	Alpine.data('friendClaimForm', (config) => ({
		nonce: config.nonce,
		referralCode: config.referralCode,
		ajaxUrl: config.ajaxUrl,
		storeUrl: config.storeUrl,
		checkoutUrl: config.checkoutUrl || '',
		cartUrl: config.cartUrl || '',
		cartItemCount: config.cartItemCount || 0,
		cartProductIds: config.cartProductIds || [],
		integrationSource: config.integrationSource || 'woocommerce',
		alreadyClaimed: config.alreadyClaimed || false,
		existingCoupon: config.existingCoupon || '',
		existingRewardId: config.existingRewardId || null,
		isCouponApplied: config.isCouponApplied || false,
		isAdminPreview: config.isAdminPreview || false,
		isDemo: config.isDemo || false,
		rewardType: config.rewardType || '',
		rewardTitle: config.rewardTitle || '',
		freeProductId: config.freeProductId || '',
		minimumOrderAmount: config.minimumOrderAmount || 0,
		addToCartText: config.addToCartText || 'Activate free product',
		applyToCartText: config.applyToCartText || 'Apply to cart',
		showForm: true, // Show form by default
		showSuccess: false,
		showError: false,
		isSubmitting: false,
		applyingCoupon: false,
		couponApplied: false,
		couponError: false,
		productAddedButCouponFailed: false,
		productWasNewlyAdded: false,
		couponErrorMessage: '',
		remainingToUnlock: 0, // Amount remaining to meet minimum spend
		errorMessage: '',
		successMessage: '',
		couponCode: '',
		rewardId: null,
		formData: {
			email: ''
		},

		// Computed property for cart state
		get isCartEmpty() {
			return this.cartItemCount === 0;
		},

		// Computed property to check if using WooCommerce
		get isWooCommerce() {
			return this.integrationSource === 'woocommerce';
		},

		/**
		 * Get the appropriate button text for free_product rewards based on cart state.
		 * Simple logic: if product is in cart, show "Apply to cart", otherwise "Add to cart".
		 */
		get freeProductButtonText() {
			if (this.rewardType !== 'free_product') {
				return this.applyToCartText;
			}
			if (this.isProductInCart(this.freeProductId)) {
				return this.applyToCartText;
			}
			return this.addToCartText;
		},

		// Check if the free product is already in the cart
		isProductInCart(productId) {
			if (!productId || !this.cartProductIds || this.cartProductIds.length === 0) {
				return false;
			}
			const productIdStr = String(productId);
			const productIdInt = parseInt(productId, 10);

			return this.cartProductIds.some(cartId => {
				// Handle WooCommerce variations: check if parent ID matches
				if (typeof cartId === 'number' || !isNaN(parseInt(cartId, 10))) {
					return parseInt(cartId, 10) === productIdInt || String(cartId) === productIdStr;
				}
				// Handle EDD format "download_id:price_id"
				if (typeof cartId === 'string' && cartId.includes(':')) {
					return cartId.startsWith(productIdStr + ':');
				}
				return String(cartId) === productIdStr;
			});
		},

		// Check if free product is in cart
		get isFreeProductInCart() {
			if (this.rewardType !== 'free_product' || !this.freeProductId) {
				return false;
			}
			return this.isProductInCart(this.freeProductId);
		},

		init() {
			// If already claimed, show success state immediately
			if (this.alreadyClaimed) {
				this.showSuccess = true;
				this.couponCode = this.existingCoupon;
				this.rewardId = this.existingRewardId;
				this.successMessage = 'You already claimed this reward!';
				// Set coupon applied state if it's already applied to cart
				this.couponApplied = this.isCouponApplied;
				// Clear persisted referral code so the sync logic (DOMContentLoaded)
				// doesn't recreate the cookie on the next page load.
				localStorage.removeItem('rewardswp_referral_code');
				// Notify launcher to show "Your gift" text
				window.dispatchEvent(new CustomEvent('friend-claim-success'));
			} else {
				// Auto-focus email field when component initializes (if form is visible)
				this.$nextTick(() => {
					const emailInput = this.$el.querySelector('#claim-email');
					if (emailInput && !this.showSuccess) {
						emailInput.focus();
					}
				});
			}

			// Listen for admin preview state changes (friend vs friend-claimed)
			if (this.isAdminPreview) {
				window.addEventListener('preview-friend-claim-state', (e) => {
					if (e.detail.showSuccess) {
						this.showSuccess = true;
						this.couponCode = e.detail.couponCode || 'PREVIEW-CODE';
						this.successMessage = e.detail.successMessage || 'Reward claimed!';
						this.couponApplied = false;
						// Notify launcher to show "Your gift" text
						window.dispatchEvent(new CustomEvent('friend-claim-success'));
					} else {
						this.showSuccess = false;
						this.couponCode = '';
						this.successMessage = '';
					}
				});
			}
		},

		async claimReward() {
			if (this.isSubmitting) return;

			this.isSubmitting = true;
			this.showError = false;
			this.errorMessage = '';

			try {
				const formData = new FormData();
				formData.append('action', 'rewardswp_claim_reward');
				formData.append('_ajax_nonce', this.nonce);
				formData.append('email', this.formData.email);
				formData.append('referral_code', this.referralCode);

				// Include honeypot field for anti-spam protection
				const honeypotField = this.$root.querySelector('input[name="rwp_website"]');
				if (honeypotField) {
					formData.append('rwp_website', honeypotField.value);
				}

				const response = await fetch(this.ajaxUrl, {
					method: 'POST',
					body: formData
				});

				const result = await response.json();

				if (result.success) {
					this.showSuccess = true;
					this.successMessage = result.data.success_message || 'Reward claimed successfully!';
					this.couponCode = result.data.coupon_code || '';
					this.rewardId = result.data.reward_id || null;
					if (result.data.reward_title) {
						this.rewardTitle = result.data.reward_title;
					}
					// Reset coupon applied state - user needs to manually apply the new coupon
					this.couponApplied = false;
					// Notify launcher to show "Your gift" text
					window.dispatchEvent(new CustomEvent('friend-claim-success'));
				} else {
					this.showError = true;
					this.errorMessage = result.data?.template ?
						this.extractErrorFromTemplate(result.data.template) :
						'An error occurred while claiming your reward.';
				}
			} catch (error) {
				this.showError = true;
				this.errorMessage = 'Network error. Please try again.';
			} finally {
				this.isSubmitting = false;
			}
		},

	async applyCouponToCart() {
		if (this.applyingCoupon || !this.couponCode) return;

		this.applyingCoupon = true;
		this.couponError = false;
		this.couponErrorMessage = '';

		// Mock apply for demo/admin preview — simulate success without AJAX.
		if (this.isAdminPreview || this.isDemo) {
			setTimeout(() => {
				this.couponApplied = true;
				if (this.cartItemCount === 0) {
					this.cartItemCount = 1;
				}
				this.applyingCoupon = false;
			}, 400);
			return;
		}

		try {
			// Read fresh nonce from web component attribute (updated after registration)
			const webComponent = document.querySelector('rewards-panel');
			const freshNonce = webComponent ? webComponent.getAttribute('data-claim-nonce') : this.nonce;

			const formData = new FormData();
			formData.append('action', 'rewardswp_apply_coupon_to_cart');
			formData.append('_ajax_nonce', freshNonce);
			formData.append('coupon_code', this.couponCode);
			formData.append('reward_id', this.rewardId);

				const response = await fetch(this.ajaxUrl, {
					method: 'POST',
					body: formData
				});

				// Check if response is ok
				if (!response.ok) {
					throw new Error(`HTTP error! status: ${response.status}`);
				}

				const result = await response.json();

				if (result.success) {
					this.couponApplied = true;

					// Update cart count - the product was added to cart by the backend.
					// Ensure at least 1 item in cart so "Go to checkout" buttons appear.
					if (this.cartItemCount === 0) {
						this.cartItemCount = 1;
					}
				} else {
					// Coupon failed to apply
					const productWasAdded = result.data?.product_was_added || false;

					if (productWasAdded) {
						// Check if product was already in cart before this action
						const productWasAlreadyInCart = this.freeProductId && this.cartProductIds.includes(parseInt(this.freeProductId, 10));

						// Product added (or was already there) but coupon failed
						this.productAddedButCouponFailed = true;
						this.productWasNewlyAdded = !productWasAlreadyInCart;
						this.couponErrorMessage = result.data?.message || 'Could not apply coupon.';

						// Calculate remaining amount to unlock (minimum - cart total).
						const cartTotal = parseFloat(result.data?.cart_total) || 0;
						const minimumAmount = parseFloat(this.minimumOrderAmount) || 0;
						this.remainingToUnlock = Math.max(0, Math.ceil(minimumAmount - cartTotal));

						// Notify parent component that free product coupon failed.
						window.dispatchEvent(new CustomEvent('free-product-coupon-failed', {
							detail: { remainingToUnlock: this.remainingToUnlock }
						}));

						// Update cart state since product was added
						if (this.cartItemCount === 0) {
							this.cartItemCount = 1;
						}
						if (this.freeProductId && !productWasAlreadyInCart) {
							this.cartProductIds.push(parseInt(this.freeProductId, 10));
						}
					} else {
						// Regular error
						this.couponError = true;
						this.couponErrorMessage = result.data?.message || 'Failed to apply coupon. Please try again.';
					}
				}
			} catch (error) {
				console.error('Coupon apply error:', error);
				this.couponError = true;
				this.couponErrorMessage = 'Network error. Please try again.';
			} finally {
				this.applyingCoupon = false;
			}
		},

		extractErrorFromTemplate(template) {
			// Extract error message from template HTML
			const tempDiv = document.createElement('div');
			tempDiv.innerHTML = template;
			const errorDiv = tempDiv.querySelector('.text-red-700');
			return errorDiv ? errorDiv.textContent.trim() : 'An error occurred.';
		}
	}));

}
