/* Yatoon: scoped i18n wrapper -- keeps __() out of the global namespace. */
(function () {
/**
 * Yatoon i18n bridge.
 *
 * Every user-facing literal in this file is wrapped in __(). This resolves it
 * against the WordPress JS translation catalog registered for this handle via
 * wp_set_script_translations(), so a locale's .json file translates the
 * interactive booking flow -- not just the PHP-rendered chrome around it.
 *
 * The fallback matters: if wp-i18n is unavailable for any reason (an
 * aggressive JS optimizer, a very old core, a hand-rolled enqueue) __() must
 * still return the original English rather than throwing and taking the whole
 * booking form down with it. A missing translation is a cosmetic problem; a
 * dead booking form is a lost sale.
 */
var __ = (function () {
    var d = 'yatoon-booking-system';
    if (typeof window !== 'undefined' && window.wp && window.wp.i18n && typeof window.wp.i18n.__ === 'function') {
        return function (s) { return window.wp.i18n.__(s, d); };
    }
    return function (s) { return s; };
})();

/**
 * Context-aware translation, for msgids that are ambiguous on their own.
 *
 * "Sun" is the case that forced this: as a bare msgid several locales had
 * already translated it as the star, so the weekday row of the date picker
 * read "太阳 Mon Tue...". The PHP side and the .po/.mo/.json catalogues use
 * msgctxt "weekday abbreviation"; this is the browser half of that.
 *
 * 4.6.1 V4: this shim was missing entirely -- _x() was called seven times in
 * the month view and defined nowhere, so opening the month view threw
 * ReferenceError and the calendar never rendered. Same fallback contract as
 * __(): return the original English rather than take the booking form down.
 */
var _x = (function () {
    var d = 'yatoon-booking-system';
    if (typeof window !== 'undefined' && window.wp && window.wp.i18n && typeof window.wp.i18n._x === 'function') {
        return function (s, c) { return window.wp.i18n._x(s, c, d); };
    }
    return function (s) { return s; };
})();

(function($) {
    'use strict';
    // Yatoon booking frontend.

    /**
     * "Now", expressed in the SALON's timezone.
     *
     * FIX (4.4.3): the date picker used plain `new Date()`, i.e. the visitor's
     * clock, to decide which day is "today", which week to show, and what
     * "Today"/"Tomorrow" mean. The calendar being sold belongs to the salon,
     * so those must follow the WordPress timezone instead. With a UTC+8 salon
     * viewed from a UTC-4 browser the picker opened on the wrong day and the
     * customer's first impression was "This date has already passed."
     *
     * Returns a Date shifted so that its LOCAL getters (getFullYear,
     * getMonth, getDate, getHours...) report the salon's wall clock. The
     * picker only ever uses local getters and setDate arithmetic, so this is
     * a drop-in replacement for `new Date()`.
     *
     * getTimezoneOffset() is minutes BEHIND UTC (UTC-4 gives +240), while the
     * server sends minutes EAST of UTC (UTC+8 gives +480), hence the addition.
     *
     * Falls back to the visitor's clock when the offset was not supplied, so
     * an older cached config cannot break the picker.
     */
    function ysSiteNow() {
        // NOTE: wp_localize_script() casts every scalar to a STRING, so this
        // arrives as "480" rather than 480. An earlier revision tested
        // `typeof === 'number'` here, which silently failed that check and
        // fell back to the visitor's clock -- the exact bug this function
        // exists to fix. Coerce and range-check instead of type-checking.
        var raw = (typeof yatoonData !== 'undefined' && yatoonData) ? yatoonData.siteUtcOffsetMin : null;
        if (raw === null || raw === undefined || raw === '') return new Date();

        var off = Number(raw);
        // Real world offsets run from -12:00 to +14:00.
        if (!isFinite(off) || off < -840 || off > 840) return new Date();

        return new Date(Date.now() + (off + new Date().getTimezoneOffset()) * 60000);
    }
    window.ysSiteNow = ysSiteNow;

    /**
     * The salon's UTC offset in minutes east of UTC, or null when unknown.
     *
     * Used to stamp the persisted booking cart: cart entries carry absolute
     * ISO start_at values ("2026-08-10T16:00:00+08:00"). If the site timezone
     * changes while a cart is still in sessionStorage, replaying those values
     * submits the wrong instant and the server rejects it with a confusing
     * "that time slot is no longer available".
     */
    function ysSiteOffsetMin() {
        var raw = (typeof yatoonData !== 'undefined' && yatoonData) ? yatoonData.siteUtcOffsetMin : null;
        if (raw === null || raw === undefined || raw === '') return null;
        var off = Number(raw);
        if (!isFinite(off) || off < -840 || off > 840) return null;
        return off;
    }
    window.ysSiteOffsetMin = ysSiteOffsetMin;

    /**
     * Format money with the site's configured ISO currency and the visitor's
     * locale. Keep a readable fallback for older browsers or an invalid
     * currency code instead of assuming every salon charges US dollars.
     */
    function ysFormatMoney(amount) {
        const value = Number(amount) || 0;
        const currency = (typeof yatoonData !== 'undefined' && yatoonData && yatoonData.currency)
            ? String(yatoonData.currency).toUpperCase()
            : 'USD';
        try {
            // 4.6.2: dropped the hardcoded minimumFractionDigits/maximumFractionDigits: 2.
            // Forcing two decimals is only correct for the two-decimal currencies.
            // A JPY salon saw every price rendered as "¥1,234.00" and a KWD salon
            // lost its third decimal. Intl already knows the right number of minor
            // units per ISO code, so letting it decide is both shorter and correct.
            return new Intl.NumberFormat(undefined, {
                style: 'currency',
                currency: currency
            }).format(value);
        } catch (e) {
            return currency + ' ' + value.toFixed(2);
        }
    }
    window.ysFormatMoney = ysFormatMoney;

    /**
     * What the customer will actually be charged up front, for a given total.
     *
     * 4.6.2: the booking form could only say "Deposit ready" on the service
     * cards. The real figure did not appear until the checkout step, and a cost
     * that only surfaces at the end is one of the most reliable ways to lose a
     * booking that was otherwise finished.
     *
     * Mirrors Yatoon_Stripe::calculate_deposit_cents(), and THE ORDER OF THE
     * TWO CLAMPS MATTERS: cap at the full price first, then raise to the
     * gateway minimum. The other way round quotes less than the customer is
     * charged on a cheap service, which is worse than showing nothing at all -
     * it reads as a bait and switch at exactly the moment they hand over a card.
     *
     * yatoonData.depositMinimum arrives in major units and is already resolved
     * for zero-decimal currencies, so there is no currency table here.
     *
     * @param {number} total Order total in major units.
     * @returns {number} Amount due now, or 0 when deposits are not in play.
     */
    function ysCalcDeposit(total) {
        if (typeof yatoonData === 'undefined' || !yatoonData || !yatoonData.depositEnabled) return 0;
        const amount = Number(yatoonData.depositAmount);
        const gross = Number(total);
        if (!isFinite(amount) || amount <= 0 || !isFinite(gross) || gross <= 0) return 0;

        let deposit = (yatoonData.depositType === 'percent') ? gross * (amount / 100) : amount;
        deposit = Math.min(deposit, gross);
        const floor = Number(yatoonData.depositMinimum);
        if (isFinite(floor) && floor > 0) deposit = Math.max(floor, deposit);
        return deposit;
    }
    window.ysCalcDeposit = ysCalcDeposit;

    // -- Multi-location: inject yatoon_location_id into every AJAX POST -------
    // yatoonData.locationId is set by wp_localize_script when [yatoon_booking location="N"] is used.
    if ( typeof yatoonData !== 'undefined' && yatoonData.locationId ) {
        $.ajaxSetup({
            beforeSend: function(xhr, settings) {
                if ( settings.type === 'POST' && settings.data ) {
                    if ( typeof settings.data === 'string' ) {
                        settings.data += '&yatoon_location_id=' + encodeURIComponent(yatoonData.locationId);
                    } else if ( typeof settings.data === 'object' ) {
                        settings.data.yatoon_location_id = yatoonData.locationId;
                    }
                }
            }
        });
    }

    // -- US Phone number validation (hoisted to IIFE scope so class methods can access) --
    function validateUSPhone(raw) {
        const digits = (raw || '').replace(/\D/g, '');
        const d = (digits.length === 11 && digits[0] === '1') ? digits.slice(1) : digits;
        if (d.length !== 10) return __('Please enter a 10-digit US phone number.');
        const area     = d.slice(0, 3);
        const exchange = d.slice(3, 6);
        if (area[0] === '0' || area[0] === '1')     return __('Invalid area code. US area codes cannot start with 0 or 1.');
        // 4.4.3: this used to say only "Invalid phone number format.", which
        // gives the customer no idea WHICH digits are wrong -- they can retype
        // the same number repeatedly and never find it. The rule is the North
        // American numbering plan: the three digits after the area code (the
        // central office code) cannot begin with 0 or 1. Say so.
        if (exchange[0] === '0' || exchange[0] === '1') {
            return __('The 3 digits after the area code cannot start with 0 or 1. Please check the number.');
        }
        if (new Set(d).size === 1) return __('Please enter a valid phone number.');
        const fake = ['1234567890','0123456789','9876543210','5555555555','1231231234'];
        if (fake.includes(d)) return __('Please enter a valid phone number.');
        return '';
    }

    /**
     * 4.6.2: generic inline field error, extracted from showPhoneError().
     *
     * Before this, phone was the ONLY step-5 field with an inline error, and its
     * markup/aria wiring lived inline in showPhoneError(). Name and email had
     * nothing at all -- a typo'd email only surfaced as a native validation
     * bubble after the customer pressed Confirm, which is the worst possible
     * moment to send them back up the form.
     *
     * The error node is created next to the input, given role="alert" so a
     * screen reader announces it the moment it appears, and pointed at from the
     * input via aria-describedby. aria-invalid mirrors the same state, so an
     * assistive technology reports the field as invalid rather than relying on
     * the red border alone.
     *
     * @param {string} inputId Field id, without '#'.
     * @param {string} msg     Error text, or '' to clear.
     */
    function showFieldError(inputId, msg) {
        const $input = $('#' + inputId);
        if (!$input.length) return;
        const errId = inputId + '-error';
        let $err = $('#' + errId);
        if (!$err.length) {
            // Keep the inline style: this node is created by JS and must stay
            // legible on a theme that stripped or never loaded the plugin CSS.
            $err = $('<p class="sbs-field-error" role="alert" aria-live="assertive" style="color:#dc2626;font-size:12px;margin-top:4px;"></p>')
                .attr('id', errId);
            $input.after($err);
        }
        $err.text(msg).toggle(!!msg);
        $input
            .css('border-color', msg ? '#dc2626' : '')
            .attr('aria-invalid', msg ? 'true' : 'false');
        if (msg) {
            $input.attr('aria-describedby', errId);
        } else {
            $input.removeAttr('aria-describedby');
        }
    }

    function showPhoneError(msg) {
        // 4.6.2: this built its own node with the id "sbs-phone-error";
        // showFieldError() derives "sbs-customer-phone-error" from the input id
        // instead. Nothing else in the plugin referenced the old id, and the
        // styling hook is now the shared .sbs-field-error class.
        showFieldError('sbs-customer-phone', msg);
    }

    /**
     * 4.6.2: name/email validators for step 5, mirroring validateUSPhone().
     *
     * Return '' when valid, or a translated message. Kept deliberately loose:
     * the goal is catching the typo the customer can see and fix ("gmail.con",
     * an empty surname), not rejecting exotic-but-legal addresses. Anything
     * stricter turns a conversion aid into a conversion blocker.
     */
    function validateCustomerName(raw) {
        const val = (raw || '').trim();
        if (val.length < 2) return __('Please enter your name.');
        return '';
    }

    function validateCustomerEmail(raw) {
        const val = (raw || '').trim();
        // One @, something either side, and a dotted TLD of 2+ chars. This is
        // the same shape the browser's type="email" check uses, minus the parts
        // that only matter to a mail server.
        if (!/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(val)) {
            return __('Please enter a valid email address.');
        }
        return '';
    }

    /**
     * 4.6.2: minimal focus trap for the plugin's JS-built modal overlays.
     *
     * The duplicate-service dialog already carried role="dialog"
     * aria-modal="true", which PROMISES a keyboard user that focus is contained
     * -- but nothing implemented it. Tab walked straight out of the dialog into
     * the page behind it, which is still fully interactive, and there was no
     * Escape handler, so a keyboard-only customer could end up operating the
     * service list underneath a dialog they could neither see past nor dismiss.
     *
     * Returns a release function; call it when the overlay is removed. Focus is
     * returned to whatever was focused before the overlay opened, so the
     * customer lands back on the card they tapped rather than at the top of the
     * document.
     *
     * @param {jQuery}   $overlay The overlay element.
     * @param {Function} onEscape Called when Escape is pressed (usually close).
     * @returns {Function}        Release/cleanup function.
     */
    function ysFocusTrap($overlay, onEscape) {
        if (!$overlay || !$overlay.length) return function () {};
        const opener = document.activeElement;
        const SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';

        function focusable() {
            return $overlay.find(SELECTOR).filter(':visible');
        }

        const ns = 'keydown.ysFocusTrap';
        $(document).on(ns, function (e) {
            if (!$.contains(document, $overlay[0])) return;
            if (e.key === 'Escape' || e.key === 'Esc') {
                e.preventDefault();
                if (typeof onEscape === 'function') onEscape();
                return;
            }
            if (e.key !== 'Tab') return;
            const $items = focusable();
            if (!$items.length) {
                // Nothing tabbable inside: pin focus on the dialog rather than
                // letting Tab escape to the page behind it.
                e.preventDefault();
                return;
            }
            const first = $items.first()[0];
            const last  = $items.last()[0];
            const inside = $.contains($overlay[0], document.activeElement);
            if (e.shiftKey && (document.activeElement === first || !inside)) {
                e.preventDefault();
                last.focus();
            } else if (!e.shiftKey && (document.activeElement === last || !inside)) {
                e.preventDefault();
                first.focus();
            }
        });

        const $initial = focusable().first();
        if ($initial.length) {
            $initial.trigger('focus');
        } else {
            $overlay.attr('tabindex', '-1').trigger('focus');
        }

        return function releaseFocusTrap() {
            $(document).off(ns);
            if (opener && typeof opener.focus === 'function' && document.contains(opener)) {
                try { opener.focus(); } catch (e) {}
            }
        };
    }

    class SquareBookingForm {
	        constructor() {
	            this._stateVersion = '3.5.0-booking-flow';
	            this.currentStep = 1;
            this.selectedService = null;
            this.selectedCategory = null; // FIX: track parent category
            this.selectedOptions = [];
            this.selectedStaff = null;
            this.selectedDateTime = null;
            this.bookingConfirmed = false;
            this.designUploadAttachmentId = 0;
            this.designUploadToken = '';
            this.designUploadInProgress = false;
            this.designUploadImageUrl = '';
            this.designUploadFileName = '';
            
            // Cart: stores completed appointments (each with its own time)
            this.bookingCart = [];

            // pendingServices: multiple services+staff being built into ONE appointment
            // e.g. [{service, options, staff, guestLabel}, ...]
            // They all share the same start time chosen in step 4
            this.pendingServices = [];

            // Guest booking: label for the NEXT person being added (null = 'Me')
            this._nextGuestLabel = null;    // e.g. 'Me', 'Guest', 'Guest 2'
            this._guestCount     = 0;       // how many guests added so far
            this._staffCache     = {};      // service_id -> [{id,name,image_url}]

            // Track removed items to prevent re-adding them
            this._pendingAdd = false;
            this._optionsRequest = null;
            this._staffRequest = null;
            this._timesRequest = null;
            this._optionsSeq = 0;
            this._staffSeq = 0;
            this._timesSeq = 0;
            this._dateAvailability = {};

            // 4.6.2: availability-density state (calendar heat map).
            // Same sequence-number + abort pattern as the three lookups above,
            // so a slow response for last month cannot overwrite the density of
            // the month the customer is actually looking at.
            this._densityRequest = null;
            this._densitySeq = 0;
            this._densityKey = '';

            // 4.6.2: debounce timer for loadAvailableTimes(). See the comment on
            // that method for which call sites debounce and which run at once.
            this._timesDebounceTimer = null;

            // 4.6.2: auto-advance bookkeeping for steps 2 and 3.
            // _autoAdvanceTimer is the pending "commit this choice" timer;
            // _autoAdvanceBlockedUntil is the back-navigation guard.
            this._autoAdvanceTimer = null;
            this._autoAdvanceBlockedUntil = 0;

            // 4.6.2: focus-trap release handle for the duplicate-service dialog.
            this._dupDialogRelease = null;

            this.init();
        }

        // -- Scroll Helper ----------------------------------------------------
        _topObstructionHeight() {
            // WP admin bar (logged-in users)
            let topBarH = 0;
            const $adminBar = $('#wpadminbar');
            if ($adminBar.length) topBarH += ($adminBar.outerHeight() || 0);

            // Manual offset from plugin settings
            const manualOffset = (yatoonData && yatoonData.headerOffset) ? parseInt(yatoonData.headerOffset) : 0;

            if (manualOffset > 0) {
                // Use configured value - but on mobile the nav is often shorter, cap it
                const isMobile = window.innerWidth <= 768;
                topBarH += isMobile ? Math.min(manualOffset, 90) : manualOffset;
            } else {
                // Auto-detect: scan all elements fixed/sticky at top of viewport
                $('*').each(function() {
                    try {
                        const el = this;
                        const pos = window.getComputedStyle(el).position;
                        if ((pos === 'fixed' || pos === 'sticky') && el.offsetHeight > 20) {
                            const rect = el.getBoundingClientRect();
                            if (rect.top <= 8 && rect.bottom > 20 && rect.width >= window.innerWidth * 0.45) {
                                topBarH = Math.max(topBarH, rect.bottom);
                            }
                        }
                    } catch(e) {}
                });

                // Some mobile themes make a child sticky while a transformed
                // header wrapper actually covers the viewport. Probe what is
                // visibly under the top edge as a second, layout-based check.
                const probeX = Math.max(1, Math.floor(window.innerWidth / 2));
                const probes = document.elementsFromPoint ? document.elementsFromPoint(probeX, 6) : [];
                probes.forEach(function(node) {
                    let el = node;
                    for (let depth = 0; el && depth < 6; depth++, el = el.parentElement) {
                        try {
                            if (el === document.body || el === document.documentElement) continue;
                            const rect = el.getBoundingClientRect();
                            const style = window.getComputedStyle(el);
                            const identity = ((el.tagName || '') + ' ' + (el.id || '') + ' ' + (el.className || '')).toLowerCase();
                            const headerLike = /header|masthead|navbar|site-nav|sticky|brizy/.test(identity);
                            const anchored = style.position === 'fixed' || style.position === 'sticky';
                            if ((headerLike || anchored) && rect.top <= 12 && rect.bottom > 20 && rect.width >= window.innerWidth * 0.45) {
                                topBarH = Math.max(topBarH, rect.bottom);
                            }
                        } catch(e) {}
                    }
                });
            }

            return Math.max(0, topBarH);
        }

        _scrollToTarget($target, gap) {
            if (!$target || !$target.length) return;
            const target = $target.get(0);

            // Step changes can render preloaded/AJAX content and update the
            // sidebar in the same tick. Coalesce every request into one final
            // scroll after layout, otherwise the page visibly moves up, down,
            // then up again while competing timers target different anchors.
            if (this._stepScrollFrame) window.cancelAnimationFrame(this._stepScrollFrame);
            this._stepScrollFrame = window.requestAnimationFrame(() => {
                this._stepScrollFrame = null;
                if (!target || !document.documentElement.contains(target)) return;
                const topBarH = this._topObstructionHeight();
                const scrollTo = Math.max(0, $(target).offset().top - topBarH - gap);
                if (Math.abs(window.scrollY - scrollTo) < 2) return;
                $('html, body').stop(true, true);
                window.scrollTo({ top: scrollTo, behavior: 'auto' });
            });
        }

        _scrollToFormTop() {
            const mobileExtraGap = window.innerWidth <= 768 ? 28 : 0;
            this._scrollToTarget($('.sbs-booking-container'), 12 + mobileExtraGap);
        }

        _scrollToStaffChoices() {
            const $target = $('.sbs-step-3 .sbs-staff-grid');
            if (!$target.length) {
                this._scrollToFormTop();
                return;
            }

            // Go directly to the final anchor. Scrolling to the form first and
            // the staff cards second caused the visible vertical bounce.
            this._scrollToTarget($target, 32);
        }

        _esc(value) {
            return String(value || '').replace(/[&<>"']/g, function(ch) {
                return {
                    '&': '&amp;',
                    '<': '&lt;',
                    '>': '&gt;',
                    '"': '&quot;',
                    "'": '&#039;'
                }[ch];
            });
        }

        // -- Session Persistence ----------------------------------------------
        _saveSession() {
            try {
	                const state = {
	                    version:          this._stateVersion,
	                    // 4.4.3: staleness guards -- see _restoreSession().
	                    tzOffset:         ysSiteOffsetMin(),
	                    savedAt:          Date.now(),
	                    currentStep:      this.currentStep,
                    selectedService:  this.selectedService,
                    selectedCategory: this.selectedCategory,
                    selectedOptions:  this.selectedOptions,
                    selectedStaff:    this.selectedStaff,
                    selectedDateTime: this.selectedDateTime,
                    bookingCart:      this.bookingCart,
                    pendingServices:  this.pendingServices,
                    _nextGuestLabel:  this._nextGuestLabel,
                    _guestCount:      this._guestCount,
                    groupIntent:     !!window._sbsGroupIntent,
                };
                sessionStorage.setItem('yatoon_booking_state', JSON.stringify(state));
            } catch(e) {}
        }

        _restoreSession() {
            try {
                const raw = sessionStorage.getItem('yatoon_booking_state');
                if (!raw) return false;
	                const state = JSON.parse(raw);
	                if (!state || (!state.selectedService && !(state.pendingServices || []).length && !(state.bookingCart || []).length)) return false;
	                if (state.version !== this._stateVersion) {
	                    this._clearSession();
	                    return false;
	                }
	                // 4.4.3: the cart stores ABSOLUTE start_at strings carrying the
	                // site's UTC offset at the moment the slot was picked. Replaying
	                // them after the salon's timezone changed submits the wrong
	                // instant, and the only feedback the customer gets is a generic
	                // "that time slot is no longer available". Drop the cart instead.
	                const _curOff = ysSiteOffsetMin();
	                if (_curOff !== null && state.tzOffset !== _curOff) {
	                    this._clearSession();
	                    return false;
	                }
	                // Absolute times also rot with age: a tab left open overnight
	                // restores a cart whose slots are already in the past.
	                if (!state.savedAt || (Date.now() - state.savedAt) > 12 * 60 * 60 * 1000) {
	                    this._clearSession();
	                    return false;
	                }
                this.currentStep      = state.currentStep      || 1;
                this.selectedService  = state.selectedService  || null;
                this.selectedCategory = state.selectedCategory || null;
                this.selectedOptions  = state.selectedOptions  || [];
                this.selectedStaff    = state.selectedStaff    || null;
                this.selectedDateTime = state.selectedDateTime || null;
                this.bookingCart      = state.bookingCart      || [];
                this.pendingServices  = state.pendingServices  || [];
                this._nextGuestLabel  = state._nextGuestLabel  || null;
                this._guestCount      = state._guestCount      || 0;
                window._sbsGroupIntent = !!state.groupIntent;
                if (this.bookingCart.length && !this.selectedService && !this.pendingServices.length && this.currentStep < 5) {
                    this.currentStep = 5;
                }
                return true;
            } catch(e) { return false; }
        }

        _consumeRebookRequest() {
            try {
                const raw = sessionStorage.getItem('yatoon_rebook_request');
                if (!raw) return false;
                sessionStorage.removeItem('yatoon_rebook_request');

                const request = JSON.parse(raw);
                const isFresh = request && request.createdAt && (Date.now() - request.createdAt) <= 10 * 60 * 1000;
                if (!isFresh || request.version !== 1 || !Array.isArray(request.services)) return false;

                const selections = request.services.filter(function (item) {
                    return item && item.service && parseInt(item.service.id, 10) > 0;
                }).map(function (item) {
                    return {
                        service: Object.assign({}, item.service),
                        options: Array.isArray(item.options) ? item.options.map(function (option) { return Object.assign({}, option); }) : [],
                        optionsComplete: item.optionsComplete !== false,
                        staff: item.staff ? Object.assign({}, item.staff) : null,
                        eligibleStaffIds: Array.isArray(item.eligibleStaffIds) ? item.eligibleStaffIds.slice() : [],
                        guestLabel: item.guestLabel || __('Me'),
                        groupAppointment: false,
                        design: null
                    };
                });
                if (!selections.length) return false;

                this._clearSession();
                this.selectedService = null;
                this.selectedCategory = null;
                this.selectedOptions = [];
                this.selectedStaff = null;
                this.selectedDateTime = null;
                this.bookingCart = [];
                this.pendingServices = [];
                this._nextGuestLabel = null;
                this._guestCount = 0;
                window._sbsGroupIntent = false;

                const needsOptionsIndex = selections.findIndex(function (item) { return !item.optionsComplete; });
                const needsStaffIndex = selections.findIndex(function (item) { return !item.staff; });
                if (needsOptionsIndex !== -1) {
                    const current = selections[needsOptionsIndex];
                    this.selectedService = current.service;
                    this.selectedCategory = current.service.category || null;
                    this.selectedOptions = current.options;
                    this.selectedStaff = null;
                    this.pendingServices = selections.filter(function (item, index) {
                        return index !== needsOptionsIndex && item.optionsComplete;
                    }).map(function (item) {
                        if (item.staff) return item;
                        item.staff = { id: 0, name: __('Any Staff'), price_override: null };
                        return item;
                    });
                    this.currentStep = 2;
                } else if (needsStaffIndex === -1) {
                    this.pendingServices = selections;
                    this.currentStep = 4;
                } else {
                    const current = selections[needsStaffIndex];
                    this.selectedService = current.service;
                    this.selectedCategory = current.service.category || null;
                    this.selectedOptions = current.options;
                    this.selectedStaff = null;
                    this.pendingServices = selections.filter(function (item, index) {
                        return index !== needsStaffIndex;
                    }).map(function (item) {
                        if (item.staff) return item;
                        item.staff = { id: 0, name: __('Any Staff'), price_override: null };
                        return item;
                    });
                    this.currentStep = 3;
                }

                this._saveSession();
                return true;
            } catch(e) {
                try { sessionStorage.removeItem('yatoon_rebook_request'); } catch(ignore) {}
                return false;
            }
        }

        _clearSession() {
            try { sessionStorage.removeItem('yatoon_booking_state'); } catch(e) {}
        }

        _abortLookupRequests() {
            // 4.6.2: a pending debounce timer is an in-flight lookup that has
            // not left the browser yet. Cancelling the XHRs but leaving the
            // timer armed would fire a request for state we just tore down.
            if (this._timesDebounceTimer) {
                clearTimeout(this._timesDebounceTimer);
                this._timesDebounceTimer = null;
            }
            ['_optionsRequest', '_staffRequest', '_timesRequest', '_densityRequest'].forEach((key) => {
                const req = this[key];
                if (req && req.readyState !== 4 && typeof req.abort === 'function') {
                    try { req.abort(); } catch(e) {}
                }
                this[key] = null;
            });
        }
        // --------------------------------------------------------------------

        _updateOptionActionSummary() {
            const $step = $('.sbs-step-2');
            const $summary = $step.find('.sbs-option-action-summary');
            const $next = $step.find('.sbs-btn-next');
            if (!$summary.length || !$next.length) return;

            const selected = this.selectedOptions || [];
            const mainSelected = selected.filter(o => !o.is_addon);
            const addonSelected = selected.filter(o => o.is_addon);
            const requiresMain = !!this._optionHasMainGroup;
            const canContinue = !requiresMain || mainSelected.length > 0;
            const price = this._optionsPrice(this.selectedService, selected);
            const duration = selected.length
                ? selected.reduce((sum, o) => sum + (parseInt(o.duration) || 0), 0)
                : parseInt((this.selectedService || {}).duration || 0);
            const addonText = addonSelected.length
                ? ` + ${addonSelected.length} add-on${addonSelected.length > 1 ? 's' : ''}`
                : '';
            const title = canContinue
                ? `${mainSelected.length ? mainSelected[0].name : ((this.selectedService || {}).name || __('Service'))}${addonText}`
                : __('Choose one option to continue');
            const meta = canContinue
                ? `${duration || 0} min - ${price > 0 ? ysFormatMoney(price) : __('Price varies')}`
                : __('Add-ons are optional after the main choice');

            $summary.html(`
                <div class="sbs-option-action-title">${this._esc(title)}</div>
                <div class="sbs-option-action-meta">${this._esc(meta)}</div>
            `).toggleClass('is-ready', canContinue);
            $next
                .prop('disabled', false)
                .attr('aria-disabled', canContinue ? 'false' : 'true')
                .toggleClass('is-disabled', !canContinue);
        }

        // -- 4.6.2: single-choice auto-advance (steps 2 and 3) --------------------
        //
        // Steps 2 (Options) and 3 (Staff) each ended with their own Continue
        // button, even when the step offered exactly one decision to make. The
        // customer tapped their choice, saw it highlight, and then had to tap
        // Continue to confirm a decision they had already communicated - two
        // taps for one intent, twice, on the narrowest part of the funnel.
        //
        // Auto-advance removes the second tap for the single-choice case only.
        // Multi-select stays on Continue: with add-ons the customer may want
        // several, so advancing on the first tick would be actively wrong.
        //
        // THE RISK THIS GUARDS AGAINST: the classic failure of this pattern is
        // "user presses Back, lands on step 3 where a staff member is still
        // selected, and is instantly bounced forward to step 4 again" - Back
        // becomes impossible to use. Two independent defences:
        //
        //   1. Auto-advance is scheduled ONLY from the user's own click/change
        //      event handlers, never from _selectStaffCard() or the option
        //      restore loop. Coming back re-selects the previous choice
        //      programmatically through exactly those functions, which no
        //      longer schedule anything, so nothing fires.
        //   2. goToStep() calls _blockAutoAdvance() whenever the new step is
        //      LOWER than the current one, which cancels any pending timer and
        //      refuses new ones for a short window. That covers the Back button,
        //      browser Back, the progress bar, the summary "Edit" links, the
        //      accordion, and any backwards path added later, because they all
        //      funnel through goToStep().

        _blockAutoAdvance() {
            if (this._autoAdvanceTimer) {
                clearTimeout(this._autoAdvanceTimer);
                this._autoAdvanceTimer = null;
            }
            // Long enough to outlive the re-selection that happens while a step
            // re-renders after Back, short enough that a customer who lands on
            // step 3 and immediately taps a different person still gets the
            // fast path.
            this._autoAdvanceBlockedUntil = Date.now() + 900;
        }

        /**
         * Schedule an auto-advance out of `step`.
         *
         * The delay is the point, not an implementation detail: advancing on the
         * same tick as the tap makes the selected state flash by before the
         * customer can register that their choice landed, which reads as the
         * form jumping on its own. ~260ms is long enough to see the selection
         * confirmed and short enough not to feel like waiting.
         *
         * @param {number}   step Step this advance belongs to.
         * @param {Function} run  What to do when it fires.
         */
        _scheduleAutoAdvance(step, run) {
            const self = this;
            if (self._autoAdvanceTimer) {
                clearTimeout(self._autoAdvanceTimer);
                self._autoAdvanceTimer = null;
            }
            if (Date.now() < self._autoAdvanceBlockedUntil) return;
            if (self.currentStep !== step) return;
            self._autoAdvanceTimer = setTimeout(function() {
                self._autoAdvanceTimer = null;
                // Re-check on fire: during the delay the customer may have hit
                // Back, tapped a different card, or navigated away entirely.
                if (Date.now() < self._autoAdvanceBlockedUntil) return;
                if (self.currentStep !== step) return;
                run();
            }, 260);
        }

        /**
         * Is step 2 a single-choice step (one required pick, nothing optional)?
         *
         * Only then is auto-advance right. Anything else - an add-on group the
         * customer can multi-select, several groups to work through, or a design
         * photo they may want to attach - needs Continue, because leaving the
         * step is not implied by the tap that just happened.
         */
        _isSingleChoiceOptionStep() {
            const $groups = $('.sbs-options-container .sbs-option-group');
            if ($groups.length !== 1) return false;
            if (String($groups.first().attr('data-multi')) === '1') return false;
            // A reference-photo upload lives on this step; advancing past it
            // would silently drop a feature the customer was about to use.
            if (this.selectedService && this.selectedService.allowDesignUpload) return false;
            if ((this.selectedOptions || []).some(function(o) { return !!o.allowDesignUpload; })) return false;
            return true;
        }

        _ensureOptionActionSummary() {
            const $nav = $('.sbs-step-2 .sbs-navigation-buttons');
            if (!$nav.length) return;
            if (!$nav.find('.sbs-option-action-summary').length) {
                $nav.prepend('<div class="sbs-option-action-summary" aria-live="polite"></div>');
            }
            this._updateOptionActionSummary();
        }

        _stepCopy(step) {
            const map = {
                1: { title: __('Choose service'), hint: __('Pick a service, then customize it if options are available.') },
                2: { title: __('Customize service'), hint: __('Select the option that fits your visit.') },
                3: { title: __('Choose staff'), hint: __('Any available staff is fastest, or choose your preferred person.') },
                4: { title: __('Choose time'), hint: __('Pick a date and available time.') },
                5: { title: __('Review your booking'), hint: __('Check your appointment and enter your details.') }
            };
            return map[step] || map[1];
        }

        _publicStepMeta(step) {
            // Matches the 5-dot progress bar 1:1 so the two step indicators
            // never disagree with each other on screen.
            const progressByStep = { 1: 0, 2: 20, 3: 40, 4: 60, 5: 100 };
            return { current: step, total: 5, progress: progressByStep[step] ?? 0 };
        }

        _formatSelectedDateTime() {
            if (!this.selectedDateTime || !this.selectedDateTime.date) return '';
            const parts = String(this.selectedDateTime.date).split('-');
            const dt = new Date(parts[0], (parseInt(parts[1], 10) || 1) - 1, parts[2]);
            const dateLabel = dt.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' });
            return dateLabel + (this.selectedDateTime.display ? ' at ' + this.selectedDateTime.display : '');
        }

        _stepAccordionData() {
            const bookingConfirmed = !!this.bookingConfirmed || $('.sbs-step-success:visible').length > 0;
            const hasService = !!this.selectedService || this.pendingServices.length > 0 || this.bookingCart.length > 0;
            const hasOptions = (this.selectedOptions || []).length > 0 || this.pendingServices.some(s => (s.options || []).length > 0);
            const hasStaff = !!this.selectedStaff || this.pendingServices.some(s => !!s.staff) || this.bookingCart.length > 0;
            const hasTime = !!this.selectedDateTime || this.bookingCart.length > 0;
            const serviceName = this.selectedService
                ? this.selectedService.name
                : (this.pendingServices[0] && this.pendingServices[0].service ? this.pendingServices[0].service.name : '');
            const serviceCount = this.bookingCart.length
                ? this.bookingCart.reduce((count, item) => count + ((item.services && item.services.length) ? item.services.length : 1), 0)
                : (this.pendingServices.length || (this.selectedService ? 1 : 0));
            const optionLabel = hasOptions
                ? (this.selectedOptions || []).map(o => o.name).join(', ') || __('Options selected')
                : __('No options selected yet');
            const staffLabel = this.selectedStaff
                ? this.getFirstName(this.selectedStaff.name)
                : (hasStaff ? __('Staff selected') : __('Choose staff'));
            const timeLabel = this._formatSelectedDateTime() || (hasTime ? __('Time selected') : __('Choose a time'));

            return [
                {
                    key: 'service',
                    number: 1,
                    title: __('Service'),
                    target: this.currentStep <= 1 ? this.currentStep : 1,
                    isActive: this.currentStep === 1,
                    isDone: hasService && this.currentStep > 1,
                    isLocked: false,
                    summary: hasService
                        ? (serviceCount > 1 ? serviceCount + ' services selected' : serviceName || __('Service selected'))
                        : __('Pick a service to begin')
                },
                {
                    key: 'customize',
                    number: 2,
                    title: __('Options'),
                    target: 2,
                    isActive: this.currentStep === 2,
                    isDone: hasService && this.currentStep > 2,
                    isLocked: !hasService,
                    summary: hasOptions ? optionLabel : (this.currentStep > 2 ? __('No options selected') : __('Choose add-ons or options'))
                },
                {
                    key: 'staff',
                    number: 3,
                    title: 'Staff',
                    target: 3,
                    isActive: this.currentStep === 3,
                    isDone: hasStaff && this.currentStep > 3,
                    isLocked: !hasService,
                    summary: hasStaff ? staffLabel : __('Choose any available staff or a preferred person')
                },
                {
                    key: 'time',
                    number: 4,
                    title: __('Date & Time'),
                    target: 4,
                    isActive: this.currentStep === 4,
                    isDone: hasTime && this.currentStep > 4,
                    isLocked: !hasService || (!hasStaff && this.currentStep < 4),
                    summary: timeLabel
                },
                {
                    key: 'details',
                    number: 5,
                    title: __('Details / Pay'),
                    target: 5,
                    isActive: this.currentStep === 5 && !bookingConfirmed,
                    isDone: bookingConfirmed,
                    isLocked: !hasTime && this.currentStep < 5,
                    summary: bookingConfirmed ? __('Booking confirmed') : (hasTime || this.currentStep === 5 ? __('Review details and confirm') : __('Available after choosing a time'))
                }
            ];
        }

        _updateStepAccordion() {
            const $wrap = $('#sbs-step-accordion');
            if (!$wrap.length) return;
            const rows = this._stepAccordionData().map(step => {
                const classes = [
                    'sbs-step-accordion-item',
                    step.isActive ? 'is-active' : '',
                    step.isDone ? 'is-done' : '',
                    step.isLocked ? 'is-locked' : 'is-clickable'
                ].filter(Boolean).join(' ');
                const status = step.isLocked ? __('Locked') : (step.isDone ? 'Done' : (step.isActive ? __('Current') : 'Open'));
                return `
                    <button type="button" class="${classes}" data-step-target="${step.target}" ${step.isLocked ? 'disabled aria-disabled="true"' : 'aria-disabled="false"'}>
                        <span class="sbs-step-accordion-num">${step.isDone ? 'Done' : step.number}</span>
                        <span class="sbs-step-accordion-copy">
                            <span class="sbs-step-accordion-title">${this._esc(step.title)}</span>
                            <span class="sbs-step-accordion-summary">${this._esc(step.summary)}</span>
                        </span>
                        <span class="sbs-step-accordion-status">${this._esc(status)}</span>
                    </button>`;
            }).join('');
            $wrap.html(rows);
            if (!!this.bookingConfirmed || $('.sbs-step-success:visible').length > 0) {
                this._markBookingFlowComplete();
            }
        }

        _markBookingFlowComplete() {
            this.bookingConfirmed = true;

            const $mobileHeader = $('#sbs-mobile-flow-header');
            if ($mobileHeader.length) {
                $mobileHeader.html(`
                    <div>
                        <span class="sbs-mobile-flow-step">${__('Complete')}</span>
                        <strong>${__('Booking confirmed')}</strong>
                        <p>${__('Your appointment details are below.')}</p>
                    </div>
                    <span class="sbs-mobile-flow-count">${__('Booked')}</span>
                    <span class="sbs-mobile-flow-progress" aria-hidden="true"><i style="width:100%"></i></span>
                `).addClass('is-complete');
            }

            const $details = $('#sbs-step-accordion .sbs-step-accordion-item[data-step-target="5"]');
            if ($details.length) {
                $details
                    .removeClass('is-active is-locked')
                    .addClass('is-done is-clickable')
                    .attr('aria-disabled', 'false')
                    .prop('disabled', false);
                $details.find('.sbs-step-accordion-num').text('Done');
                $details.find('.sbs-step-accordion-summary').text(__('Booking confirmed'));
                $details.find('.sbs-step-accordion-status').text('Done');
            }

            $('.sbs-prog-step').each(function() {
                $(this)
                    .removeClass('active')
                    .addClass('completed clickable')
                    .removeAttr('aria-current')
                    .prop('disabled', false);
            });
            $('.sbs-prog-line').addClass('done');
        }

        _goToAccordionStep(target) {
            target = parseInt(target, 10);
            if (!target || target === this.currentStep) return;
            if (target < this.currentStep) {
                if (target <= 2) {
                    this.selectedDateTime = null;
                    $('#sbs-selected-start-at').val('');
                    $('.sbs-time-slot').removeClass('selected');
                }
                if (target <= 3 && this.currentStep > 3) {
                    this.selectedDateTime = null;
                    $('#sbs-selected-start-at').val('');
                }
            }
            // Staff/options loaders scroll once after their final content is
            // rendered. Do not move to a temporary anchor first.
            this.goToStep(target, target !== 2 && target !== 3);
            if (target === 2) this.loadServiceOptions();
            if (target === 3) this.loadAvailableStaff();
            if (target === 4) {
                const selDate = $('.sbs-day-button.selected').data('date');
                if (selDate) this.loadAvailableTimes(selDate);
            }
        }

        _updateMobileFlowHeader() {
            let $header = $('#sbs-mobile-flow-header');
            if (!$header.length) {
                $header = $('<div id="sbs-mobile-flow-header" class="sbs-mobile-flow-header" aria-live="polite"></div>');
                const $title = $('#sbs-main-title');
                if ($title.length) {
                    $title.after($header);
                } else {
                    $('.sbs-booking-main, .sbs-booking-form').first().prepend($header);
                }
            }
            const copy = this._stepCopy(this.currentStep);
            const selectedText = this._selectionSummaryLabel();
            const publicStep = this._publicStepMeta(this.currentStep);
            const progress = Math.max(0, Math.min(100, publicStep.progress));
            $header.html(`
                <div>
                    <span class="sbs-mobile-flow-step">Step ${publicStep.current} of ${publicStep.total}</span>
                    <strong>${this._esc(copy.title)}</strong>
                    <p>${this._esc(copy.hint)}</p>
                </div>
                ${selectedText ? `<span class="sbs-mobile-flow-count">${this._esc(selectedText)}</span>` : ''}
                <span class="sbs-mobile-flow-progress" aria-hidden="true"><i style="width:${progress}%"></i></span>
            `);
        }

        _optionAllowsDesignUpload(option) {
            if (!option) return false;
            if (option.allowDesignUpload) return true;
            const name = String(option.name || '').toLowerCase();
            return name.includes('nail art') || name.includes('custom design');
        }

        _parkDesignUploadCard() {
            const $card = $('.yatoon-design-upload-card');
            const $container = $('.sbs-options-container');
            if ($card.length && $container.length && $card.closest('.sbs-options-container').length) {
                $container.after($card.detach());
            }
        }

        _syncDesignUploadCard(focusWhenOpened = false, preferredOptionItem = null) {
            const selectedAllows = !!(this.selectedService && this.selectedService.allowDesignUpload);
            const selectedOptionAllows = (this.selectedOptions || []).some(option =>
                this._optionAllowsDesignUpload(option)
            );
            // The upload belongs only to the service currently being configured.
            // Previously, an earlier cart item kept this card active and caused a
            // later guest's photo to replace the first service's reference image.
            const allowed = selectedAllows || selectedOptionAllows;
            const $card = $('.yatoon-design-upload-card');
            const wasVisible = $card.hasClass('is-visible');
            $card.toggleClass('is-visible', allowed);
            if (!allowed) {
                this._parkDesignUploadCard();
                this.designUploadAttachmentId = 0;
                this.designUploadToken = '';
                this.designUploadImageUrl = '';
                this.designUploadFileName = '';
                const input = document.getElementById('sbs-design-reference');
                if (input) input.value = '';
                $('#sbs-design-preview').empty().hide();
                return;
            }

            if (this.designUploadAttachmentId && this.designUploadImageUrl && !$('#sbs-design-preview').children().length) {
                const restoredName = this.designUploadFileName || __('Reference photo');
                $('#sbs-design-preview').html(
                    '<img src="' + this._esc(this.designUploadImageUrl) + '" alt="">' +
                    '<span class="yatoon-design-file-name" title="' + this._esc(restoredName) + '">' + this._esc(restoredName) + '</span>' +
                    '<span class="yatoon-design-upload-status is-success" role="status" aria-live="polite">' + __('Uploaded') + '</span>'
                ).show();
            }

            const qualifyingIds = (this.selectedOptions || [])
                .filter(option => this._optionAllowsDesignUpload(option))
                .map(option => String(option.id));
            let $target = preferredOptionItem && preferredOptionItem.jquery
                ? preferredOptionItem
                : $();
            if (
                !$target.length
                || !$target.hasClass('selected')
                || !qualifyingIds.includes(String($target.find('.sbs-option-checkbox').val()))
            ) {
                $target = $('.sbs-option-item.selected').filter(function() {
                    return qualifyingIds.includes(String($(this).find('.sbs-option-checkbox').val()));
                }).last();
            }

            if ($target.length) {
                $target.after($card.detach());
                const targetId = String($target.find('.sbs-option-checkbox').val());
                const targetOption = (this.selectedOptions || []).find(option => String(option.id) === targetId);
                const targetName = String((targetOption && targetOption.name) || '')
                    .replace(/\s*\([^)]*\)\s*$/, '')
                    .trim();
                $card.find('.yatoon-design-upload-copy strong').text(
                    targetName ? __('Upload a reference photo for') + ' ' + targetName : __('Upload a reference photo')
                );
            } else {
                this._parkDesignUploadCard();
                $card.find('.yatoon-design-upload-copy strong').text(__('Upload a reference photo'));
            }

            if (focusWhenOpened && (!wasVisible || $target.length) && $card.length) {
                setTimeout(() => {
                    $card.get(0).scrollIntoView({ behavior: 'smooth', block: 'nearest' });
                }, 60);
            }
        }

        _blockWhileDesignUploadInProgress() {
            if (!this.designUploadInProgress) return false;
            this._showToast(__('Please wait for the reference photo to finish uploading.'), 'info');
            const $status = $('.yatoon-design-upload-status.is-uploading').first();
            if ($status.length) {
                $status.attr('tabindex', '-1').trigger('focus');
                const card = $status.closest('.yatoon-design-upload-card').get(0);
                if (card) card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
            }
            return true;
        }

        /**
         * Group a slot under Morning / Afternoon / Evening.
         *
         * FIX (4.4.3): this used to do
         *
         *     const d = new Date(startAt);   // startAt is a UTC "...Z" string
         *     hour = d.getHours();           // ...but getHours() is BROWSER local
         *
         * which labelled slots using the visitor's timezone rather than the
         * salon's. With a UTC+8 salon and a UTC-4 visitor the headings came out
         * 12 hours off -- an 11:30 AM slot appeared under "Evening" and the
         * whole afternoon appeared under "Morning". Confirmed on a live site.
         *
         * The slot's local time is already available and authoritative: the
         * server sends `time` as 24-hour "HH:MM" in SITE time, and `display`
         * as the same instant formatted for humans. We parse those instead and
         * never construct a Date, so the visitor's clock cannot influence the
         * grouping.
         *
         * @param {string} display e.g. "11:30 AM" (site local)
         * @param {string} startAt Unused; kept so existing call sites work.
         * @param {string} time    e.g. "11:30" 24h (site local), preferred.
         */
        _timeBucket(display, startAt, time) {
            let hour = null;

            // Preferred: the server's 24-hour local "HH:MM".
            const hhmm = String(time || '').match(/^(\d{1,2}):(\d{2})$/);
            if (hhmm) {
                hour = parseInt(hhmm[1], 10);
            }

            // Fallback: parse the localized display string.
            if (hour === null) {
                const match = String(display || '').match(/(\d{1,2})(?::\d{2})?\s*(AM|PM)?/i);
                if (match) {
                    hour = parseInt(match[1], 10);
                    const ap = (match[2] || '').toUpperCase();
                    if (ap === 'PM' && hour < 12) hour += 12;
                    if (ap === 'AM' && hour === 12) hour = 0;
                }
            }

            if (hour === null || isNaN(hour)) hour = 12;

            if (hour < 12) return __('Morning');
            if (hour < 17) return __('Afternoon');
            return __('Evening');
        }

        _showToast(message, type = 'info') {
            // 4.5.0: the toast element carries aria-live="polite", which is the
            // right urgency for a confirmation but too quiet for a failure --
            // "polite" waits for the reader to finish whatever it is saying,
            // and by then the toast has often already been dismissed. Errors
            // are mirrored into the assertive region so they interrupt.
            if (type === 'error') {
                this._announceError(message);
            }
            let $toast = $('#sbs-toast');
            if (!$toast.length) {
                $toast = $('<div id="sbs-toast" role="status" aria-live="polite"></div>');
                $('body').append($toast);
            }
            $toast
                .removeClass('show error success info')
                .addClass(type)
                .text(message || '');
            clearTimeout(this._toastTimer);
            setTimeout(function() { $toast.addClass('show'); }, 10);
            this._toastTimer = setTimeout(function() {
                $toast.removeClass('show');
            }, 4200);
        }

        _newBookingReference() {
            if (this._bookingAttemptReference) return this._bookingAttemptReference;
            const random = Math.random().toString(36).slice(2, 8).toUpperCase();
            this._bookingAttemptReference = `YB-${Date.now().toString(36).toUpperCase()}-${random}`;
            return this._bookingAttemptReference;
        }

        _clearBookingRecovery() {
            $('#sbs-booking-recovery').remove();
        }

        _showBookingRecovery(details = {}) {
            const self = this;
            const reference = String(details.reference || this._newBookingReference());
            const failedIndex = Math.max(0, parseInt(details.failedIndex || 0, 10));
            const confirmedCount = Math.max(0, parseInt(details.confirmedCount || 0, 10));
            const uncertain = !!details.uncertain;
            const safeToRetry = !!details.safeToRetry && confirmedCount === 0 && !uncertain;
            const message = String(details.message || __('The appointment was not confirmed.'));
            const title = uncertain
                ? __('Please check before trying again')
                : (confirmedCount > 0 ? __('Part of your request needs attention') : __('Your appointment was not confirmed'));
            const guidance = uncertain
                ? __('The connection ended before we could verify the result. Check your email or My Appointments first so you do not create a duplicate booking or payment.')
                : (confirmedCount > 0
                    ? __('Some appointments were confirmed. Do not submit the whole cart again. Review the failed appointment below.')
                    : __('Your details are still here. You can review this appointment and choose another staff member or time.'));

            this._clearBookingRecovery();
            const $panel = $('<section/>', {
                id: 'sbs-booking-recovery',
                class: 'sbs-booking-recovery',
                role: 'alert',
                'aria-live': 'assertive',
                tabindex: '-1'
            });
            $('<h3/>', { text: title }).appendTo($panel);
            $('<p/>', { class: 'sbs-booking-recovery-message', text: message }).appendTo($panel);
            $('<p/>', { class: 'sbs-booking-recovery-guidance', text: guidance }).appendTo($panel);
            const $reference = $('<p/>', { class: 'sbs-booking-recovery-reference' }).appendTo($panel);
            $('<span/>', { text: __('Support reference:') + ' ' }).appendTo($reference);
            $('<code/>', { text: reference }).appendTo($reference);
            const $actions = $('<div/>', { class: 'sbs-booking-recovery-actions' }).appendTo($panel);

            if (!uncertain && failedIndex < this.bookingCart.length) {
                $('<button/>', {
                    type: 'button',
                    class: 'sbs-booking-recovery-edit',
                    text: __('Review appointment')
                }).appendTo($actions).on('click', function() {
                    self._clearBookingRecovery();
                    self._bookingAttemptReference = '';
                    self.editCartItem(failedIndex);
                });
            }
            if (safeToRetry) {
                $('<button/>', {
                    type: 'button',
                    class: 'sbs-booking-recovery-retry',
                    text: __('Try again')
                }).appendTo($actions).on('click', function() {
                    self._clearBookingRecovery();
                    // Keep the same attempt reference. Server-side locking and
                    // provider idempotency make this a true retry instead of a
                    // second appointment/payment operation.
                    self.submitBooking();
                });
            }
            $('<button/>', {
                type: 'button',
                class: 'sbs-booking-recovery-copy',
                text: __('Copy reference')
            }).appendTo($actions).on('click', function() {
                if (navigator.clipboard && navigator.clipboard.writeText) {
                    navigator.clipboard.writeText(reference);
                }
                self._showToast(__('Support reference copied.'), 'success');
            });

            const $form = $('#sbs-customer-form');
            if ($form.length) $form.before($panel);
            else $('.sbs-step-5').prepend($panel);
            $panel.get(0).scrollIntoView({ behavior: 'smooth', block: 'center' });
            setTimeout(function() { $panel.trigger('focus'); }, 260);
        }

        _clearMainOptionRequired() {
            $('#sbs-main-option-required-message').remove();
            $('.sbs-option-section-main').removeClass('sbs-option-required-pulse');
            $('.sbs-option-section-main .sbs-option-item')
                .removeClass('sbs-option-required-outline')
                .css({ borderColor: '', boxShadow: '' });
        }

        _showMainOptionRequired() {
            const $section = $('.sbs-option-section-main').first();
            if (!$section.length) {
                this._showToast(__('Please choose one main option before continuing.'), 'error');
                return;
            }

            let $message = $('#sbs-main-option-required-message');
            if (!$message.length) {
                $message = $('<div id="sbs-main-option-required-message" role="alert" aria-live="assertive"></div>');
                $section.prepend($message);
            }

            $message
                .text(__('Please choose one main option before continuing. Add-ons are optional after that.'))
                .css({
                    background: '#fff7ed',
                    border: '1px solid #fb923c',
                    color: '#9a3412',
                    borderRadius: '8px',
                    fontWeight: '700',
                    margin: '0 0 12px',
                    padding: '10px 12px'
                });
            $section.addClass('sbs-option-required-pulse');
            $section.find('.sbs-option-item')
                .addClass('sbs-option-required-outline')
                .css({
                    borderColor: '#fb923c',
                    boxShadow: '0 0 0 2px rgba(251, 146, 60, 0.18)'
                });

            const top = Math.max(0, $section.offset().top - 100);
            $('html, body').stop(true).animate({ scrollTop: top }, 220);

            clearTimeout(this._mainOptionRequiredTimer);
            this._mainOptionRequiredTimer = setTimeout(() => {
                $('.sbs-option-section-main').removeClass('sbs-option-required-pulse');
            }, 1800);
        }

        _bindViewportAccessibility() {
            const viewport = window.visualViewport;
            const updateViewport = function() {
                const visibleHeight = viewport ? viewport.height : window.innerHeight;
                const keyboardOpen = !!viewport && (window.innerHeight - visibleHeight) > 140;
                document.documentElement.style.setProperty('--yatoon-visible-viewport-height', `${Math.round(visibleHeight)}px`);
                document.body.classList.toggle('yatoon-booking-keyboard-open', keyboardOpen);
            };
            updateViewport();
            if (viewport) {
                viewport.addEventListener('resize', updateViewport, { passive: true });
                viewport.addEventListener('scroll', updateViewport, { passive: true });
            }
            window.addEventListener('orientationchange', updateViewport, { passive: true });
            $(document).on('focusin.yatoonViewport', '#sbs-booking-form input, #sbs-booking-form select, #sbs-booking-form textarea', function() {
                const field = this;
                setTimeout(function() {
                    const rect = field.getBoundingClientRect();
                    const bottom = viewport ? viewport.height : window.innerHeight;
                    if (rect.bottom > bottom - 20 || rect.top < 12) {
                        field.scrollIntoView({ behavior: 'smooth', block: 'center' });
                    }
                }, 180);
            });
        }

        init() {
            if (!yatoonData || yatoonData.immersiveMode !== false) {
                $('body').addClass('sbs-booking-immersive');
            }
            this.bindEvents();
            this._bindSummaryEditButtons();
            this.initWeekView();
            this.initMobileCart();
            this._updateMobileCartBar();
            this._bindFormPolish();
            this._bindViewportAccessibility();
            this._updateMobileFlowHeader();
            this._syncDesignUploadCard();

            // Intercept browser Back/Forward so it navigates within the widget
            const self = this;
            history.replaceState({ sbsStep: 1 }, '', window.location.href);
            window.addEventListener('popstate', function(e) {
                // Only handle if we're on a page with the booking widget
                if (!$('#sbs-booking-form').length) return;
                if (e.state && e.state.sbsStep) {
                    // Browser went back to a recorded step - simulate our Back button
                    self._handleBrowserBack();
                } else {
                    // No sbs state - push a new entry to stay on this page
                    history.pushState({ sbsStep: self.currentStep }, '', window.location.href);
                }
                // Always prevent leaving the page while widget is active
                return false;
            });

            // Start cart bar hidden - will slide up when a service is selected
            $('#sbs-mobile-cart-bar').removeClass('sbs-bar-visible').css('display', 'none');
            // Text Us starts at bottom:24px (CSS default), no inline override needed

            // Restore previous session state if available
            if (this._consumeRebookRequest() || this._restoreSession()) {
                const self = this;
                // Re-populate hidden fields
                if (self.selectedService) {
                    $('#sbs-selected-service-id').val(self.selectedService.id);
                    // Mark service as selected in DOM (step 1 list)
                    $(`.sbs-service-item[data-service-id="${self.selectedService.id}"]`).addClass('selected');
                    // Hide empty guide since we have a service
                    $('#sbs-sidebar-empty-guide').hide();
                }
                if (self.selectedStaff) {
                    $('#sbs-selected-staff-id').val(self.selectedStaff.id);
                }
                if (self.selectedOptions.length) {
                    $('#sbs-selected-options').val(JSON.stringify(self.selectedOptions.map(o => o.id)));
                }
                self.updateSummary();
                self._updateMobileCartBar();

                // Navigate to the correct step
                if (self.currentStep === 2) {
                    self.loadServiceOptions();
                } else if (self.currentStep === 3) {
                    self.goToStep(3);
                    self.loadAvailableStaff();
                } else if (self.currentStep === 4) {
                    self.goToStep(4);
                } else if (self.currentStep === 5) {
                    // bookingCart is persisted separately from the in-progress
                    // selection. Rebuild the main review cards before rendering
                    // the checkout totals after a page refresh.
                    self.renderCart();
                    self.goToStep(5);
                    self.updateSummaryForCheckout();
                } else {
                    self.goToStep(self.currentStep);
                }
            } else {
                this._applyInstantBookingParams();
            }
        }

        _bindFormPolish() {
            $(document).on('focus blur input', '.sbs-form-group input, .sbs-form-group textarea', function(e) {
                const $field = $(this);
                const $group = $field.closest('.sbs-form-group');
                $group.toggleClass('is-focused', e.type === 'focus');
                $group.toggleClass('has-value', !!String($field.val() || '').trim());
            });
        }

        _applyInstantBookingParams() {
            const params = new URLSearchParams(window.location.search || '');
            const serviceId = params.get('yatoon_service') || params.get('service_id') || params.get('sbs_service');
            const category = params.get('yatoon_category') || params.get('category') || params.get('sbs_category');
            const staffId = params.get('yatoon_staff') || params.get('staff_id') || params.get('sbs_staff');

            if (category) {
                const cat = String(category).trim().toLowerCase();
                const $tab = $('.sbs-cat-tab, .sbs-category-tab').filter(function() {
                    const dataCat = String($(this).data('cat') || $(this).data('category') || '').toLowerCase();
                    const text = String($(this).text() || '').trim().toLowerCase().replace(/\s+/g, '-');
                    return dataCat === cat || text === cat || text === cat.replace(/\s+/g, '-');
                }).first();
                if ($tab.length) {
                    setTimeout(function() { $tab.trigger('click'); }, 120);
                }
            }

            if (!serviceId) return;
            const $service = $('.sbs-service-item').filter(function() {
                return String($(this).data('service-id')) === String(serviceId);
            }).first();
            if (!$service.length) return;

            if (staffId) {
                this._instantStaffId = String(staffId);
            }

            setTimeout(() => {
                $service.addClass('yatoon-instant-target');
                $service.trigger('click');
            }, 240);
        }
        
        getFirstName(fullName) {
            if (!fullName) return '';
            if (fullName === 'Any staff') return __('Any Staff Available');
            return fullName.trim().split(/\s+/)[0];
        }

        _staffDisplayName(name) {
            return name === 'Any staff' ? __('Any Staff Available') : (name || __('Any Staff Available'));
        }

        _peopleCount(services) {
            const labels = [];
            (services || []).forEach((s) => {
                const label = s && s.guestLabel ? s.guestLabel : 'Me';
                if (!labels.includes(label)) labels.push(label);
            });
            return labels.length || 1;
        }

        _selectionSummaryLabel() {
            let services = [];
            if (this.bookingCart && this.bookingCart.length) {
                this.bookingCart.forEach((item) => {
                    if (item.services && item.services.length) {
                        services = services.concat(item.services);
                    } else if (item.service) {
                        services.push({ service: item.service, guestLabel: item.guestLabel || 'Me' });
                    }
                });
            } else if (this.pendingServices && this.pendingServices.length) {
                services = this.pendingServices.slice();
            } else if (this.selectedService) {
                services = [{ service: this.selectedService, guestLabel: this._nextGuestLabel || 'Me' }];
            }

            const serviceCount = services.length;
            if (!serviceCount) return '';
            const peopleCount = this._peopleCount(services);
            const svcText = serviceCount + (serviceCount === 1 ? ' service' : ' services');
            return peopleCount > 1 ? (svcText + ' for ' + peopleCount + ' people') : svcText;
        }

        _getNextGuestLabel(existingLabels) {
            const labels = existingLabels || [];
            const guestLabels = labels.filter(label => label && label !== 'Me');
            if (!guestLabels.length) return __('Guest 1');
            let maxGuestNum = 0;
            guestLabels.forEach(label => {
                const match = String(label).match(/^Guest(?:\s+(\d+))?$/i);
                if (match) maxGuestNum = Math.max(maxGuestNum, parseInt(match[1] || '1', 10));
            });
            let nextNum = Math.max(maxGuestNum + 1, guestLabels.length + 1);
            while (labels.indexOf('Guest ' + nextNum) !== -1) nextNum++;
            return 'Guest ' + nextNum;
        }

        _displayGuestLabel(label) {
            return label === 'Guest' ? __('Guest 1') : (label || 'Me');
        }

        _maxGuestsTogether() {
            const raw = yatoonData && yatoonData.maxGuestsTogether;
            const parsed = parseInt(raw, 10);
            if (isNaN(parsed)) return 2;
            return Math.max(0, parsed);
        }

        _isGroupAppointmentSelected() {
            const $selected = $('.sbs-appt-type-card.active').first();
            const type = String($selected.data('type') || '').toLowerCase();
            // The appointment-type cards are hidden while choosing services and
            // can be re-rendered/restored without their active class. The stored
            // intent is the authoritative state throughout the multi-step flow.
            return !!window._sbsGroupIntent || type === 'group';
        }

        _isGroupAppointmentFlow() {
            if (this._isGroupAppointmentSelected()) return true;
            return (this.pendingServices || []).some(function(service) {
                return service.groupAppointment === true || (service.guestLabel && service.guestLabel !== 'Me');
            });
        }

        _guestLabelsInProgress() {
            const seen = [];
            (this.pendingServices || []).forEach((service) => {
                const label = service.guestLabel || 'Me';
                if (label !== 'Me' && seen.indexOf(label) === -1) {
                    seen.push(label);
                }
            });
            return seen;
        }

        _addGuestTogether() {
            const maxGuests = this._maxGuestsTogether();
            if (maxGuests <= 0) {
                this._showToast(__('Guest booking is not available for this visit.'), 'error');
                return false;
            }

            const existingGuestLabels = this._guestLabelsInProgress();
            if (existingGuestLabels.length >= maxGuests) {
                const guestWord = maxGuests === 1 ? 'guest' : 'guests';
                this._showToast(__('You can add up to') + ' ' + maxGuests + ' ' + guestWord + ' ' + __('with this booking.'), 'error');
                return false;
            }

            const existingLabels = (this.pendingServices || []).map((service) => service.guestLabel || 'Me');
            const newLabel = this._getNextGuestLabel(existingLabels);
            this._guestCount = Math.max(this._guestCount || 0, existingGuestLabels.length) + 1;
            this._promptGuestName(newLabel);
            return true;
        }

        _normalizeGuestName(value) {
            return String(value || '').replace(/\s+/g, ' ').trim().slice(0, 40);
        }

        _promptGuestName(defaultLabel) {
            const self = this;
            const fallbackLabel = defaultLabel || this._getNextGuestLabel((this.pendingServices || []).map(s => s.guestLabel || 'Me'));
            $('#sbs-guest-name-overlay').remove();

            const safeLabel = this._esc(this._displayGuestLabel(fallbackLabel));
            const overlay = $(`<div id="sbs-guest-name-overlay" style="
                position:fixed;inset:0;background:rgba(17,24,39,.32);z-index:9995;
                display:flex;align-items:center;justify-content:center;padding:18px;">
                <div style="background:#fff;border-radius:16px;width:100%;max-width:420px;box-shadow:0 18px 50px rgba(15,23,42,.18);padding:20px;">
                    <h3 style="margin:0 0 6px;font-size:20px;line-height:1.2;color:#111827;font-weight:800;">${safeLabel} name</h3>
                    <p style="margin:0 0 14px;font-size:13px;line-height:1.45;color:#6b7280;">Name is optional. You can keep ${safeLabel}.</p>
                    <input type="text" id="sbs-guest-name-input" maxlength="40" autocomplete="off" placeholder="Name (optional)"
                        style="width:100%;box-sizing:border-box;border:1px solid #d1d5db;border-radius:10px;padding:12px 13px;font-size:15px;color:#111827;outline:none;">
                    <div style="display:flex;gap:10px;justify-content:flex-end;margin-top:16px;flex-wrap:wrap;">
                        <button type="button" id="sbs-guest-use-default"
                            style="background:#fff;color:#374151;border:1px solid #d1d5db;border-radius:999px;padding:11px 16px;font-size:13px;font-weight:700;cursor:pointer;">
                            Use ${safeLabel}
                        </button>
                        <button type="button" id="sbs-guest-save-name"
                            style="background:#43a047;color:#fff;border:none;border-radius:999px;padding:11px 18px;font-size:13px;font-weight:800;cursor:pointer;">
                            Save &amp; Continue
                        </button>
                    </div>
                </div>
            </div>`);

            const continueWith = function(label) {
                const normalized = self._normalizeGuestName(label) || fallbackLabel;
                if (normalized.toLowerCase() === 'me') {
                    self._showToast(__('Please use a guest name instead of Me.'), 'error');
                    return;
                }
                const existing = self._guestLabelsInProgress().map(existingLabel => existingLabel.toLowerCase());
                if (normalized !== fallbackLabel && existing.indexOf(normalized.toLowerCase()) !== -1) {
                    self._showToast(__('That guest is already added. Add a service to that guest or use a different name.'), 'error');
                    return;
                }
                overlay.remove();
                self._enterServiceSelection(normalized, true);
            };

            $('body').append(overlay);
            const $input = overlay.find('#sbs-guest-name-input');
            setTimeout(() => $input.trigger('focus'), 40);
            overlay.on('click', '#sbs-guest-use-default', function() {
                continueWith(fallbackLabel);
            });
            overlay.on('click', '#sbs-guest-save-name', function() {
                continueWith($input.val());
            });
            overlay.on('keydown', '#sbs-guest-name-input', function(e) {
                if (e.key === 'Enter') {
                    e.preventDefault();
                    continueWith($input.val());
                } else if (e.key === 'Escape') {
                    e.preventDefault();
                    overlay.remove();
                    self._showGuestReviewScreen();
                }
            });
            overlay.on('click', function(e) {
                if (e.target === overlay[0]) $input.trigger('focus');
            });
        }

        _serviceDuration(s) {
            if (!s) return 0;
            const opts = s.options || [];
            const optsDur = opts.reduce((sum, o) => sum + (parseInt(o.duration) || 0), 0);
            const baseDur = parseInt(s.service && s.service.duration) || 0;
            return Math.max(1, optsDur > 0 ? optsDur : baseDur);
        }

        _durationByGuest(services) {
            const groups = {};
            (services || []).forEach((s) => {
                const label = s.guestLabel || 'Me';
                groups[label] = (groups[label] || 0) + this._serviceDuration(s);
            });
            const totals = Object.values(groups);
            return {
                groups,
                totalDuration: totals.length ? Math.max.apply(null, totals) : 0
            };
        }

        _getItemVisitDuration(item) {
            if (item && item.services && item.services.length) {
                return this._durationByGuest(item.services).totalDuration;
            }
            return this.getItemDuration(item);
        }

        _formatItemTimeRange(item, dateStr) {
            const visitDuration = this._getItemVisitDuration(item);
            const startLabel = (item && item.dateTime && item.dateTime.display) ? String(item.dateTime.display).trim() : '';
            const startAt = item && item.dateTime ? item.dateTime.start_at : null;
            if (!startLabel || !visitDuration) {
                return dateStr && startLabel ? `${dateStr} at ${startLabel}` : (dateStr || startLabel || '');
            }
            const startDate = startAt ? new Date(startAt) : null;
            if (startDate && !isNaN(startDate.getTime())) {
                const endDate = new Date(startDate.getTime() + visitDuration * 60000);
                // FIX (4.4.3): startLabel comes from the server already in salon
                // time, but endLabel was formatted with the visitor's clock, so
                // the pair printed as "3:00 PM - 3:30 AM" whenever the visitor
                // was in a different timezone from the salon. Both ends must
                // use the same reference.
                //
                // 4.6.2: _siteFmtTime now follows the visitor's locale instead of
                // being pinned to en-US, so pairing the server's ASCII label with
                // a locally formatted end would read "3:00 PM - 15:30". Format
                // BOTH ends through _siteFmtTime (same timezone, same locale) and
                // fall back to the server label only if that yields nothing.
                const endLabel   = this._siteFmtTime(endDate);
                const startPretty = this._siteFmtTime(startDate) || startLabel;
                return `${dateStr} &bull; ${startPretty} &ndash; ${endLabel}`;
            }
            return `${dateStr} &bull; ${startLabel}`;
        }

        _renderSuccessBookingSummary(items) {
            const $summary = $('#sbs-success-booking-summary');
            if (!$summary.length) return;
            const bookings = Array.isArray(items) ? items : [];
            if (!bookings.length) {
                $summary.empty().hide();
                return;
            }

            const allGuestLabels = [];
            bookings.forEach(item => {
                (item.services || []).forEach(entry => {
                    const label = entry.guestLabel || item.guestLabel || 'Me';
                    if (!allGuestLabels.includes(label)) allGuestLabels.push(label);
                });
            });
            const showGuestLabels = allGuestLabels.length > 1 || allGuestLabels.some(label => label !== 'Me');
            const checkoutTip = Math.max(0, parseFloat($('#sbs-tip-amount').val()) || 0);
            let totalPrice = 0;
            let html = `<h4>${bookings.length === 1 ? __('Appointment details') : __('Booking details')}</h4>`;

            bookings.forEach((item, bookingIndex) => {
                const services = item.services || [{ service: item.service, options: item.options || [], staff: item.staff, guestLabel: item.guestLabel || 'Me' }];
                const offsets = this._serviceOffsetsByGuest(services);
                const startAt = item.dateTime && item.dateTime.start_at ? new Date(item.dateTime.start_at) : null;
                const dateParts = item.dateTime && item.dateTime.date ? String(item.dateTime.date).split('-') : [];
                const dateObj = dateParts.length === 3
                    ? new Date(parseInt(dateParts[0], 10), parseInt(dateParts[1], 10) - 1, parseInt(dateParts[2], 10))
                    : startAt;
                const dateLabel = dateObj && !isNaN(dateObj.getTime())
                    ? dateObj.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })
                    : '';
                const bookingPrice = this.getItemPrice(item);
                const bookingDuration = this._getItemVisitDuration(item);
                totalPrice += bookingPrice;

                const serviceRows = services.map((entry, serviceIndex) => {
                    const duration = this._serviceDuration(entry);
                    const serviceStart = startAt && !isNaN(startAt.getTime())
                        ? new Date(startAt.getTime() + (offsets[serviceIndex] || 0) * 60000)
                        : null;
                    const serviceEnd = serviceStart ? new Date(serviceStart.getTime() + duration * 60000) : null;
                    const timeLabel = serviceStart && serviceEnd
                        ? this._siteFmtTime(serviceStart) + ' \u2013 ' + this._siteFmtTime(serviceEnd)
                        : '';
                    const guestLabel = entry.guestLabel || item.guestLabel || 'Me';
                    const staffName = this._staffDisplayName(entry.staff && entry.staff.name);
                    const servicePrice = this._optionsPrice(entry.service, entry.options || []);
                    return `
                        <div class="sbs-success-service-row">
                            <div class="sbs-success-service-copy">
                                ${showGuestLabels ? `<span class="sbs-success-guest-label">${this._esc(this._displayGuestLabel(guestLabel))}</span>` : ''}
                                <strong>${this._svcDisplayLabelHtml(entry.service, entry.options || [])}</strong>
                                <span>${this._esc(staffName)}${timeLabel ? ' &bull; ' + this._esc(timeLabel) : ''}</span>
                            </div>
                            <span class="sbs-success-service-price">${ysFormatMoney(servicePrice)}</span>
                        </div>`;
                }).join('');

                // 4.4.3: the confirmation card jumped straight from the service
                // price to the grand total, so with tax enabled the customer saw
                // an unexplained difference ($25.00 -> $27.50). Show the tax line
                // here too. `row` cannot be reused: it carries element IDs that
                // are already used by the sidebar summary on the same page.
                const taxParts = this._taxParts(bookingPrice);
                const bookingTip = bookingIndex === 0 ? checkoutTip : 0;
                html += `
                    <section class="sbs-success-booking-card">
                        ${bookings.length > 1 ? `<div class="sbs-success-booking-number">Appointment ${bookingIndex + 1}</div>` : ''}
                        ${dateLabel ? `<div class="sbs-success-date">${this._esc(dateLabel)}</div>` : ''}
                        ${serviceRows}
                        ${taxParts.hasTax ? `
                        <div class="sbs-success-booking-tax">
                            <span>${taxParts.label}</span>
                            <span>${ysFormatMoney(taxParts.amount)}</span>
                        </div>` : ''}
                        ${bookingTip > 0 ? `
                        <div class="sbs-success-booking-tax">
                            <span>${__('Tip')}</span>
                            <span>${ysFormatMoney(bookingTip)}</span>
                        </div>` : ''}
                        <div class="sbs-success-booking-total">
                            <span>${bookingDuration} min</span>
                            <strong>${ysFormatMoney(taxParts.total + bookingTip)}</strong>
                        </div>
                    </section>`;
            });

            if (bookings.length > 1) {
                const grandParts = this._taxParts(totalPrice);
                if (grandParts.hasTax) {
                    html += `<div class="sbs-success-booking-tax"><span>${grandParts.label}</span><span>${ysFormatMoney(grandParts.amount)}</span></div>`;
                }
                if (checkoutTip > 0) {
                    html += `<div class="sbs-success-booking-tax"><span>${__('Tip')}</span><span>${ysFormatMoney(checkoutTip)}</span></div>`;
                }
                html += `<div class="sbs-success-grand-total"><span>${__('Total')}</span><strong>${ysFormatMoney(grandParts.total + checkoutTip)}</strong></div>`;
            }
            $summary.html(html).show();
        }

        _serviceOffsetsByGuest(services) {
            const totals = {};
            return (services || []).map((s) => {
                const label = s.guestLabel || 'Me';
                const offset = totals[label] || 0;
                totals[label] = offset + this._serviceDuration(s);
                return offset;
            });
        }

        // -- Time-aware staff conflict detection --------------------------------
        // All guests in a booking share the SAME start time (they run in parallel),
        // so a guest's Nth service overlaps in real time with whatever "Me" (or any
        // other guest) has scheduled during that same window - NOT with services
        // that simply come earlier in the pendingServices array.
        //
        // Returns the set of staff IDs (as strings) already booked (via
        // pendingServices) during the real-time window [myStart, myEnd), where
        // myStart/myEnd are minutes-from-appointment-start.
        //
        // excludeEntry: pass the pendingServices entry to skip (e.g. when editing
        // a service already in the list, so it doesn't conflict with itself).
        _computeBusyStaffIds(myStart, myEnd, excludeEntry) {
            const self = this;
            const busyIds = new Set();
            const offsets = self._serviceOffsetsByGuest(self.pendingServices);
            self.pendingServices.forEach(function(ps, i) {
                if (excludeEntry && ps === excludeEntry) return; // skip self
                if (!ps.staff || !ps.staff.id || Number(ps.staff.id) === 0) return; // "Any staff" doesn't block anyone specific
                const start = offsets[i];
                const end   = start + self._serviceDuration(ps);
                if (start < myEnd && end > myStart) {
                    busyIds.add(String(ps.staff.id));
                }
            });
            return busyIds;
        }

        // Start offset (minutes from appointment start) for a NEW service about to be
        // added for guestLabel, given what's already in pendingServices for that guest.
        _nextOffsetForGuest(guestLabel) {
            const label = guestLabel || 'Me';
            return (this.pendingServices || [])
                .filter(function(ps) { return (ps.guestLabel || 'Me') === label; })
                .reduce((sum, ps) => sum + this._serviceDuration(ps), 0);
        }

        _splitCartItemByGuest(item) {
            const services = item && item.services
                ? item.services
                : [{ service: item.service, options: item.options || [], staff: item.staff }];
            const groups = {};
            services.forEach((s, originalIndex) => {
                const label = s.guestLabel || 'Me';
                if (!groups[label]) groups[label] = [];
                groups[label].push({ ...s, _originalIndex: originalIndex });
            });
            return Object.keys(groups).map((label) => {
                const grouped = groups[label];
                return {
                    ...item,
                    services: grouped,
                    service: grouped[0].service,
                    options: grouped[0].options || [],
                    staff: grouped[0].staff,
                    guestLabel: label
                };
            });
        }

        _guestLabelsInCart() {
            const labels = [];
            (this.bookingCart || []).forEach((item) => {
                const services = item.services || [{ service: item.service, options: item.options || [], staff: item.staff }];
                services.forEach((s) => {
                    const label = s.guestLabel || 'Me';
                    if (label !== 'Me' && !labels.includes(label)) labels.push(label);
                });
            });
            return labels;
        }

        _renderGuestContactFields() {
            const $wrap = $('#sbs-guest-contact-fields');
            if (!$wrap.length) return;
            $wrap.empty().hide();
        }

        _collectGuestContacts() {
            const contacts = {};
            $('.sbs-guest-contact-card').each(function() {
                const label = $(this).data('guest-label') || '';
                if (!label) return;
                const name = String($(this).find('.sbs-guest-name').val() || '').trim();
                const phone = String($(this).find('.sbs-guest-phone').val() || '').trim();
                if (name || phone) {
                    contacts[label] = { name, phone };
                }
            });
            return contacts;
        }

        // Filter out option names that simply repeat the service name or category name
        // e.g. "Custom Design / Nail Art + Custom Design / Nail Art" ->just show service name
        _filterOptNames(options, service) {
            if (!options || options.length === 0) return [];
            const svcName  = (service && service.name     || '').trim().toLowerCase();
            const catName  = (service && service.category || '').trim().toLowerCase();
            const fullName = catName ? (catName + ' \u2014 ' + svcName) : svcName;
            return options.filter(o => {
                 if (o.is_addon) return true; // always keep add-ons
                const n = (o.name || '').trim().toLowerCase();
                return n !== svcName && n !== catName && n !== fullName;
            });
        }

        _optLabel(options, service) {
            const filtered = this._filterOptNames(options, service);
            return filtered.length > 0 ? filtered.map(o => o.name).join(', ') : '';
        }

        _cleanServiceLabelText(label) {
            return (label || '').replace(/^\s*with\s+/i, '').trim();
        }

        _splitServiceDisplayNote(label) {
            const text = (label || '').trim();
            const match = text.match(/\s*\((price\s+varies[^)]*)\)\s*$/i);
            if (!match) return { name: text, note: '' };
            return {
                name: text.slice(0, match.index).trim(),
                note: match[1].trim()
            };
        }

        _svcDisplayLabelHtml(service, options) {
            const parts = this._splitServiceDisplayNote(this._svcDisplayLabel(service, options));
            const note = parts.note
                ? ` <span class="sbs-service-note">${this._esc(parts.note)}</span>`
                : '';
            return `${this._esc(parts.name)}${note}`;
        }

        _cleanOptionLabelText(label, serviceName) {
            let text = (label || '').trim();
            const svc = (serviceName || '').trim();
            if (svc && text.toLowerCase().indexOf(svc.toLowerCase()) === 0) {
                text = text.slice(svc.length).replace(/^[\s\-\u2013\u2014:]+/, '').trim();
            }
            return text;
        }

        // Returns a concise display label for a service+options combo.
        // Priority: include the parent service before a meaningful main option.
        //   - Has meaningful options ->show "Service - Option", e.g. "Regular Pedicure - Regular Polish/No Polish"
        //   - No options (or options just repeat service name) ->show service name only, e.g. "Acrylic Gel"
        // The category prefix (e.g. "Nail Enhancements - ") is intentionally omitted
        // to keep the display short and clean.
        _svcDisplayLabel(service, options) {
            if (!service) return '';
            const svcName = (service.name || '').trim();
            const opts    = options || [];
            if (opts.length === 0) return this._cleanServiceLabelText(svcName);

            // Separate main (single-select) from add-ons
            const mainOpts  = opts.filter(o => !o.is_addon);
            const addonOpts = opts.filter(o => o.is_addon);

            // Base: always keep the parent service visible; append the concrete option when useful.
            const filteredMain = this._filterOptNames(mainOpts, service);
            const mainName = filteredMain
                .map(o => this._cleanOptionLabelText(o.name, svcName))
                .filter(Boolean)
                .join(', ');
            let baseName = mainName
                ? (svcName + ' - ' + mainName)
                : svcName;

            // Collect all extra names to append: add-ons + any non-addon opts with different names
            // This catches cases where group_name mismatch causes add-ons to appear as mainOpts
            const extraNames = [];
            // Add-ons always appended
            addonOpts.forEach(o => {
                const n = this._cleanOptionLabelText(o.name, svcName);
                if (n && !extraNames.includes(n)) extraNames.push(n);
            });
            // Also append main opts whose names differ from service AND from baseName
            mainOpts.forEach(o => {
                const n = this._cleanOptionLabelText(o.name, svcName);
                if (n.toLowerCase() !== svcName.toLowerCase() &&
                    mainName.toLowerCase().indexOf(n.toLowerCase()) === -1 &&
                    baseName.toLowerCase().indexOf(n.toLowerCase()) === -1 &&
                    !extraNames.includes(n)) {
                    extraNames.push(n);
                }
            });

            if (extraNames.length > 0) {
                return this._cleanServiceLabelText(baseName + ', ' + extraNames.join(', '));
            }
            return this._cleanServiceLabelText(baseName);
        }

        // Returns the correct confirm button label based on cart size (single vs plural)
        // and applies the matching CSS class for styling.
        _applyConfirmBtn() {
            const isMulti = this.bookingCart.length > 1;
            const label = isMulti
                ? ((yatoonData.btnLabels && yatoonData.btnLabels.confirm) || __('Confirm All Bookings'))
                : ((yatoonData.btnLabels && yatoonData.btnLabels.confirm_single) || __('Confirm Booking'));
            const $btn = $('.sbs-btn-submit');
            $btn.text(label);
            if (isMulti) {
                $btn.removeClass('sbs-btn-single');
            } else {
                $btn.addClass('sbs-btn-single');
            }
        }

        _requestCustomerFormSubmit() {
            const form = $('#sbs-customer-form').get(0);
            if (!form) {
                this.submitBooking();
                return;
            }

            if (typeof form.requestSubmit === 'function') {
                form.requestSubmit();
                return;
            }

            if (typeof form.checkValidity === 'function' && !form.checkValidity()) {
                if (typeof form.reportValidity === 'function') {
                    form.reportValidity();
                }
                return;
            }

            this.submitBooking();
        }
        
        _getCurrentPrice() {
            return this._optionsPrice(this.selectedService, this.selectedOptions);
        }

        _getCurrentDuration() {
            if (this.selectedOptions.length > 0) {
                const mainOpts  = this.selectedOptions.filter(o => !o.is_addon);
                const addonOpts = this.selectedOptions.filter(o => o.is_addon);
                const mainDur   = mainOpts.length > 0
                    ? Math.max(...mainOpts.map(o => o.duration || 0))
                    : (this.selectedService ? (this.selectedService.duration || 0) : 0);
                const addonDur  = addonOpts.reduce((s, o) => s + (o.duration || 0), 0);
                return mainDur + addonDur;
            }
            return this.selectedService ? (this.selectedService.duration || 0) : 0;
        }
        
        getItemPrice(item) {
            // Multi-service appointment: sum all services
            const services = item.services || [{ service: item.service, options: item.options }];
            return services.reduce((total, s) => total + this._optionsPrice(s.service, s.options), 0);
        }

        // Price for one service + its selected options: base price, adjusted by the
        // selected Main option's own modifier (if any), plus every selected Add-on's
        // modifier stacked on top. Add-ons must be summed (not maxed) so a genuinely
        // priced add-on (e.g. French Tips) actually adds to the total instead of being
        // silently dropped whenever a cheaper/free add-on is also selected.
        _optionsPrice(service, options) {
            const opts = options || [];
            const base = (service && service.price) || 0;
            if (opts.length === 0) return base;
            const mainOpt   = opts.find(o => !o.is_addon);
            const addonOpts = opts.filter(o => o.is_addon);
            const hasModifiers = opts.some(o => typeof o.price_modifier === 'number' && !isNaN(o.price_modifier));
            if (!hasModifiers) {
                // Fallback for any option objects that predate the price_modifier field
                // (shouldn't normally happen, but avoids a hard crash).
                return Math.max(...opts.map(o => o.price || 0));
            }
            const mainPrice   = mainOpt ? (base + (mainOpt.price_modifier || 0)) : base;
            const addonExtra  = addonOpts.reduce((sum, o) => sum + (o.price_modifier || 0), 0);
            return mainPrice + addonExtra;
        }

        getItemDuration(item) {
            // Multi-service appointment: sum all durations
            const services = item.services || [{ service: item.service, options: item.options || [] }];
            return services.reduce((total, s) => {
                const opts = s.options || [];
                if (opts.length === 0) return total + (parseInt(s.service && s.service.duration) || 0);
                const mainOpts  = opts.filter(o => !o.is_addon);
                const addonOpts = opts.filter(o => o.is_addon);
                const mainDur   = mainOpts.length > 0
                    ? Math.max(...mainOpts.map(o => parseInt(o.duration) || 0))
                    : (parseInt(s.service && s.service.duration) || 0);
                const addonDur  = addonOpts.reduce((d, o) => d + (parseInt(o.duration) || 0), 0);
                return total + mainDur + addonDur;
            }, 0);
        }

        // Prompt user: add more services or proceed to time selection
        _showAddMorePrompt() {
            const self = this;
            $('#sbs-add-more-overlay').remove();

            // Work out who was just added (last pendingService) and next guest label
            const lastService = self.pendingServices[self.pendingServices.length - 1];
            const currentPersonLabel = (lastService && lastService.guestLabel) ? lastService.guestLabel : 'Me';
            const hasMultiplePeople = self.pendingServices.some(s => s.guestLabel && s.guestLabel !== 'Me');
            const nextGuestLabel = self._getNextGuestLabel(self.pendingServices.map(s => s.guestLabel || 'Me'));

            // Badge color for currentPersonLabel
            const isMe = currentPersonLabel === 'Me';
            const badgeBg    = isMe ? '#dbeafe' : '#dcfce7';
            const badgeColor = isMe ? '#1d4ed8' : '#16a34a';
            const personBadge = `<span style="display:inline-block;background:${badgeBg};color:${badgeColor};border-radius:4px;font-size:13px;font-weight:700;padding:2px 8px;margin-left:6px;">${self._esc(self._displayGuestLabel(currentPersonLabel))}</span>`;

            const overlay = $(`
                <div id="sbs-add-more-overlay" style="
                    position:fixed;inset:0;background:rgba(0,0,0,0.5);
                    z-index:9999;display:flex;align-items:center;justify-content:center;
                    padding:20px;box-sizing:border-box;">
                    <div style="
                        background:#fff;border-radius:20px;padding:28px 24px;
                        max-width:380px;width:100%;box-shadow:0 12px 40px rgba(0,0,0,0.2);">
                        <h3 style="margin:0 0 6px;font-size:19px;font-weight:700;color:#1a1a1a;text-align:center;">What would you like to do?</h3>
                        <p style="margin:0 0 18px;font-size:14px;color:#6b7280;text-align:center;">Add more to this appointment or select a time.</p>
                        <div style="display:flex;flex-direction:column;gap:10px;">
                            <button type="button" id="sbs-add-more-yes" style="
                                padding:12px 16px;border:2px solid var(--sbs-primary,#ec4899);
                                background:#fff;color:var(--sbs-primary,#ec4899);
                                border-radius:12px;font-size:15px;font-weight:600;cursor:pointer;text-align:left;display:flex;align-items:center;flex-wrap:wrap;gap:4px;">
                                <span>&#10133; Add Another Service for</span>${personBadge}
                            </button>
                            <button type="button" id="sbs-add-more-guest" style="
                                padding:12px 16px;border:2px solid #16a34a;
                                background:#f0fdf4;color:#16a34a;
                                border-radius:12px;font-size:15px;font-weight:600;cursor:pointer;text-align:left;">
                                &#128101; Add Guest Together
                            </button>
                            <button type="button" id="sbs-add-more-no" style="
                                padding:12px 16px;border:none;
                                background:#111;color:#fff;
                                border-radius:12px;font-size:15px;font-weight:600;cursor:pointer;text-align:center;">
                                Select Time &#8594;
                            </button>
                        </div>
                    </div>
                </div>
            `);

            $('body').append(overlay);

            $('#sbs-add-more-yes').on('click', function() {
                $('#sbs-add-more-overlay').remove();
                // Set _nextGuestLabel so Step 1 knows we're adding for the same person
                self._enterServiceSelection(currentPersonLabel, true);
            });

            $('#sbs-add-more-guest').on('click', function(e) {
                e.preventDefault();
                e.stopPropagation();
                $('#sbs-add-more-overlay').remove();
                self._addGuestTogether();
            });

            $('#sbs-add-more-no').on('click', function() {
                $('#sbs-add-more-overlay').remove();
                self.goToStep(4);
            });

            overlay.on('click', function(e) {
                if (e.target === overlay[0]) {
                    $('#sbs-add-more-overlay').remove();
                    self.goToStep(4);
                }
            });
        }

        // Show a green banner at the top of Step 1 indicating who we are selecting for
        _showGuestPickBanner(label) {
            const self = this;
            // Remove any existing banner first (prevent duplicates)
            $('#sbs-guest-pick-banner').remove();
            const $banner = $(`
                <div id="sbs-guest-pick-banner" class="sbs-guest-pick-banner" style="
                    background:#f0fdf4;border:1.5px solid #16a34a;border-radius:12px;
                    padding:14px 16px;margin:0 0 18px;box-sizing:border-box;min-height:68px;overflow:visible;
                    display:flex;align-items:center;justify-content:space-between;line-height:1.3;">
                    <span style="font-size:14px;font-weight:600;color:#16a34a;">
                        &#128101; Selecting service for <strong>${label}</strong>
                    </span>
                    <button type="button" id="sbs-guest-banner-cancel" style="
                        background:none;border:none;color:#6b7280;font-size:13px;cursor:pointer;">
                        Cancel
                    </button>
                </div>
            `);
            // Prepend immediately - guard against double-fire by checking if already present
            if (!$('#sbs-guest-pick-banner').length) {
                $('.sbs-step-1').prepend($banner);
            }
            $('#sbs-guest-banner-cancel').on('click', function() {
                $('#sbs-guest-pick-banner').remove();
                self._nextGuestLabel = null;
                self._guestCount = Math.max(0, self._guestCount - 1);
                self._saveSession();
            });
        }

        // Fetch staff for a service (with cache) - used for inline dropdown
        _fetchStaffForService(serviceId, cb) {
            if (this._staffCache[serviceId] && this._staffCache[serviceId].length > 0) {
                cb(this._staffCache[serviceId]);
                return;
            }
            const preloaded = this._getPreloadedServiceStaff(serviceId);
            if (preloaded && preloaded.staff && preloaded.staff.length > 0) {
                this._staffCache[serviceId] = preloaded.staff;
                cb(preloaded.staff);
                return;
            }
            const self = this;
            $.ajax({
                url: yatoonData.ajax_url, type: 'POST',
                data: { action: 'yatoon_get_available_staff', nonce: yatoonData.nonce, service_id: serviceId },
                success: function(r) {
                    if (r.success && r.data.staff) {
                        self._staffCache[serviceId] = r.data.staff;
                        cb(r.data.staff);
                    } else { cb([]); }
                },
                error: function() { cb([]); }
            });
        }

        // Build an inline staff <select> for a service entry
        _buildStaffSelect(svcEntry, pendingIdx) {
            const self = this;
            const svcId = svcEntry.service ? svcEntry.service.id : null;
            const current = svcEntry.staff;
            const cached = svcId ? (self._staffCache[svcId] || null) : null;

            if (!cached) {
                // Fetch async and re-render summary/cart when done
                if (svcId) {
                    self._fetchStaffForService(svcId, function() {
                        self.updateSummary();
                        self.renderCart();
                        if (self.currentStep === 4) self._showPendingServicesSummary();
                    });
                }
                const n = current ? self.getFirstName(current.name) : 'Any';
                return `<span style="color:#6b7280;font-size:12px;">&#128100; ${n}</span>`;
            }

            if (cached.length <= 1) {
                const n = current ? self.getFirstName(current.name) : 'Any';
                return `<span style="color:#6b7280;font-size:12px;">&#128100; ${n}</span>`;
            }

            // -- Time-aware staff conflict detection ------------------------
            // Guests run in PARALLEL (they all start at the same appointment time),
            // while each guest's own services run sequentially one after another.
            // So the correct window for THIS service is its offset within its own
            // guest's timeline, and it can conflict with ANY guest's services that
            // overlap that same real-time window (not just services listed earlier).
            const myIdx = self.pendingServices.indexOf(svcEntry);
            const offsetsAll = self._serviceOffsetsByGuest(self.pendingServices);
            const myStart = myIdx >= 0 ? offsetsAll[myIdx] : self._nextOffsetForGuest(svcEntry.guestLabel);
            const myDur   = self._serviceDuration(svcEntry);
            const myEnd   = myStart + myDur;

            // Staff is busy if another guest's (or my own other) service overlaps [myStart, myEnd)
            const busyIds = self._computeBusyStaffIds(myStart, myEnd, svcEntry);

            const opts = cached.map(function(st) {
                const sel     = current && String(st.id) === String(current.id) ? ' selected' : '';
                const isBusy  = st.id > 0 && busyIds.has(String(st.id));
                const dis     = isBusy ? ' disabled' : '';
                const sty     = isBusy ? ' style="color:#d1d5db;"' : '';
                const sfx     = isBusy ? ' (busy)' : '';
                return `<option value="${st.id}" data-name="${self._esc(st.name)}"${sel}${dis}${sty}>${self._esc(self.getFirstName(st.name))}${sfx}</option>`;
            }).join('');

            return `<select class="sbs-inline-staff-sel"
                title="${__('Change staff')}" aria-label="${__('Change staff for this service')}"
                data-svc-id="${svcId}"
                data-pending-idx="${typeof pendingIdx === 'number' ? pendingIdx : ''}">${opts}</select>`;
        }

        // Show a compact summary of pending services above the calendar in step 4
        _showPendingServicesSummary() {
            if (!this.pendingServices.length) return;

            // Build one row per service with Edit + Remove buttons
            const self = this;
            const totalServices = this.pendingServices.length;
            // Count services per person so UP/DOWN only appear when a person has 2+ services
            const personServiceCount = {};
            this.pendingServices.forEach(s => {
                const lbl = s.guestLabel || 'Me';
                personServiceCount[lbl] = (personServiceCount[lbl] || 0) + 1;
            });
            // Group display by person: Me first, then guests in order of first appearance
            // Each entry keeps its original idx so Edit/Remove/UP/DOWN still work correctly
            const personOrder2 = [];
            this.pendingServices.forEach((s) => {
                const lbl = s.guestLabel || 'Me';
                if (!personOrder2.includes(lbl)) personOrder2.push(lbl);
            });
            const hasMultiplePeople = personOrder2.some(lbl => lbl !== 'Me');
            const displayOrder = [];
            personOrder2.forEach(lbl => {
                this.pendingServices.forEach((s, idx) => {
                    if ((s.guestLabel || 'Me') === lbl) displayOrder.push({ s, idx });
                });
            });
            const lines = displayOrder.map(({ s, idx }) => {
                const svcName   = this._svcDisplayLabelHtml(s.service, s.options);
                // FIX: hide option names that duplicate the service name or category name (e.g. "Custom Design / Nail Art + Custom Design / Nail Art")
                const _filteredOpts = this._filterOptNames(s.options, s.service);
                const optName   = ''; // Now handled inside _svcDisplayLabel
                const staffName = s.staff ? '&middot; ' + this._esc(this.getFirstName(s.staff.name)) : '';
                const staffSelectHtml = this._buildStaffSelect(s, idx);
                // Duration = main option (single-select) duration + add-on durations
                // If no options, use service base duration
                const _mainOpts  = s.options.filter(o => !o.is_addon);
                const _addonOpts = s.options.filter(o => o.is_addon);
                const _mainDur   = _mainOpts.length > 0
                    ? Math.max(..._mainOpts.map(o => o.duration || 0))
                    : (s.service.duration || 0);
                const _addonDur  = _addonOpts.reduce((d, o) => d + (o.duration || 0), 0);
                const dur        = _mainDur + _addonDur;
                const price     = this._optionsPrice(s.service, s.options);

                // Order buttons only shown when this person has 2+ services.
                const personLbl = s.guestLabel || 'Me';
                const personCount = personServiceCount[personLbl] || 1;
                const upBtn   = (idx > 0 && personCount > 1 && (this.pendingServices[idx-1].guestLabel || 'Me') === personLbl)
                    ? `<button type="button" class="sbs-banner-move-up" data-idx="${idx}"
                            title="${__('Move this service earlier')}" aria-label="${__('Move this service earlier')}">&uarr;</button>`
                    : '';
                const downBtn = (idx < totalServices - 1 && personCount > 1 && (this.pendingServices[idx+1].guestLabel || 'Me') === personLbl)
                    ? `<button type="button" class="sbs-banner-move-down" data-idx="${idx}"
                            title="${__('Move this service later')}" aria-label="${__('Move this service later')}">&darr;</button>`
                    : '';

                let orderLabel = '';
                if (!hasMultiplePeople && totalServices > 1) {
                    const samePersonBefore = this.pendingServices.slice(0, idx).filter(ps => (ps.guestLabel || 'Me') === personLbl);
                    const prevDur = samePersonBefore.reduce((sum, ps) => sum + this._serviceDuration(ps), 0);
                    const serviceNo = samePersonBefore.length + 1;
                    orderLabel = `<span class="sbs-banner-order" aria-label="Service ${serviceNo}">${serviceNo}</span>`;
                }

                const guestBadge = s.guestLabel && s.guestLabel !== 'Me'
                    ? `<span style="display:inline-block;background:#dcfce7;color:#16a34a;border-radius:4px;font-size:10px;font-weight:700;padding:1px 5px;margin-right:4px;">${this._esc(this._displayGuestLabel(s.guestLabel))}</span>`
                    : (s.guestLabel === 'Me' && totalServices > 1
                        ? `<span style="display:inline-block;background:#dbeafe;color:#1d4ed8;border-radius:4px;font-size:10px;font-weight:700;padding:1px 5px;margin-right:4px;">Me</span>`
                        : '');
                const guestSep = guestBadge ? `<span style="color:#9ca3af;margin-right:4px;">-</span>` : '';
                return `<div class="sbs-banner-service-row" data-idx="${idx}">
                    <div class="sbs-banner-service-main">
                        <div class="sbs-banner-service-name">${guestBadge}${guestSep}${orderLabel}${svcName}${optName}</div>
                        <div class="sbs-banner-service-meta">${staffSelectHtml}<span>${dur} min</span><span>${ysFormatMoney(price)}</span></div>
                    </div>
                    <div class="sbs-banner-service-actions">
                        ${upBtn}${downBtn}
                        <button type="button" class="sbs-banner-edit-btn" data-idx="${idx}"
                                aria-label="${__('Edit this service')}">${__('Edit')}</button>
                        <button type="button" class="sbs-banner-remove-btn" data-idx="${idx}"
                                aria-label="${__('Remove this service')}">${__('Remove')}</button>
                    </div>
                </div>`;
            }).join('');

            const total = this._durationByGuest(this.pendingServices).totalDuration;

            // Group services by person label
                const personGroups = {};
                const personOrder  = [];
                self.pendingServices.forEach(function(s) {
                const lbl = s.guestLabel || 'Me';
                if (!personGroups[lbl]) { personGroups[lbl] = []; personOrder.push(lbl); }
                personGroups[lbl].push(s);
            });
            const isGroupMode = personOrder.length > 1 || (personOrder.length === 1 && personOrder[0] !== 'Me');
            const durationCopy = isGroupMode
                ? ''
                : '';

            const html = `<section id="sbs-pending-services-banner" aria-labelledby="sbs-pending-services-title">
                <div class="sbs-banner-heading">
                    <div><span class="sbs-banner-check" aria-hidden="true">&#10003;</span><strong id="sbs-pending-services-title">Your appointment</strong><span>${totalServices} service${totalServices === 1 ? '' : 's'}</span></div>
                </div>
                <div id="sbs-banner-rows">${lines}</div>
                <div class="sbs-banner-add-actions">
                    ${personOrder.map(lbl => `<button type="button" class="sbs-add-service-for-person sbs-btn sbs-btn-add-more-banner sbs-add-service-to-appt" data-person-label="${this._esc(lbl)}"
                        >${(yatoonData.btnLabels && yatoonData.btnLabels.add_for_person) || __('Add service for')} ${this._esc(this._displayGuestLabel(lbl))}</button>`).join('')}
                    <button type="button" id="sbs-banner-add-guest" class="sbs-btn"
                        >${(yatoonData.btnLabels && yatoonData.btnLabels.add_guest) || __('Add guest together')}
                    </button>
                </div>
                ${durationCopy ? `<div style="font-size:12px;color:#6b7280;margin-top:8px;">${durationCopy}</div>` : ''}
            </section>`;

            const existing = $('#sbs-pending-services-banner');
            if (existing.length) existing.replaceWith(html);
            else $('.sbs-step-4 h2').after(html);

            // Rebind banner Edit / Remove (use document delegation to survive re-renders)
            $(document).off('click.bannerEdit').on('click.bannerEdit', '.sbs-banner-edit-btn', function() {
                const idx = parseInt($(this).data('idx'));
                self._editPendingService(idx);
            });
            $(document).off('click.bannerRemove').on('click.bannerRemove', '.sbs-banner-remove-btn', function() {
                const idx = parseInt($(this).data('idx'));
                self._removePendingService(idx);
            });
            $(document).off('change.bannerInlineStaff').on('change.bannerInlineStaff', '#sbs-pending-services-banner .sbs-inline-staff-sel', function() {
                const idx     = parseInt($(this).data('pending-idx'));
                const newId   = parseInt($(this).val());
                const newName = $(this).find('option:selected').data('name');
                if (!Number.isNaN(idx) && self.pendingServices[idx]) {
                    self.pendingServices[idx].staff = { id: newId, name: newName };
                    self.selectedDateTime = null;
                    $('#sbs-selected-start-at').val('');
                    $('.sbs-time-slot').removeClass('selected');
                    self._saveSession();
                    self.updateSummary();
                    self._showPendingServicesSummary();
                    // 4.6.2: a different staff member has a different calendar,
                    // so the whole visible range's density is now wrong. Same
                    // dates, new answer -> force.
                    if (self.currentStep === 4) self.loadAvailabilityDensity(true);
                    const selDate = $('.sbs-day-button.selected').data('date');
                    if (selDate && self.currentStep === 4) {
                        self.loadAvailableTimes(selDate);
                    }
                }
            });

            // -- Move service UP in pendingServices (swap with previous) --------
            $(document).off('click.bannerMoveUp').on('click.bannerMoveUp', '.sbs-banner-move-up', function() {
                const idx = parseInt($(this).data('idx'));
                if (idx <= 0 || idx >= self.pendingServices.length) return;
                // Swap idx and idx-1
                [self.pendingServices[idx - 1], self.pendingServices[idx]] =
                    [self.pendingServices[idx], self.pendingServices[idx - 1]];
                self._saveSession();
                self._showPendingServicesSummary();
                // Reload times for the new order
                const selDate = $('.sbs-day-button.selected').data('date');
                if (selDate) self.loadAvailableTimes(selDate);
            });

            // -- Move service DOWN in pendingServices (swap with next) ----------
            $(document).off('click.bannerMoveDown').on('click.bannerMoveDown', '.sbs-banner-move-down', function() {
                const idx = parseInt($(this).data('idx'));
                if (idx < 0 || idx >= self.pendingServices.length - 1) return;
                // Swap idx and idx+1
                [self.pendingServices[idx], self.pendingServices[idx + 1]] =
                    [self.pendingServices[idx + 1], self.pendingServices[idx]];
                self._saveSession();
                self._showPendingServicesSummary();
                // Reload times for the new order
                const selDate = $('.sbs-day-button.selected').data('date');
                if (selDate) self.loadAvailableTimes(selDate);
            });

            // Sync right-side summary + mobile cart bar
            this.updateSummary();
        }

        _syncSidebarWithPending() {
            // Deprecated: now handled by updateSummary()
            this.updateSummary();
        }

        _syncSidebarWithPending_OLD() {
            if (!this.pendingServices.length) return;
            const all = this.pendingServices;
            // Service names
            const svcLines = all.map(s => {
                const name = s.service.category ? s.service.category + ' - ' + s.service.name : s.service.name;
                const opt  = s.options.length > 0 ? ' + ' + s.options.map(o => o.name).join(', ') : '';
                return name + opt;
            });
            const svcText = svcLines.length === 1 ? svcLines[0] : svcLines.length + ' services: ' + svcLines.join(', ');
            $('#sbs-summary-service').text(svcText).css('color', '#1a1a1a');
            $('#sbs-edit-service-btn, #sbs-remove-service-btn').hide();

            // Show/hide sidebar empty guide - check cart AND pending services
            const hasAnyService = svcLines.length > 0 || (this.bookingCart && this.bookingCart.length > 0);
            $('#sbs-sidebar-empty-guide').toggle(!hasAnyService);
            $('#sbs-booking-summary-panel').toggle(hasAnyService);

            // Staff names
            const staffNames = [...new Set(all.map(s => s.staff ? this.getFirstName(s.staff.name) : null).filter(Boolean))];
            $('#sbs-summary-staff').text(staffNames.length ? staffNames.join(' & ') : __('Not selected')).css('color', staffNames.length ? '#1a1a1a' : '#9ca3af');

            // Options
            const allOpts = all.flatMap(s => s.options.map(o => o.name));
            if (allOpts.length) {
                $('#sbs-summary-options').text(allOpts.join(', '));
                $('#sbs-summary-options-container').show();
            } else {
                $('#sbs-summary-options-container').hide();
            }

            // Duration + total
            const totalDur = this._durationByGuest(all).totalDuration;
            const totalPrice = all.reduce((sum, s) => sum + this._optionsPrice(s.service, s.options), 0);
            $('#sbs-summary-duration').text(totalDur ? __('Appointment Length:') + ' ' + totalDur + ' ' + __('minutes') : '-');
            this._renderSummaryTax(totalPrice);
        }

        /**
         * Show the tax line and the amount actually payable.
         *
         * FIX (4.4.3): the summary printed the configured service price and
         * stopped there. With tax-exclusive pricing the server adds tax when
         * the booking is written, so the customer confirmed one number and was
         * charged a larger one -- under a panel that promises "Review your
         * total before confirming".
         *
         * Mirrors YATOON_Tax::breakdown() exactly, including rounding once at
         * the end, so the figure shown here matches the row the server stores.
         * The server remains authoritative; this is display only.
         *
         * @param {number} netOrGross Sum of configured service prices.
         */
        /**
         * Tax line HTML and the payable total for a given net/gross figure.
         *
         * Single source of truth for the summary panel, which is rendered from
         * THREE different places (updateSummary plus two full-panel rebuilds).
         * Duplicating the arithmetic per renderer is how the panel came to
         * disagree with the server in the first place.
         *
         * @param {number} netOrGross Sum of configured service prices.
         * @returns {{row: string, total: number, hasTax: boolean}}
         */
        /**
         * The "due now" row under the summary total, or '' when there is no
         * deposit to declare.
         *
         * 4.6.2: this is deliberately rendered next to the total rather than on
         * the individual service cards. In percent mode the deposit is a share
         * of the whole order, so a per-card figure would be wrong the moment a
         * second service is added - and a number that changes when you are not
         * looking at it is worse than no number. The cards say a deposit
         * applies; the summary, where the total actually exists, says how much.
         *
         * @param {number} total Order total in major units.
         * @returns {string} HTML row, or ''.
         */
        _depositRowHtml(total) {
            const due = (typeof ysCalcDeposit === 'function') ? ysCalcDeposit(total) : 0;
            if (!due || due <= 0) return '';
            const label = (yatoonData.depositType === 'percent')
                /* translators: %s is a percentage, e.g. 20 */
                ? __('Deposit due now (%s%)').replace('%s', String(Number(yatoonData.depositAmount)))
                : __('Deposit due now');
            return `
                    <div class="sbs-summary-item sbs-summary-deposit" style="margin-top:6px;">
                        <span style="font-size:13px;color:var(--sbs-muted,#6b7280);">${label}</span>
                        <span style="font-size:14px;font-weight:700;">${ysFormatMoney(due)}</span>
                    </div>`;
        }

        _taxParts(netOrGross) {
            const cfg = (typeof yatoonData !== 'undefined' && yatoonData && yatoonData.tax) ? yatoonData.tax : null;

            // wp_localize_script() stringifies scalars, so coerce rather than
            // type-check (see ysSiteNow for the same trap).
            const enabled = !!(cfg && (cfg.enabled === true || cfg.enabled === '1' || cfg.enabled === 1));
            const defaultRate = cfg ? parseFloat(cfg.rate) : 0;
            const inclusive = !!(cfg && (cfg.inclusive === true || cfg.inclusive === '1' || cfg.inclusive === 1));

            if (!netOrGross || !enabled || !isFinite(defaultRate)) {
                return { row: '', total: netOrGross || 0, hasTax: false, label: '', amount: 0, net: netOrGross || 0 };
            }

            // Build service lines so per-service overrides (including an
            // explicit zero rate) use the same parent-service rate as PHP.
            const lines = [];
            const addService = (service, options) => {
                if (!service) return;
                const amount = this._optionsPrice(service, options || []);
                if (amount > 0) lines.push({
                    amount: amount,
                    rate: service.taxRate === null || service.taxRate === undefined || isNaN(service.taxRate)
                        ? defaultRate : Number(service.taxRate)
                });
            };
            (this.bookingCart || []).forEach(item => {
                if (item.services && item.services.length) item.services.forEach(s => addService(s.service, s.options));
                else addService(item.service, item.options);
            });
            if (!lines.length) (this.pendingServices || []).forEach(s => addService(s.service, s.options));
            if (!lines.length) addService(this.selectedService, this.selectedOptions);

            const lineBase = lines.reduce((sum, line) => sum + line.amount, 0);
            const scale = lineBase > 0 ? netOrGross / lineBase : 1;
            if (!lines.length) lines.push({ amount: netOrGross, rate: defaultRate });

            let net = 0, tax = 0, gross = 0;
            const usedRates = [];
            lines.forEach(line => {
                const amount = line.amount * scale;
                const rate = Math.max(0, Math.min(100, Number(line.rate) || 0));
                if (usedRates.indexOf(rate) === -1) usedRates.push(rate);
                if (inclusive) {
                    const lineNet = rate > 0 ? amount / (1 + rate / 100) : amount;
                    net += lineNet;
                    tax += amount - lineNet;
                    gross += amount;
                } else {
                    net += amount;
                    tax += amount * (rate / 100);
                    gross += amount * (1 + rate / 100);
                }
            });
            if (tax <= 0) {
                return { row: '', total: Math.round(gross * 100) / 100, hasTax: false, label: '', amount: 0, net: Math.round(net * 100) / 100 };
            }
            // Round once, at the end - same as YATOON_Tax::breakdown().
            tax   = Math.round(tax * 100) / 100;
            gross = Math.round(gross * 100) / 100;

            const label  = this._esc(cfg.label || 'Tax');
            const pretty = usedRates.length === 1 ? String(usedRates[0]).replace(/\.0+$/, '') + '%' : 'mixed rates';
            const text   = inclusive ? (__('Includes') + ' ' + label + ' (' + pretty + ')')
                                     : (label + ' (' + pretty + ')');

            return {
                row: '<div class="sbs-summary-item" id="sbs-summary-row-tax">' +
                     '<span id="sbs-summary-tax-label" style="font-size:13px;color:#6b7280;">' + text + '</span>' +
                     '<span id="sbs-summary-tax-amount" style="font-size:13px;color:#6b7280;">' + ysFormatMoney(tax) + '</span>' +
                     '</div>',
                total: gross,
                hasTax: true,
                // Raw parts, so callers that cannot reuse `row` (it carries IDs
                // that must stay unique on the page) can build their own markup.
                label: text,
                amount: tax,
                net: Math.round(net * 100) / 100
            };
        }

        /**
         * Format an absolute instant as a wall-clock time in the SALON's zone.
         *
         * FIX (4.4.3): the confirmation screen used
         *     new Date(start_at).toLocaleTimeString(...)
         * where start_at is the server's UTC "...Z" string, and
         * toLocaleTimeString renders in the VISITOR's timezone. A 2:00 PM
         * appointment at a UTC+8 salon was confirmed to a US visitor as
         * "2:00 AM" -- the wrong time on the one screen customers keep.
         * Same root cause as the slot-grouping bug.
         *
         * @param {Date} d
         * @returns {string}
         */
        _siteFmtTime(d) {
            if (!d || isNaN(d.getTime())) return '';
            const zone = (typeof yatoonData !== 'undefined' && yatoonData) ? String(yatoonData.siteTimezone || '') : '';
            if (zone && zone.indexOf('/') !== -1 && typeof Intl !== 'undefined' && Intl.DateTimeFormat) {
                try {
                    // 4.6.2: was pinned to 'en-US' with hour12:true, which forced
                    // an American 12-hour clock on every visitor -- a German or
                    // Japanese customer read "2:00 PM" where their whole locale
                    // uses "14:00". The rest of this file already passes
                    // `undefined` to follow the browser locale; do the same here.
                    // The timeZone stays pinned to the salon, which is the part
                    // that actually had to be forced. hour12 is dropped too so
                    // the locale picks its own clock convention.
                    //
                    // The old comment justified 'en-US' by "the server sends an
                    // ASCII 12-hour start label, so the end must match it". That
                    // is now handled at the only place it mattered
                    // (_formatItemTimeRange), which formats BOTH ends through
                    // this method instead of pairing a server label with a
                    // locally formatted one.
                    return new Intl.DateTimeFormat(undefined, {
                        timeZone: zone,
                        hour: 'numeric',
                        minute: '2-digit'
                    }).format(d);
                } catch (e) {
                    // Raw UTC offsets are not valid Intl timeZone values; use
                    // the server supplied offset fallback below for those sites.
                }
            }
            const raw = (typeof yatoonData !== 'undefined' && yatoonData) ? yatoonData.siteUtcOffsetMin : null;
            let shifted = d;
            if (raw !== null && raw !== undefined && raw !== '') {
                const off = Number(raw);
                if (isFinite(off) && off >= -840 && off <= 840) {
                    shifted = new Date(d.getTime() + (off + d.getTimezoneOffset()) * 60000);
                }
            }
            // 4.6.2: same change as the Intl branch above -- the raw-UTC-offset
            // fallback path was also hardcoded to the US 12-hour clock.
            return shifted.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' });
        }

        /**
         * DOM-updating variant used by updateSummary().
         *
         * @param {number} netOrGross
         */
        _renderSummaryTax(netOrGross) {
            const parts = this._taxParts(netOrGross);
            $('#sbs-summary-row-tax').remove();
            if (parts.hasTax) {
                $('#sbs-summary-row-total').before(parts.row);
            }
            $('#sbs-summary-total').text(netOrGross ? ysFormatMoney(parts.total) : '-');
        }

        bindEvents() {
            const self = this;

            // Progress bar - clicking any reached/completed step jumps there
            // Use both 'click' and touchend for mobile reliability
            function handleProgStepTap(el) {
                const $el = $(el);
                const target = parseInt($el.data('go'));
                if (!$el.hasClass('active') && !$el.hasClass('completed')) return;
                if (target === self.currentStep) return;
                if (target < 5 && self.bookingCart.length && !self.selectedService && !self.pendingServices.length) {
                    self._showToast(__('Use Edit on an appointment to change its service, staff, date, or time.'), 'info');
                    $('#sbs-booking-cart-list .sbs-cart-edit').first().trigger('focus');
                    return;
                }
                if (target < self.currentStep) {
                    if (target <= 1) { self.selectedService = null; self.selectedCategory = null; }
                    if (target <= 2) self.selectedOptions = [];
                    if (target <= 3) self.selectedStaff = null;
                    if (target <= 4) self.selectedDateTime = null;
                }
                self.goToStep(target, target !== 3);  // Staff scrolls after its cards render.
                if (target === 3) self.loadAvailableStaff();
            }
            $(document).on('click', '.sbs-prog-step', function(e) {
                e.preventDefault();
                handleProgStepTap(this);
            });
            // touchend fallback for mobile (avoids 300ms delay and scroll cancel issues)
            let _progTouchMoved = false;
            $(document).on('touchstart', '.sbs-prog-step', function() { _progTouchMoved = false; });
            $(document).on('touchmove', '.sbs-prog-step', function() { _progTouchMoved = true; });
            $(document).on('touchend', '.sbs-prog-step', function(e) {
                if (_progTouchMoved) return;
                e.preventDefault();
                handleProgStepTap(this);
            });

            $(document).on('click', '.sbs-step-accordion-item.is-clickable', function(e) {
                e.preventDefault();
                self._goToAccordionStep($(this).data('step-target'));
            });

            // More dropdown
            let _moreOpen = false;

            function _ensureMoreDropdownItems() {
                const wrap = document.querySelector('.sbs-category-tabs');
                const dd   = document.getElementById('sbs-cat-more-dropdown');
                if (!wrap || !dd) return false;
                if (dd.children.length > 0) return true;

                const overflowTabs = Array.from(wrap.querySelectorAll('.sbs-cat-tab.sbs-cat-overflow:not(.sbs-cat-more-btn)'));
                dd.innerHTML = '';
                overflowTabs.forEach(t => {
                    if (!t.dataset.cat) return;
                    const btn = document.createElement('button');
                    btn.type = 'button';
                    btn.className = 'sbs-cat-dropdown-item' + (t.classList.contains('active') ? ' active' : '');
                    btn.dataset.cat = t.dataset.cat;
                    btn.textContent = t.textContent.trim();
                    dd.appendChild(btn);
                });
                return dd.children.length > 0;
            }

            function _positionDropdown() {
                const btn = document.getElementById('sbs-cat-more-btn');
                const dd  = document.getElementById('sbs-cat-more-dropdown');
                if (!btn || !dd) return;
                const r   = btn.getBoundingClientRect();
                const host = document.querySelector('.sbs-step-1') || document.querySelector('.sbs-booking-main') || document.body;
                const hr   = host.getBoundingClientRect();
                const hostW = host.clientWidth || window.innerWidth;
                const ddW = 180;
                let left = r.right - hr.left - ddW;
                if (left < 6) left = 6;
                if (left + ddW > hostW - 6) left = hostW - ddW - 6;
                dd.style.top  = (r.bottom - hr.top + 6) + 'px';
                dd.style.left = left + 'px';
                dd.style.width = Math.min(ddW, hostW - 12) + 'px';
            }
            function _setMoreButtonState(open) {
                const $button = $('#sbs-cat-more-btn');
                const selectedLabel = $button.attr('data-selected-label') || __('More');
                $button.text(selectedLabel + (open ? ' ⌃' : ' ⌄'));
                $button.attr('aria-expanded', open ? 'true' : 'false');
            }
            function _openMore() {
                _adjustTabVisibility();
                if (!_ensureMoreDropdownItems()) return;
                _positionDropdown();
                $('#sbs-cat-more-dropdown').addClass('open');
                _setMoreButtonState(true);
                _moreOpen = true;
            }
            function _closeMore() {
                $('#sbs-cat-more-dropdown').removeClass('open');
                _setMoreButtonState(false);
                _moreOpen = false;
            }

            let _lastMoreTap = 0;
            $(document).on('click touchend', '#sbs-cat-more-btn', function(e) {
                const now = Date.now();
                if (now - _lastMoreTap < 220) return;
                _lastMoreTap = now;
                e.preventDefault();
                e.stopPropagation();
                _moreOpen = $('#sbs-cat-more-dropdown').hasClass('open');
                _moreOpen ? _closeMore() : _openMore();
            });
            $(document).on('click touchend', function(e) {
                if (_moreOpen && !$(e.target).is('#sbs-cat-more-btn') && !$(e.target).closest('#sbs-cat-more-dropdown').length) _closeMore();
            });
            $(document).on('click touchend', '.sbs-cat-dropdown-item', function(e) {
                e.preventDefault();
                e.stopPropagation();
                const slug = $(this).data('cat');
                _closeMore();
                $('.sbs-cat-tab').removeClass('active');
                $('.sbs-cat-dropdown-item').removeClass('active');
                $(this).addClass('active');
                $('#sbs-cat-more-btn').addClass('has-active');
                const $heading = $('#cat-' + slug);
                if ($heading.length) {
                    const navH = ($('.sbs-category-tabs-wrap').outerHeight() || 0) + 8;
                    setTimeout(function() {
                        $('html, body').animate({ scrollTop: $heading.offset().top - navH }, 350);
                    }, 30);
                }
            });
            $(window).on('scroll.moreDD resize.moreDD', function() {
                if (_moreOpen) _positionDropdown();
            });

            // Drawer swipe
            let touchStartY = 0;
            $(document).on('touchstart', '#sbs-mobile-drawer', function(e) {
                touchStartY = e.originalEvent.touches[0].clientY;
            });
            $(document).on('touchend', '#sbs-mobile-drawer', function(e) {
                const diff = e.originalEvent.changedTouches[0].clientY - touchStartY;
                if (diff > 60) self._closeDrawer();
            });

            // Text Us - auto-collapse to icon-only after 4 seconds
            const $textUsBtn = $('#sbs-text-us-btn');
            if ($textUsBtn.length) {
                setTimeout(function() {
                    $textUsBtn.addClass('sbs-textus-collapsed');
                }, 4000);
                // Re-expand on click (before opening modal), re-collapse after modal closes
                $textUsBtn.on('mouseenter touchstart', function() {
                    $textUsBtn.removeClass('sbs-textus-collapsed');
                });
            }

            // Text Us modal
            $(document).on('click', '#sbs-text-us-btn', function() {
                $('#sbs-text-us-modal').addClass('open');
                $('#sbs-text-us-form').show();
                $('#sbs-text-success').hide();
            });
            $(document).on('click', '.sbs-text-modal-close, .sbs-text-modal', function(e) {
                if (e.target === this) {
                    $('#sbs-text-us-modal').removeClass('open');
                    // Re-collapse after modal closes
                    setTimeout(function() { $textUsBtn.addClass('sbs-textus-collapsed'); }, 1500);
                }
            });
            $(document).on('submit', '#sbs-text-us-form', function(e) {
                e.preventDefault();
                const $form = $(this);
                const $btn  = $form.find('.sbs-text-submit-btn');
                const data  = {
                    action: 'yatoon_send_contact_message',
                    nonce:  yatoonData.nonce,
                    name:   $form.find('[name="name"]').val(),
                    phone:  $form.find('[name="phone"]').val(),
                    message: $form.find('[name="message"]').val()
                };
                $btn.prop('disabled', true).text(__('Sending...'));
                $.post(yatoonData.ajax_url, data, function(resp) {
                    if (resp.success) {
                        $form.hide();
                        $('#sbs-text-success').show();
                        setTimeout(function() {
                            $('#sbs-text-us-modal').removeClass('open');
                            $form[0].reset();
                            $btn.prop('disabled', false).text(__('Send message'));
                            setTimeout(function() { $textUsBtn.addClass('sbs-textus-collapsed'); }, 500);
                        }, 2000);
                    } else {
                        self._showToast(__('Failed to send. Please try again.'), 'error');
                        $btn.prop('disabled', false).text(__('Send message'));
                    }
                }).fail(function() {
                    self._showToast(__('Network error. Please try again.'), 'error');
                    $btn.prop('disabled', false).text(__('Send message'));
                });
            });

            // Remove fade hint when tabs are scrolled to end
            $(document).on('scroll', '.sbs-category-tabs', function() {
                const el = this;
                const atEnd = el.scrollLeft + el.clientWidth >= el.scrollWidth - 5;
                $(el).toggleClass('scrolled-end', atEnd);
            });

            // Dynamically adjust which tabs are visible vs. in More dropdown
            function _adjustTabVisibility() {
                if (window.__yatoonCategoryBar423) return;
                const wrap = document.querySelector('.sbs-category-tabs');
                const moreBtn = document.getElementById('sbs-cat-more-btn');
                if (!wrap) return;

                const allTabs = Array.from(wrap.querySelectorAll('.sbs-cat-tab:not(.sbs-cat-more-btn)'));
                if (!allTabs.length) return;

                if (window.innerWidth >= 769) {
                    allTabs.forEach(t => {
                        t.classList.remove('sbs-cat-overflow');
                        t.style.display = 'inline-flex';
                        t.style.fontSize = '';
                        t.style.padding = '';
                    });
                    const dd = document.getElementById('sbs-cat-more-dropdown');
                    if (!moreBtn || allTabs.length <= 4) {
                        if (moreBtn) {
                            moreBtn.classList.remove('sbs-more-needed');
                            moreBtn.style.display = 'none';
                        }
                        if (dd) {
                            dd.classList.remove('open');
                            dd.innerHTML = '';
                        }
                        return;
                    }

                    moreBtn.classList.add('sbs-more-needed');
                    moreBtn.style.display = 'inline-flex';
                    moreBtn.style.fontSize = '';
                    moreBtn.style.padding = '';
                    const visibleCount = 4;
                    const hiddenTabs = allTabs.slice(visibleCount);
                    hiddenTabs.forEach(t => {
                        t.classList.add('sbs-cat-overflow');
                        t.style.display = 'none';
                    });
                    _syncMoreDropdownItems(hiddenTabs);
                    return;
                }

                if (!moreBtn) return;

                const w = window.innerWidth;
                const MIN_FONT = w >= 414 ? 8.6 : 9;
                const MAX_FONT = w >= 414 ? 10.2 : w < 390 ? 9.8 : 10.6;

                // Reset all tabs
                allTabs.forEach(t => {
                    t.classList.remove('sbs-cat-overflow');
                    t.style.display = '';
                    t.style.fontSize = '';
                    t.style.padding = '';
                });
                moreBtn.classList.remove('sbs-more-needed');
                moreBtn.style.display = 'none';
                moreBtn.style.fontSize = '';
                moreBtn.style.padding = '';

                // Get true usable width (account for container padding)
                const wrapStyle = window.getComputedStyle(wrap);
                const padL = parseFloat(wrapStyle.paddingLeft)  || 0;
                const padR = parseFloat(wrapStyle.paddingRight) || 0;
                const trueW = ((wrap.parentElement ? wrap.parentElement.clientWidth : wrap.clientWidth) - padL - padR);

                // If everything can fit at the mobile max size, show all tabs without More.
                _scaleFontToFit(allTabs, trueW, MIN_FONT, MAX_FONT);
                const allFit = allTabs.reduce((sum, t) => sum + t.offsetWidth, 0) <= trueW;
                if (allFit) {
                    moreBtn.classList.remove('sbs-more-needed');
                    moreBtn.style.display = 'none';
                    return;
                }

                // Measure More button at MAX_FONT
                moreBtn.style.fontSize = MAX_FONT + 'px';
                moreBtn.style.padding = Math.round(MAX_FONT * 0.55) + 'px ' + Math.round(MAX_FONT * 0.58) + 'px';
                moreBtn.style.display = 'inline-flex';
                const moreBtnW = moreBtn.offsetWidth || 44;
                moreBtn.style.display = 'none';

                const available = trueW - moreBtnW - 8;

                let visibleTarget = w >= 414 ? 3 : 2;
                visibleTarget = Math.min(visibleTarget, allTabs.length - 1);
                const minVisibleTarget = w >= 414 ? Math.min(3, allTabs.length - 1) : 1;
                while (visibleTarget > minVisibleTarget) {
                    const testTabs = allTabs.slice(0, visibleTarget);
                    _scaleFontToFit(testTabs, available, MIN_FONT, MAX_FONT);
                    const testW = testTabs.reduce((sum, t) => sum + t.offsetWidth, 0);
                    if (testW <= available) break;
                    visibleTarget--;
                }

                const visibleTabs = allTabs.slice(0, visibleTarget);
                const hiddenTabs  = allTabs.slice(visibleTarget);

                hiddenTabs.forEach(t => {
                    t.classList.add('sbs-cat-overflow');
                    t.style.display = 'none';
                });

                _scaleFontToFit(visibleTabs, available, MIN_FONT, MAX_FONT);

                const finalSize = parseFloat(visibleTabs[0].style.fontSize) || MAX_FONT;
                moreBtn.classList.add('sbs-more-needed');
                moreBtn.style.fontSize = finalSize + 'px';
                moreBtn.style.padding = Math.round(finalSize * 0.55) + 'px ' + Math.round(finalSize * 0.58) + 'px';
                moreBtn.style.display = 'inline-flex';

                // Rebuild dropdown
                _syncMoreDropdownItems(hiddenTabs);

                const atEnd = wrap.scrollLeft + wrap.clientWidth >= wrap.scrollWidth - 5;
                $(wrap).toggleClass('scrolled-end', atEnd);
            }

            function _scaleFontToFit(tabs, availableW, minFont, maxFont) {
                if (!tabs.length) return;
                let lo = minFont, hi = maxFont, bestSize = minFont;
                for (let iter = 0; iter < 8; iter++) {
                    const mid = (lo + hi) / 2;
                    tabs.forEach(t => {
                        t.style.fontSize = mid + 'px';
                        t.style.padding = Math.round(mid * 0.6) + 'px ' + Math.round(mid * 0.75) + 'px';
                    });
                    let totalW = 0;
                    tabs.forEach(t => { totalW += t.offsetWidth; });
                    if (totalW <= availableW) { bestSize = mid; lo = mid; }
                    else { hi = mid; }
                }
                tabs.forEach(t => {
                    t.style.fontSize = bestSize + 'px';
                    t.style.padding = Math.round(bestSize * 0.6) + 'px ' + Math.round(bestSize * 0.75) + 'px';
                });
            }

            // Only touch the dropdown DOM when its category list actually
            // changed. Replacing identical children woke the page-wide
            // MutationObserver repeatedly and caused the menu to flash/empty.
            function _syncMoreDropdownItems(tabs) {
                const dd = document.getElementById('sbs-cat-more-dropdown');
                if (!dd) return;
                const desired = tabs.filter(t => t.dataset.cat).map(t => ({
                    slug: String(t.dataset.cat),
                    label: t.textContent.trim(),
                    active: t.classList.contains('active')
                }));
                const current = Array.from(dd.querySelectorAll('.sbs-cat-dropdown-item[data-cat]')).map(item => ({
                    slug: String(item.dataset.cat),
                    label: item.textContent.trim(),
                    active: item.classList.contains('active')
                }));
                if (JSON.stringify(current) === JSON.stringify(desired)) return;
                dd.innerHTML = '';
                desired.forEach(item => {
                    const btn = document.createElement('button');
                    btn.type = 'button';
                    btn.className = 'sbs-cat-dropdown-item' + (item.active ? ' active' : '');
                    btn.dataset.cat = item.slug;
                    btn.textContent = item.label;
                    dd.appendChild(btn);
                });
            }

            // Run on load (after fonts/layout settle) and on resize
            setTimeout(_adjustTabVisibility, 400);
            $(window).on('resize.sbsTabs', function() {
                setTimeout(_adjustTabVisibility, 100);
            });

            // Page builder editor canvases (Elementor, and similar) can
            // re-render this widget's markup via an AJAX call into the same
            // already-loaded iframe, with no full page load for the above
            // one-time setTimeout/resize binding to ever run again against
            // the fresh markup. A ResizeObserver on the tab row itself also
            // catches cases where the available width changes for reasons
            // that don't fire a window resize event at all (e.g. an editor
            // sidebar panel opening/closing, or the iframe's own layout
            // still settling after initial paint).
            if ( typeof ResizeObserver !== 'undefined' ) {
                const _tabsWrapEl = document.querySelector( '.sbs-category-tabs' );
                if ( _tabsWrapEl ) {
                    new ResizeObserver( function() {
                        setTimeout( _adjustTabVisibility, 50 );
                    } ).observe( _tabsWrapEl );
                }
            }

            // Compact service search (filters services list by name/description)
            $(document).on('input', '#sbs-service-search', function() {
                const q = $(this).val().trim().toLowerCase();
                $('#sbs-service-search-clear').toggle(q.length > 0);

                const $items = $('.sbs-services-list .sbs-service-item');
                const $headings = $('.sbs-services-list .sbs-category-title');

                if (!q) {
                    $items.removeClass('sbs-search-hidden');
                    $headings.removeClass('sbs-search-hidden');
                    $('#sbs-service-search-empty').hide();
                    return;
                }

                $items.each(function() {
                    const name = ($(this).data('service-name') || '').toString().toLowerCase();
                    const desc = ($(this).data('service-desc') || '').toString().toLowerCase();
                    const match = name.indexOf(q) !== -1 || desc.indexOf(q) !== -1;
                    $(this).toggleClass('sbs-search-hidden', !match);
                });

                $headings.each(function() {
                    const $visibleSiblings = $(this).nextUntil('.sbs-category-title', '.sbs-service-item').not('.sbs-search-hidden');
                    $(this).toggleClass('sbs-search-hidden', $visibleSiblings.length === 0);
                });

                const anyVisible = $items.not('.sbs-search-hidden').length > 0;
                $('#sbs-service-search-empty').toggle(!anyVisible);
            });
            $(document).on('click', '#sbs-service-search-clear', function() {
                $('#sbs-service-search').val('').trigger('input').trigger('focus');
            });

            // Keep the original 4.2.3 text-link appearance while reliably
            // expanding the complete description without inline JavaScript.
            $(document).on('click', '.sbs-read-more', function(e) {
                e.preventDefault();
                e.stopPropagation();
                const $button = $(this);
                const $wrap = $button.closest('.sbs-service-desc-wrap');
                const $short = $wrap.find('.sbs-desc-short').first();
                const $full = $wrap.find('.sbs-desc-full').first();
                if (!$short.length || !$full.length) return;
                const willExpand = $button.attr('aria-expanded') !== 'true';
                $short.prop('hidden', willExpand);
                $full.prop('hidden', !willExpand);
                $button.attr('aria-expanded', willExpand ? 'true' : 'false');
                $button.text(willExpand ? $button.data('show-less') : $button.data('read-more'));
            });

            // Category tabs - scroll to section
            $(document).on('click', '.sbs-cat-tab:not(.sbs-cat-more-btn)', function() {
                const slug = $(this).data('cat');
                if (!slug) return;
                const $heading = $('#cat-' + slug);
                if ($heading.length) {
                    const navH = $('.sbs-category-tabs-wrap').outerHeight() || 0;
                    $('html, body').animate({ scrollTop: $heading.offset().top - navH - 12 }, 350);
                }
                $('.sbs-cat-tab').removeClass('active');
                $('.sbs-cat-dropdown-item').removeClass('active');
                $('#sbs-cat-more-btn').removeClass('has-active');
                $(this).addClass('active');
            });

            // Highlight active tab while scrolling
            $(window).on('scroll.sbsTab', function() {
                if (!$('.sbs-category-tabs-wrap').is(':visible')) return;
                if ($('.sbs-category-tabs').attr('data-yatoon-active-category') === 'all') return;
                const navH = ($('.sbs-category-tabs-wrap').outerHeight() || 0) + 20;
                let current = null;
                $('.sbs-category-title').each(function() {
                    if ($(window).scrollTop() >= $(this).offset().top - navH) current = $(this).attr('id');
                });
                if (current) {
                    const slug = current.replace('cat-', '');
                    const $mainTab = $('.sbs-cat-tab[data-cat="' + slug + '"]:not(.sbs-cat-more-btn)');
                    const $dropItem = $('.sbs-cat-dropdown-item[data-cat="' + slug + '"]');
                    if ($mainTab.length && !$mainTab.hasClass('sbs-cat-overflow')) {
                        $('.sbs-cat-tab').removeClass('active');
                        $('.sbs-cat-dropdown-item').removeClass('active');
                        $('#sbs-cat-more-btn').removeClass('has-active');
                        $mainTab.addClass('active');
                    } else if ($dropItem.length) {
                        $('.sbs-cat-tab').removeClass('active');
                        $('.sbs-cat-dropdown-item').removeClass('active');
                        $dropItem.addClass('active');
                        $('#sbs-cat-more-btn').addClass('has-active');
                    }
                }
            });

            $(document).on('click', '#yatoon-ai-service-submit', function(e) {
                e.preventDefault();
                const ai = (typeof yatoonData !== 'undefined' && yatoonData.ai) ? yatoonData.ai : {};
                const $btn = $(this);
                const $need = $('#yatoon-ai-service-need');
                const $status = $('#yatoon-ai-service-status');
                const $result = $('#yatoon-ai-service-result');
                const $consent = $('#yatoon-ai-provider-consent');
                const need = ($need.val() || '').trim();

                if (!ai.serviceHelperEnabled || !ai.available) {
                    $status.text(ai.status || __('AI is not available yet.'));
                    return;
                }
                if (need.length < 3) {
                    $status.text(__('Tell us what you are looking for first.'));
                    $need.trigger('focus');
                    return;
                }
                if (!$consent.is(':checked')) {
                    $status.text(__('Please agree before sending your request to the configured third-party AI provider.'));
                    $consent.trigger('focus');
                    return;
                }

                $btn.prop('disabled', true).text(__('Finding matches...'));
                $status.text('');
                $result.hide().empty();
                const slowNotice = window.setTimeout(function() {
                    $status.text(__('Still working — checking the fastest available route...'));
                }, 4000);

                const finishRequest = function() {
                    window.clearTimeout(slowNotice);
                    $btn.prop('disabled', false).text(__('Find My Best Match'));
                };

                $.ajax({
                    url: yatoonData.ajax_url,
                    method: 'POST',
                    timeout: 8000,
                    data: {
                        action: 'yatoon_ai_consent_receipt',
                        nonce: yatoonData.nonce,
                        agreed: '1'
                    }
                }).done(function(consentResp) {
                    const receipt = consentResp && consentResp.success && consentResp.data ? consentResp.data.receipt : '';
                    if (!receipt) {
                        $status.text(consentResp && consentResp.data && consentResp.data.message
                            ? consentResp.data.message
                            : __('AI could not verify your consent. Please try again.'));
                        finishRequest();
                        return;
                    }
                    $.ajax({
                        url: yatoonData.ajax_url,
                        method: 'POST',
                        timeout: 16000,
                        data: {
                            action: 'yatoon_ai_service_recommendations',
                            nonce: yatoonData.nonce,
                            need: need,
                            ai_consent_receipt: receipt
                        }
                    }).done(function(resp) {
                        if (resp && resp.success && resp.data && resp.data.answer) {
                            window.clearTimeout(slowNotice);
                            $status.text('');
                            let recHtml = '';
                            const recs = Array.isArray(resp.data.recommendations) ? resp.data.recommendations : [];
                            if (recs.length) {
                                recHtml += '<div class="yatoon-ai-recommendation-actions">';
                                recHtml += '<strong>Recommended services</strong>';
                                recHtml += '<div class="yatoon-ai-recommendation-list">';
                                recs.forEach(function(rec) {
                                    const name = $('<div>').text(rec.name || __('Service')).html();
                                    const duration = parseInt(rec.duration, 10) || 0;
                                    const price = parseFloat(rec.price) || 0;
                                    const meta = [
                                        duration ? duration + ' min' : '',
                                        price ? ysFormatMoney(price).replace(/\.00$/, '') : ''
                                    ].filter(Boolean).join(' - ');
                                    const nextOpening = rec.next_opening ? $('<div>').text(rec.next_opening).html() : '';
                                    recHtml += '<button type="button" class="yatoon-ai-pick-service" data-service-id="' + parseInt(rec.id, 10) + '">';
                                    recHtml += '<span>' + name + '</span>';
                                    if (meta) recHtml += '<small>' + meta + '</small>';
                                    if (nextOpening) recHtml += '<small>Next: ' + nextOpening + '</small>';
                                    recHtml += '</button>';
                                });
                                recHtml += '</div>';
                                recHtml += '</div>';
                            }
                            if (resp.data.privacy_notice && !resp.data.local_only) {
                                recHtml += '<div class="yatoon-ai-privacy-notice">' + $('<div>').text(resp.data.privacy_notice).html() + '</div>';
                            }
                            $result.html(recHtml + resp.data.answer).show();
                        } else {
                            const msg = resp && resp.data && resp.data.message ? resp.data.message : __('AI could not help right now.');
                            $status.text(msg);
                        }
                    }).fail(function(xhr, textStatus) {
                        $status.text(textStatus === 'timeout'
                            ? __('AI is taking longer than expected. Please try once more or choose a service below.')
                            : __('AI could not help right now.'));
                    }).always(finishRequest);
                }).fail(function(xhr, textStatus) {
                    $status.text(textStatus === 'timeout'
                        ? __('AI could not verify your consent. Please try again.')
                        : __('AI could not help right now.'));
                    finishRequest();
                });
            });

            $(document).on('click', '.yatoon-ai-toggle', function(e) {
                e.preventDefault();
                const $helper = $(this).closest('.yatoon-ai-helper');
                $helper.removeClass('is-collapsed').addClass('is-expanded');
                $(this).attr('aria-expanded', 'true').hide();
            });

            $(document).on('click', '.yatoon-ai-collapse', function(e) {
                e.preventDefault();
                const $helper = $(this).closest('.yatoon-ai-helper');
                $helper.removeClass('is-expanded').addClass('is-collapsed');
                let $toggle = $helper.children('.yatoon-ai-toggle');
                if (!$toggle.length) {
                    const label = $.trim($helper.find('.yatoon-ai-helper-header strong').first().text()) || __('Need help choosing?');
                    $toggle = $('<button type="button" class="yatoon-ai-toggle" aria-expanded="false"></button>').text(label);
                    $helper.children('.yatoon-ai-helper-header').after($toggle);
                }
                $toggle.attr('aria-expanded', 'false').show();
            });

            $(document).on('click', '.yatoon-ai-pick-service', function(e) {
                e.preventDefault();
                const serviceId = parseInt($(this).data('service-id'), 10);
                if (!serviceId) return;

                const $service = $('.sbs-service-item[data-service-id="' + serviceId + '"]').first();
                if (!$service.length) {
                    $('#yatoon-ai-service-status').text(__('This recommended service is not visible on the menu right now.'));
                    return;
                }

                const navH = ($('.sbs-category-tabs-wrap').outerHeight() || 0) + 20;
                $('html, body').animate({ scrollTop: Math.max(0, $service.offset().top - navH - 12) }, 280, function() {
                    $service.trigger('click');
                    $service.addClass('yatoon-ai-picked-pulse');
                    setTimeout(function() { $service.removeClass('yatoon-ai-picked-pulse'); }, 900);
                });
            });

            // Step 1: Service selection
            let _serviceTouchMoved = false;
            let _lastServiceTap = 0;
            function _handleServiceSelection(e, trigger) {
                if (e && $(e.target).closest('a, .sbs-read-more, .sbs-service-readmore').length) return;
                const now = Date.now();
                if (now - _lastServiceTap < 260) {
                    if (e) e.preventDefault();
                    return;
                }
                _lastServiceTap = now;
                if (e) {
                    e.preventDefault();
                    e.stopPropagation();
                }
                self._abortLookupRequests();
                self._hardUnlockBookingSurface();

                const $item = $(trigger).hasClass('sbs-service-item') ? $(trigger) : $(trigger).closest('.sbs-service-item');
                $('.sbs-service-item, .sbs-service-card').removeClass('selected');
                $item.addClass('selected');
                
                // FIX: capture category from nearest heading for summary display
                const $heading = $item.prevAll('.sbs-category-title').first();
                self.selectedCategory = $heading.length ? $heading.text().trim() : null;
                
                const _newServiceName = ($item.data('service-name') || '').toLowerCase().trim();

                // Duplicate check at service selection using service ID + square_id + name fallback
                if (self.pendingServices.length > 0) {
                    const _newSvcId     = parseInt($item.data('service-id'));
                    const _newSquareId  = ($item.data('square-id') || '').trim();
                    const _newSvcName   = ($item.data('service-name') || '').toLowerCase().trim();
                    // The person currently being added for
                    const _currentPerson = self._nextGuestLabel || 'Me';
                    // Check main services by ID - only flag as duplicate if SAME person already has it
                    const _dupInMain = self.pendingServices.find(ps =>
                        parseInt(ps.service.id) === _newSvcId &&
                        (ps.guestLabel || 'Me') === _currentPerson
                    );
                    // Check options: by linked_service_id, square_id match, OR name similarity - same person only
                    const _dupInOpts = self.pendingServices.find(ps =>
                        (ps.guestLabel || 'Me') === _currentPerson &&
                        ps.options && ps.options.find(o => {
                            if (parseInt(o.linked_service_id) === _newSvcId) return true;
                            // Match by square_id if available on both sides
                            if (_newSquareId && o.square_id && _newSquareId === o.square_id) return true;
                            const oName = (o.name || '').toLowerCase().trim();
                            return !!(oName && _newSvcName && (oName.includes(_newSvcName) || _newSvcName.includes(oName)));
                        })
                    );
                    if (_dupInMain || _dupInOpts) {
                        const _svcName = self._esc($item.data('service-name'));
                        const _$savedItem = $item;
                        $('#sbs-dup-overlay').remove();
                        $('body').append(`
                            <div id="sbs-dup-overlay" role="dialog" aria-modal="true" aria-labelledby="sbs-dup-title" style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:99999;display:flex;align-items:center;justify-content:center;padding:20px;">
                                <div style="background:#fff;border-radius:16px;padding:28px 24px;max-width:380px;width:100%;text-align:center;box-shadow:0 20px 60px rgba(0,0,0,0.3);">
                                    <div style="font-size:32px;margin-bottom:12px;">!</div>
                                    <h3 id="sbs-dup-title" style="margin:0 0 8px;font-size:17px;font-weight:700;color:#111;">${__('You already added this service to your booking')}<br><em>${_svcName}</em></h3>
                                    <p style="margin:0 0 22px;font-size:14px;color:#6b7280;">${__('Would you like to add it again, or was that an accidental tap?')}</p>
                                    <div style="display:flex;gap:10px;justify-content:center;">
                                        <button type="button" id="sbs-dup-cancel" style="flex:1;padding:11px 0;border-radius:8px;border:1.5px solid #e5e7eb;background:#fff;font-size:14px;font-weight:600;cursor:pointer;color:#374151;">${__('Dismiss')}</button>
                                        <button type="button" id="sbs-dup-confirm" style="flex:1;padding:11px 0;border-radius:8px;border:none;background:#16a34a;color:#fff;font-size:14px;font-weight:600;cursor:pointer;">${__('Add Anyway')}</button>
                                    </div>
                                </div>
                            </div>
                        `);
                        // 4.6.2: this dialog declared aria-modal="true" but never
                        // trapped focus and never handled Escape, so Tab walked
                        // out into the live service list behind the scrim and a
                        // keyboard user had no way to dismiss it. Trap on open,
                        // release (and hand focus back to the service card that
                        // opened it) on every exit path.
                        self._dupDialogRelease = ysFocusTrap($('#sbs-dup-overlay'), function() {
                            self._closeDupDialog();
                        });
                        $('#sbs-dup-cancel').on('click', function() {
                            self._closeDupDialog();
                            // Just close - stay on service menu so client can continue browsing
                        });
                        $('#sbs-dup-confirm').on('click', function() {
                            // No focus restore here: this path leaves step 1, so
                            // putting focus back on the (about to be hidden)
                            // service card would just drop it on the floor.
                            self._closeDupDialog(false);
                            // Proceed: set service and go to Step 2 (Options)
                            self.selectedService = {
                                id:       _$savedItem.data('service-id'),
                                name:     _$savedItem.data('service-name'),
                                price:    parseFloat(_$savedItem.data('service-price')),
                                taxRate:  _$savedItem.attr('data-service-tax-rate') === '' ? null : parseFloat(_$savedItem.attr('data-service-tax-rate')),
                                duration: parseInt(_$savedItem.data('service-duration')),
                                description: _$savedItem.data('service-desc') || '',
                                image_url: _$savedItem.data('service-image') || '',
                                category: self.selectedCategory,
                                requirePolicy: parseInt(_$savedItem.data('require-policy')) === 1,
                                requireConsent: parseInt(_$savedItem.data('require-consent')) === 1,
                                allowStaffSelect: parseInt(_$savedItem.data('allow-staff-select')) !== 0,
                                allowDesignUpload: parseInt(_$savedItem.data('allow-design-upload')) === 1
                            };
                            $('#sbs-selected-service-id').val(self.selectedService.id);
                            self.selectedOptions = [];
                            self.selectedStaff = null;
                            self.selectedDateTime = null;
                            self._saveSession();
                            self.updateSummary();
                            self.loadServiceOptions();
                            self.goToStep(2);
                        });
                        return;
                    }
                }

                self.selectedService = {
                    id:       $item.data('service-id'),
                    name:     $item.data('service-name'),
                    price:    parseFloat($item.data('service-price')),
                    taxRate:  $item.attr('data-service-tax-rate') === '' ? null : parseFloat($item.attr('data-service-tax-rate')),
                    duration: parseInt($item.data('service-duration')),
                    description: $item.data('service-desc') || '',
                    image_url: $item.data('service-image') || '',
                    category: self.selectedCategory,
                    requirePolicy: parseInt($item.data('require-policy')) === 1,
                    requireConsent: parseInt($item.data('require-consent')) === 1,
                    allowStaffSelect: parseInt($item.data('allow-staff-select')) !== 0,
                    allowDesignUpload: parseInt($item.data('allow-design-upload')) === 1
                };
                
                $('#sbs-selected-service-id').val(self.selectedService.id);
                // FIX: clear options on new service selection
                self.selectedOptions = [];
                self.selectedStaff = null;
                self.selectedDateTime = null;
                
                self.updateSummary();
                self._updateMobileCartBar(); // only shows if cart has items

                self._saveSession();
                $('#sbs-guest-pick-banner').remove(); // clear guest banner if present
                self.loadServiceOptions();
            }
            $(document).on('click', '.sbs-service-item, .sbs-book-btn', function(e) {
                _handleServiceSelection(e, this);
            });
            $(document).on('touchstart', '.sbs-service-item, .sbs-book-btn', function() {
                _serviceTouchMoved = false;
            });
            $(document).on('touchmove', '.sbs-service-item, .sbs-book-btn', function() {
                _serviceTouchMoved = true;
            });
            $(document).on('touchend', '.sbs-service-item, .sbs-book-btn', function(e) {
                if (_serviceTouchMoved) return;
                _handleServiceSelection(e, this);
            });

            // Step 2: Options selection
            $(document).on('change', '.sbs-option-checkbox', function() {
                const optionItem = $(this).closest('.sbs-option-item');
                const groupEl    = optionItem.closest('.sbs-option-group');
                const isMulti    = groupEl.data('multi') === 1 || groupEl.data('multi') === '1';

                if ($(this).is(':checked')) {
                    // For main (single-select) groups: uncheck + deselect all siblings first
                    if (!isMulti) {
                        groupEl.find('.sbs-option-checkbox').not(this).each(function() {
                            const sibItem = $(this).closest('.sbs-option-item');
                            const sibId   = parseInt($(this).val());
                            $(this).prop('checked', false);
                            sibItem.removeClass('selected');
                            self.selectedOptions = self.selectedOptions.filter(o => o.id !== sibId);
                        });
                    }
                    optionItem.addClass('selected');
                    const newOptId = parseInt($(this).val());
                    // Guard: don't push duplicate (can happen in edit mode where options are pre-restored)
                    if (!self.selectedOptions.some(o => o.id === newOptId)) {
                        self.selectedOptions.push({
                            id:                newOptId,
                            name:              optionItem.find('.sbs-option-name').text(),
                            price:             parseFloat(optionItem.data('absolute-price')),
                            price_modifier:    parseFloat(optionItem.data('price')) || 0,
                            duration:          parseInt(optionItem.data('duration')),
                            group:             optionItem.data('group') || '',
                            is_addon:          optionItem.hasClass('sbs-option-addon'),
                            linked_service_id: parseInt(optionItem.data('linked-service-id')) || null,
                            allowDesignUpload: parseInt(optionItem.data('allow-design-upload')) === 1,
                            square_id:         (optionItem.data('square-id') || '').toString().trim()
                        });
                    }
                } else {
                    optionItem.removeClass('selected');
                    const optionId = parseInt($(this).val());
                    self.selectedOptions = self.selectedOptions.filter(opt => opt.id !== optionId);
                }
                $('#sbs-selected-options').val(JSON.stringify(self.selectedOptions.map(o => o.id)));
                if ((self.selectedOptions || []).some(function(o) { return !o.is_addon; })) {
                    self._clearMainOptionRequired();
                }
                self._updateOptionActionSummary();
                self._updateMobileCartBar();
                self._syncDesignUploadCard(true, optionItem);
                self._saveSession();

                // 4.6.2: auto-advance out of a single-choice options step.
                // Only when the customer just CHECKED something (unchecking is
                // not a decision to move on) and only when this step offers one
                // single-select group - see _isSingleChoiceOptionStep().
                //
                // Like the staff card above, this lives in the user event
                // handler. The edit-mode restore loop in
                // _renderServiceOptionsPayload() sets .prop('checked', true)
                // without triggering 'change', so re-entering step 2 with
                // options already chosen never reaches this code.
                if ($(this).is(':checked') && self._isSingleChoiceOptionStep()) {
                    self._scheduleAutoAdvance(2, function() {
                        if (self._blockWhileDesignUploadInProgress()) return;
                        const needsMain = !!self._optionHasMainGroup;
                        const hasMain = (self.selectedOptions || []).some(function(o) { return !o.is_addon; });
                        // Same precondition the Continue button enforces; if it
                        // is not met, leave the customer on the step rather than
                        // advancing into a half-configured service.
                        if (needsMain && !hasMain) return;
                        self.loadAvailableStaff();
                    });
                }
            });

            // Make the full option card tappable on mobile, not only the hidden checkbox/label.
            $(document).on('click', '.sbs-option-item', function(e) {
                if ($(e.target).is('input, label') || $(e.target).closest('label').length) return;
                const $cb = $(this).find('.sbs-option-checkbox').first();
                if ($cb.length) {
                    $cb.prop('checked', !$cb.prop('checked')).trigger('change');
                }
            });

            // Step 3: Staff selection
            $(document).on('click', '.sbs-staff-card', function() {
                self._selectStaffCard($(this));
                // 4.6.2: staff is always a single choice, so the tap that picks
                // a person IS the decision - Continue only confirmed what the
                // customer had already said. Advance after a beat so the
                // selected state is visible first.
                //
                // Scheduled here, in the CLICK handler, and deliberately not
                // inside _selectStaffCard(): _renderStaffList() calls that
                // function to restore a previous choice when the customer comes
                // Back to this step, and restoring a selection must never count
                // as making one.
                self._scheduleAutoAdvance(3, function() {
                    self._commitSelectedStaff();
                });
            });

            $(document).on('keydown', '.sbs-staff-card[role="button"], .sbs-time-slot[role="button"]', function(e) {
                if (e.key !== 'Enter' && e.key !== ' ') return;
                e.preventDefault();
                $(this).trigger('click');
            });

            // Time slot selection
            $(document).on('click', '.sbs-next-opening-chip', function() {
                const startAt = $(this).data('start-at');
                const $slot = $('.sbs-time-slot').filter(function() {
                    return String($(this).data('start-at')) === String(startAt);
                }).first();
                if ($slot.length) {
                    $slot.trigger('click');
                    const slotTop = Math.max(0, $slot.offset().top - 140);
                    $('html, body').animate({ scrollTop: slotTop }, 180);
                }
            });

            $(document).on('click', '.sbs-time-slot', function() {
                // Hard-blocked slots (all eligible staff busy) - not selectable
                if ($(this).hasClass('sbs-time-slot-unavail')) {
                    self._showToast(__('No staff are available at this time. Please choose a different time.'), 'error');
                    return;
                }
                // Busy slots: warn but allow selection only if another staff can cover the service
                if ($(this).hasClass('sbs-time-slot-busy')) {
                    const busyIdsRaw   = $(this).data('busy-staff-ids');
                    const busyStaffIds = busyIdsRaw ? String(busyIdsRaw).split(',').map(Number).filter(Boolean) : [];

                    // For each conflicting service, check if another eligible staff member exists.
                    // If the service can ONLY be done by the busy staff member ->hard block.
                    const canSwitch  = []; // { ps, name } - services that can be reassigned
                    const cantSwitch = []; // name - services with only one possible staff

                    if (busyStaffIds.length > 0 && self.pendingServices.length > 0) {
                        self.pendingServices.forEach(function(ps) {
                            if (!ps.staff || !busyStaffIds.includes(Number(ps.staff.id))) return;
                            const name      = self.getFirstName(ps.staff.name);
                            const eligible  = ps.eligibleStaffIds || [];
                            // Other eligible staff = eligible minus the busy one
                            const otherFree = eligible.filter(sid => !busyStaffIds.includes(sid));
                            if (otherFree.length > 0) {
                                canSwitch.push({ ps: ps, name: name });
                            } else {
                                // Only this staff can do the service - cannot reassign
                                cantSwitch.push(name);
                            }
                        });
                    }

                    if (cantSwitch.length > 0) {
                        // At least one service has no substitute - this time slot is not viable
                        const svcName = self.pendingServices.find(ps =>
                            ps.staff && busyStaffIds.includes(Number(ps.staff.id))
                        );
                        const svcLabel = (svcName && svcName.service) ? svcName.service.name : 'this service';
                        self._showToast(
                            cantSwitch.join(', ') + ' is busy at this time AND is the only staff who can do "' + svcLabel + '".\n\n' +
                            __('Please choose a different time.'),
                            'error'
                        );
                        return;
                    }

                    if (canSwitch.length > 0) {
                        const names = canSwitch.map(x => x.name).join(', ');
                        if (!confirm(
                            names + ' is busy at this time (already in Appointment 1).\n\n' +
                            'Tap OK to continue - the system will assign another available staff member.\n\n' +
                            __('Tap Cancel to choose a different time.')
                        )) return;
                        // Switch to Any staff only for services that have alternatives
                        canSwitch.forEach(function(x) {
                            x.ps.staff = { id: 0, name: __('Any staff') };
                        });
                        self._showPendingServicesSummary();
                        // fall through to normal slot selection below
                    } else {
                        self._showToast(__('No alternative staff available. Please choose a different time.'), 'error');
                        return;
                    }
                }
                $('.sbs-time-slot').removeClass('selected');
                $(this).addClass('selected');
                const selectedDate = $('.sbs-day-button.selected').data('date') || $('#sbs-date-picker').val();
                const clickedStartAt = $(this).data('start-at');

                // -- Staff time-overlap guard -------------------------------------
                // Check that the new appointment doesn't overlap with an existing cart
                // item for the SAME named staff member (not "Any staff").
                //
                // Overlap condition:  newStart < existingEnd  AND  newEnd > existingStart
                //
                // "Any staff" (id=0) is skipped here because the backend/Square handles
                // those conflicts - we can only check named staff client-side.
                // -----------------------------------------------------------------

                // Build the new appointment's named staff IDs and total duration
                const newStaffIds = self.pendingServices.length > 0
                    ? self.pendingServices.map(s => s.staff ? s.staff.id : 0)
                    : (self.selectedStaff ? [self.selectedStaff.id] : [0]);

                // -- Compute new appointment's total duration ---------------------
                // Use a minimum of 1 minute so that exact same-time checks still work
                const newDuration = Math.max(1, (function() {
                    if (self.pendingServices.length > 0) {
                        return self._durationByGuest(self.pendingServices).totalDuration;
                    }
                    const opts = self.selectedOptions || [];
                    const fromOpts = opts.reduce((sum, o) => sum + (parseInt(o.duration) || 0), 0);
                    return fromOpts || parseInt((self.selectedService || {}).duration || 0);
                })());

                const newStartMs = new Date(clickedStartAt).getTime();
                const newEndMs   = newStartMs + newDuration * 60000;

                let conflictMsg = null;
                for (const cartItem of self.bookingCart) {
                    if (!cartItem.dateTime || !cartItem.dateTime.start_at) continue;

                    // Named staff in this cart item - each service can have its own staff
                    const cartServices = cartItem.services
                        || [{ service: cartItem.service, options: cartItem.options || [], staff: cartItem.staff }];
                    const cartStaffIds = cartServices.map(s => s.staff ? s.staff.id : 0);

                    // Only flag named staff conflicts (id !== 0)
                    const namedOverlapIds = newStaffIds.filter(id => id !== 0 && cartStaffIds.includes(id));
                    if (namedOverlapIds.length === 0) continue;

                    // Compute cart item's time window
                    // Use minimum 1 min so same-start-time is always caught
                    const cartStartMs  = new Date(cartItem.dateTime.start_at).getTime();
                    const cartDuration = Math.max(1, self.getItemDuration(cartItem));
                    const cartEndMs    = cartStartMs + cartDuration * 60000;

                    // Overlap: intervals [newStart, newEnd) and [cartStart, cartEnd) intersect
                    // Use <= for end comparisons so same start time is always a conflict
                    if (newStartMs < cartEndMs && newEndMs > cartStartMs) {
                        const staffName = cartServices
                            .filter(s => s.staff && namedOverlapIds.includes(s.staff.id))
                            .map(s => self.getFirstName(s.staff.name))[0] || __('A staff member');
                        // Salon time, not the visitor's - this message tells the
                        // customer which time to pick next.
                        const cartEndTime = self._siteFmtTime(new Date(cartEndMs));
                        conflictMsg = `${staffName} is already booked until ${cartEndTime} in Appointment ${self.bookingCart.indexOf(cartItem) + 1}. Please choose a time after ${cartEndTime}.`;
                        break;
                    }
                }
                if (conflictMsg) {
                    self._showToast(conflictMsg, 'error');
                    return;
                }
                // ----------------------------------------------------------------

                const staffMapRaw = $(this).data('staff-map');
                self.selectedDateTime = {
                    date:      selectedDate,
                    time:      $(this).data('time'),
                    display:   $(this).find('.sbs-slot-time').first().text() || $(this).data('time'),
                    start_at:  clickedStartAt,
                    staff_map: staffMapRaw || null   // {s_idx: sq_staff_id} from Square real-time query
                };
                $('#sbs-selected-start-at').val(self.selectedDateTime.start_at);
                self._saveSession();
                self.addToCart();
                self.goToStep(5);
            });

            // Navigation
            $(document).on('click', '.sbs-btn-back', function() {
                const prevStep = self.currentStep - 1;

                // -- Back from step 5 (checkout) ->step 4 (time picker) -------
                // addToCart() already moved pendingServices into bookingCart and
                // cleared everything. We need to pop the last cart item back out
                // so the time picker knows which service/staff to query.
                if (self.currentStep === 5 && self.bookingCart.length > 0) {
                    const last = self.bookingCart.pop();
                    self.pendingServices  = last.services ? [...last.services] : [];
                    self.selectedDateTime = null;
                    self._saveSession();
                    self._showPendingServicesSummary();
                    self.goToStep(4);
                    return;
                }

                // -- Back from step 4 (time picker) ---------------------------
                if (self.currentStep === 4) {
                    if (self.pendingServices.length > 0) {
                        self._backFromTimeToStaff();
                        return;
                    } else if (false && self.bookingCart.length > 0) {
                        // Services in pendingServices - user just wants a different
                        // time. Stay on step 4 and reload slots for current date.
                        self.selectedDateTime = null;
                        self._saveSession();
                        const $slots = $('.sbs-time-slots');
                        $slots.html('<div class="sbs-loading"><span class="spinner"></span> ' + __('Loading available times...') + '</div>');
                        const $selectedBtn = $('.sbs-day-button.selected:not(.disabled)');
                        if ($selectedBtn.length) {
                            const date = $selectedBtn.data('date');
                            if (date) self.loadAvailableTimes(date, true);
                        }
                        self._updateMobileCartBar();
                        return;
                    } else if (self.bookingCart.length > 0) {
                        // Cart has items but pendingServices is empty - pop last
                        // cart item back so time picker can reload correctly.
                        const last = self.bookingCart.pop();
                        self.pendingServices  = last.services ? [...last.services] : [];
                        self.selectedDateTime = null;
                        self._saveSession();
                        self._showPendingServicesSummary();
                        const $slots = $('.sbs-time-slots');
                        $slots.html('<div class="sbs-loading"><span class="spinner"></span> ' + __('Loading available times...') + '</div>');
                        const $selectedBtn = $('.sbs-day-button.selected:not(.disabled)');
                        if ($selectedBtn.length) {
                            const date = $selectedBtn.data('date');
                            if (date) self.loadAvailableTimes(date, true);
                        }
                        self._updateMobileCartBar();
                        return;
                    } else {
                        // Nothing selected at all - go back to step 3
                        self.goToStep(prevStep);
                        return;
                    }
                }

                self.goToStep(prevStep);
            });
	            $(document).on('click', '.sbs-btn-next, #sbs-sidebar-next-btn', function(e) {
	                e.preventDefault();
	                if ($(this).prop('disabled')) return;
                if (self._blockWhileDesignUploadInProgress()) return;
                const isCheckoutStepActive = self.currentStep === 5 || $('.sbs-step-5').hasClass('active');
                const isSidebarConfirm = this.id === 'sbs-sidebar-next-btn'
                    && /confirm booking|book now/i.test($.trim($(this).text()))
                    && isCheckoutStepActive;
                if (isCheckoutStepActive || isSidebarConfirm) {
                    self._requestCustomerFormSubmit();
                } else if (self.currentStep === 4 && self.selectedDateTime) {
                    self.goToStep(5);
                } else if (self.currentStep === 1 && self.pendingServices.length > 0) {
                    if (self._isGroupAppointmentFlow()) self._showGuestReviewScreen();
                    else self.goToStep(4, true);
                } else if (self.currentStep === 2) {
                    const needsMain = !!self._optionHasMainGroup;
                    const hasMain = (self.selectedOptions || []).some(function(o) { return !o.is_addon; });
                    if (needsMain && !hasMain) {
                        self._showToast(__('Please choose one main option before continuing.'), 'error');
                        self._showMainOptionRequired();
                        return;
                    }
                    self.loadAvailableStaff();
                } else if (self.currentStep === 3) {
                    self._commitSelectedStaff();
                }
            });
            
            // "Add Service for [Person]" in step 4 - go back to step 1, tag with person label
            $(document).on('click', '.sbs-add-service-to-appt', function() {
                const personLabel = $(this).data('person-label') || null;
                self._enterServiceSelection(personLabel, true);
            });

            $(document).on('click', '.sbs-review-services-btn', function(e) {
                e.preventDefault();
                if (self.pendingServices && self.pendingServices.length > 0) {
                    self._showGuestReviewScreen();
                    return;
                }
                self._enterServiceSelection(null, true);
            });

            // "Add Guest" button in step 4 banner
            $(document).on('click', '#sbs-banner-add-guest', function(e) {
                e.preventDefault();
                self._addGuestTogether();
            });

            // "Add Another Appointment" in step 5 - fully new appointment, clear pending
            $(document).on('click', '.sbs-add-more-service', function() {
                // Block if single-appointment mode is enabled
                if (yatoonData.multiApptEnabled === false) return;
                const MAX_APPOINTMENTS = 2;
                if (self.bookingCart.length >= MAX_APPOINTMENTS) {
                    // Show limit reached dialog
                    const $overlay = $(`
                        <div style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px;">
                            <div style="background:#fff;border-radius:16px;padding:28px 24px;max-width:360px;width:100%;text-align:center;box-shadow:0 20px 60px rgba(0,0,0,0.3);">
                                <div style="font-size:36px;margin-bottom:12px;">!</div>
                                <h3 style="margin:0 0 8px;font-size:18px;font-weight:700;color:#111;">Maximum 2 Appointments</h3>
                                <p style="color:#6b7280;font-size:14px;margin:0 0 20px;line-height:1.5;">You can book up to 2 appointments at a time. Please complete your current booking first, then book again for more.</p>
                                <button type="button" id="sbs-limit-ok" style="background:#ec4899;color:#fff;border:none;border-radius:8px;padding:12px 32px;font-size:15px;font-weight:600;cursor:pointer;width:100%;">Got it</button>
                            </div>
                        </div>`);
                    $('body').append($overlay);
                    $overlay.on('click', '#sbs-limit-ok', function() { $overlay.remove(); });
                    return;
                }
                self.pendingServices  = [];
                self._enterServiceSelection(null, true);
            });
            
            // FIX: Summary remove (X) buttons in sidebar
            $(document).on('click', '.sbs-summary-remove-btn', function() {
                const action = $(this).data('remove');
                self._handleSummaryRemove(action);
            });
            
            // Cart item remove/edit - with confirmation dialog
            $(document).on('click', '.sbs-cart-remove', function() {
                const index = parseInt($(this).data('index'));
                self._confirmRemove(index, false);
            });
            $(document).on('click', '.sbs-cart-edit', function() {
                const index = parseInt($(this).data('index'));
                self.editCartItem(index);
            });
            $(document).on('click', '#sbs-sidebar-edit-cart', function() {
                if (self.bookingCart.length > 0) self.goToStep(5);
            });

            $(document).on('change', '#sbs-design-reference', function() {
                const input = this;
                const file = this.files && this.files[0] ? this.files[0] : null;
                const $preview = $('#sbs-design-preview').empty();
                self.designUploadAttachmentId = 0;
                self.designUploadToken = '';
                self.designUploadInProgress = false;
                self.designUploadImageUrl = '';
                self.designUploadFileName = file ? String(file.name || '') : '';
                if (!file) {
                    $preview.hide();
                    return;
                }
                if (!/^image\//.test(file.type || '')) {
                    self._showToast(__('Please choose an image file for the design reference.'), 'error');
                    this.value = '';
                    $preview.hide();
                    return;
                }
                if (file.size > 10 * 1024 * 1024) {
                    self._showToast(__('Please choose a design image smaller than 10 MB.'), 'error');
                    this.value = '';
                    $preview.hide();
                    return;
                }
                $preview.html(
                    '<img src="" alt="">' +
                    '<span class="yatoon-design-file-name" title="' + self._esc(file.name) + '">' + self._esc(file.name) + '</span>' +
                    '<span class="yatoon-design-upload-status is-uploading" role="status" aria-live="polite">Uploading...</span>'
                ).show();
                const reader = new FileReader();
                reader.onload = function(e) {
                    $preview.find('img').attr('src', e.target.result);
                };
                reader.readAsDataURL(file);

                const ajaxUrl = (typeof yatoonData !== 'undefined' && (yatoonData.ajax_url || yatoonData.ajaxUrl))
                    ? (yatoonData.ajax_url || yatoonData.ajaxUrl)
                    : '';
                if (!ajaxUrl) {
                    $preview.find('.yatoon-design-upload-status')
                        .removeClass('is-uploading').addClass('is-error')
                        .text(__('Upload could not start. Choose the photo again.'));
                    return;
                }
                self.designUploadInProgress = true;
                const uploadData = new FormData();
                uploadData.append('action', 'yatoon_upload_design_reference');
                uploadData.append('nonce', yatoonData.nonce || '');
                uploadData.append('design_reference', file);
                // 4.6.1: tell the server which service/options this photo is
                // for so it can verify they actually accept reference photos.
                const designServiceId = (self.selectedService && self.selectedService.id) ? self.selectedService.id : 0;
                if (designServiceId) {
                    uploadData.append('service_id', designServiceId);
                }
                (self.selectedOptions || []).forEach(function (option) {
                    if (option && option.id) {
                        uploadData.append('option_ids[]', option.id);
                    }
                });
                $.ajax({
                    url: ajaxUrl,
                    type: 'POST',
                    data: uploadData,
                    processData: false,
                    contentType: false,
                    timeout: 45000
                }).done(function(resp) {
                    if (input.files && input.files[0] !== file) return;
                    self.designUploadInProgress = false;
                    if (resp && resp.success && resp.data && resp.data.design_attachment_id) {
                        self.designUploadAttachmentId = parseInt(resp.data.design_attachment_id, 10) || 0;
                        self.designUploadToken = String(resp.data.design_upload_token || '');
                        self.designUploadImageUrl = String(resp.data.design_image_url || '');
                        $preview.find('.yatoon-design-upload-status')
                            .removeClass('is-uploading is-error').addClass('is-success')
                            .text(__('Uploaded'));
                        return;
                    }
                    const message = resp && resp.data && resp.data.message
                        ? resp.data.message
                        : __('Upload failed. Choose the photo again to retry.');
                    $preview.find('.yatoon-design-upload-status')
                        .removeClass('is-uploading').addClass('is-error')
                        .text(message);
                }).fail(function() {
                    if (input.files && input.files[0] !== file) return;
                    self.designUploadInProgress = false;
                    $preview.find('.yatoon-design-upload-status')
                        .removeClass('is-uploading').addClass('is-error')
                        .text(__('Upload failed. Choose the photo again to retry.'));
                });
            });

            // Form submission
            $(document).on('submit', '#sbs-customer-form', function(e) {
                e.preventDefault();
                self.submitBooking();
            });
            $(document).on('click', '.sbs-btn-submit', function(e) {
                e.preventDefault();
                self._requestCustomerFormSubmit();
            });

            $(document).on('click', '.sbs-btn-new-booking', function() {
                self.reset();
            });

            $(document).on('click', '.sbs-success-book-again', function() {
                const bookUrl = (window.yatoonData && yatoonData.bookNewUrl) ? yatoonData.bookNewUrl : '';
                if (bookUrl) {
                    window.location.href = bookUrl;
                    return;
                }
                self.reset();
                self._scrollToFormTop();
            });
        }
        
        // FIX: handle summary sidebar remove (X) buttons
        _handleSummaryRemove(action) {
            if (action === 'service') {
                this.selectedService = null;
                this.selectedCategory = null;
                this.selectedOptions = [];
                this.selectedStaff = null;
                this.selectedDateTime = null;
                $('#sbs-selected-service-id, #sbs-selected-staff-id, #sbs-selected-start-at, #sbs-selected-options').val('');
                $('.sbs-service-item, .sbs-service-card').removeClass('selected');
                this._saveSession();
                this.goToStep(1);
            } else if (action === 'options') {
                this.selectedOptions = [];
                $('#sbs-selected-options').val('');
                this.updateSummary();
            } else if (action === 'staff') {
                this.selectedStaff = null;
                this.selectedDateTime = null;
                this.goToStep(3);
                this.loadAvailableStaff();
            } else if (action === 'datetime') {
                this.selectedDateTime = null;
                $('#sbs-selected-start-at').val('');
                this.goToStep(4);
            }
        }

        _prepareForServiceSelection() {
            this._closeDrawer();
            this._hardUnlockBookingSurface();
            $('.sbs-service-item, .sbs-service-card').removeClass('selected');
            this.selectedService  = null;
            this.selectedCategory = null;
            this.selectedOptions  = [];
            this.selectedStaff    = null;
            this.selectedDateTime = null;
            $('#sbs-selected-service-id, #sbs-selected-staff-id, #sbs-selected-start-at, #sbs-selected-options').val('');
        }

        _enterServiceSelection(personLabel, scrollToTop = true) {
            this._nextGuestLabel = personLabel || null;
            this._abortLookupRequests();
            this._hardUnlockBookingSurface();
            $('.sbs-service-item, .sbs-service-card').removeClass('selected');
            this.selectedService  = null;
            this.selectedCategory = null;
            this.selectedOptions  = [];
            this.selectedStaff    = null;
            this.selectedDateTime = null;
            $('#sbs-selected-service-id, #sbs-selected-staff-id, #sbs-selected-start-at, #sbs-selected-options').val('');
            this.currentStep = 1;
            this._saveSession();
            this.goToStep(1, scrollToTop);
            if (this._nextGuestLabel && this._nextGuestLabel !== 'Me') {
                this._showGuestPickBanner(this._nextGuestLabel);
            } else if (this.pendingServices && this.pendingServices.length > 0) {
                // Already have services (e.g. adding more for Me) - show Me banner
                this._showMeBanner(personLabel || 'Me');
            } else {
                $('#sbs-guest-pick-banner').remove();
            }
            // Hide the appointment-type selector once user is actively adding services
            // (i.e. has pending services already, or is adding for a specific person)
            const hasPending = this.pendingServices && this.pendingServices.length > 0;
            const isAddingForPerson = !!this._nextGuestLabel;
            if (hasPending || isAddingForPerson) {
                $('#sbs-appt-type-wrap').hide();
            } else {
                $('#sbs-appt-type-wrap').show();
            }
            this.updateSummary();
            this._updateMobileCartBar();
            setTimeout(() => this._hardUnlockBookingSurface(), 0);
            setTimeout(() => this._hardUnlockBookingSurface(), 180);
        }

        _showMeBanner(label) {
            const self = this;
            $('#sbs-guest-pick-banner').remove();
            const displayLabel = label === 'Me' ? 'yourself' : label;
            const $banner = $(`
                <div id="sbs-guest-pick-banner" class="sbs-guest-pick-banner" style="
                    background:#eff6ff;border:1.5px solid #3b82f6;border-radius:12px;
                    padding:14px 16px;margin:0 0 18px;box-sizing:border-box;min-height:68px;overflow:visible;
                    display:flex;align-items:center;justify-content:space-between;line-height:1.3;">
                    <span style="font-size:14px;font-weight:600;color:#1d4ed8;">
                        &#128100; Adding service for <strong>${displayLabel}</strong>
                    </span>
                    <button type="button" id="sbs-guest-banner-cancel" style="
                        background:none;border:none;color:#6b7280;font-size:13px;cursor:pointer;">
                        Cancel
                    </button>
                </div>
            `);
            // Prepend immediately - guard against double-fire
            if (!$('#sbs-guest-pick-banner').length) {
                $('.sbs-step-1').prepend($banner);
            }
            $('#sbs-guest-banner-cancel').on('click', function() {
                $('#sbs-guest-pick-banner').remove();
                self._nextGuestLabel = null;
                self._saveSession();
            });
        }

        /**
         * 4.6.2: single exit point for the duplicate-service dialog.
         *
         * Every close path previously just did $('#sbs-dup-overlay').remove(),
         * which now has to be paired with releasing the focus trap - otherwise
         * the document-level Tab/Escape handler would survive the dialog and
         * keep swallowing keystrokes for the rest of the session.
         *
         * @param {boolean} restoreFocus Pass false when the caller is about to
         *                               navigate away from the opener anyway.
         */
        _closeDupDialog(restoreFocus) {
            $('#sbs-dup-overlay').remove();
            const release = this._dupDialogRelease;
            this._dupDialogRelease = null;
            if (typeof release !== 'function') return;
            if (restoreFocus === false) {
                // Drop the key handler without moving focus.
                $(document).off('keydown.ysFocusTrap');
                return;
            }
            release();
        }

        _hardUnlockBookingSurface() {
            // 4.6.2: release the dup-dialog focus trap too. This method is the
            // "get me out of any stuck modal state" hatch, and a leaked trap is
            // exactly the kind of stuck state it exists to clear.
            this._closeDupDialog(false);
            $('#sbs-add-more-overlay, #sbs-dup-overlay, #sbs-confirm-overlay').remove();
            $('#sbs-drawer-backdrop').removeClass('open').css('display', '');
            $('#sbs-mobile-drawer').removeClass('open').css('display', 'none');
            $('#sbs-cat-more-dropdown').removeClass('open').css({ top: '', left: '', width: '' });
            const $moreButton = $('#sbs-cat-more-btn');
            $moreButton.text(($moreButton.attr('data-selected-label') || 'More') + ' ⌄').attr('aria-expanded', 'false');
            $('.sbs-form-step').css('pointer-events', '');
            $('body, html, .sbs-booking-container, .sbs-form-step, .sbs-services-list').css('pointer-events', '');
        }

        _backFromTimeToStaff() {
            this.selectedDateTime = null;
            $('#sbs-selected-start-at').val('');

            if (this.pendingServices.length > 0) {
                const current = this.pendingServices.pop();
                this.selectedService = current.service ? { ...current.service } : null;
                this.selectedOptions = current.options ? [...current.options] : [];
                this.selectedStaff = current.staff ? { ...current.staff } : null;
                this.designUploadAttachmentId = parseInt(current.designAttachmentId || 0, 10) || 0;
                this.designUploadToken = String(current.designUploadToken || '');
                this.designUploadImageUrl = String(current.designImageUrl || '');
                this.designUploadFileName = String(current.designFileName || '');
                this._nextGuestLabel = current.guestLabel && current.guestLabel !== 'Me' ? current.guestLabel : null;
                if (this.selectedService) $('#sbs-selected-service-id').val(this.selectedService.id);
                if (this.selectedStaff) $('#sbs-selected-staff-id').val(this.selectedStaff.id);
                $('#sbs-selected-options').val(JSON.stringify(this.selectedOptions.map(o => o.id)));
            }

            if (!this.selectedService) return false;

            this._saveSession();
            this.updateSummary();
            this.goToStep(3);
            this.loadAvailableStaff();
            return true;
        }

        _selectStaffCard($card) {
            if (!$card || !$card.length) return;
            $('.sbs-staff-card').removeClass('selected').attr('aria-pressed', 'false');
            $card.addClass('selected').attr('aria-pressed', 'true');
            this.selectedStaff = {
                id:   $card.data('staff-id'),
                name: $card.data('staff-name'),
                price_override: $card.attr('data-price-override') === '' || typeof $card.attr('data-price-override') === 'undefined'
                    ? null
                    : Number($card.attr('data-price-override'))
            };
            this._applyStaffPrice();
            $('#sbs-selected-staff-id').val(this.selectedStaff.id);
            this.updateSummary();
            this._updateMobileCartBar();
            this._saveSession();
        }

        _applyStaffPrice() {
            if (!this.selectedService) return;
            if (typeof this.selectedService._basePrice === 'undefined') {
                this.selectedService._basePrice = Number(this.selectedService.price || 0);
            }
            const override = this.selectedStaff && this.selectedStaff.price_override;
            this.selectedService.price = override !== null && typeof override !== 'undefined' && !Number.isNaN(Number(override))
                ? Number(override)
                : Number(this.selectedService._basePrice || 0);
        }

        _commitSelectedStaff() {
            if (this._blockWhileDesignUploadInProgress()) return;
            if (!this.selectedService) {
                if (this.pendingServices.length > 0) {
                    this.goToStep(4, true);
                    return;
                }
                this._showToast(__('Please choose a service first.'), 'error');
                this.goToStep(1, true);
                return;
            }

            if (!this.selectedStaff) {
                const $selected = $('.sbs-staff-card.selected').first();
                if ($selected.length) {
                    this._selectStaffCard($selected);
                } else {
                    this._showToast(__('Please choose a staff member to continue.'), 'error');
                    return;
                }
            }

            const eligible = (this.selectedService && this.selectedService._eligibleStaffIds) || [];
            const label = this._nextGuestLabel || 'Me';
            const groupAppointment = this._isGroupAppointmentFlow();
            this.pendingServices.push({
                service:          { ...this.selectedService },
                options:          [...this.selectedOptions],
                staff:            { ...this.selectedStaff },
                eligibleStaffIds: eligible,
                guestLabel:       label,
                groupAppointment: groupAppointment,
                designAttachmentId: this.designUploadAttachmentId || 0,
                designUploadToken: this.designUploadToken || '',
                designImageUrl: this.designUploadImageUrl || '',
                designFileName: this.designUploadFileName || ''
            });

            this.designUploadAttachmentId = 0;
            this.designUploadToken = '';
            this.designUploadImageUrl = '';
            this.designUploadFileName = '';
            const designInput = document.getElementById('sbs-design-reference');
            if (designInput) designInput.value = '';
            $('#sbs-design-preview').empty().hide();

            this._nextGuestLabel = null;
            if (this.selectedService && !this._staffCache[this.selectedService.id]) {
                this._staffCache[this.selectedService.id] = [];
            }

            this.selectedService  = null;
            this.selectedCategory = null;
            this.selectedOptions  = [];
            this.selectedStaff    = null;
            this.selectedDateTime = null;
            $('#sbs-selected-service-id, #sbs-selected-staff-id, #sbs-selected-start-at, #sbs-selected-options').val('');

            this._saveSession();
            if (this._isGroupAppointmentFlow()) {
                this._showGuestReviewScreen();
            } else {
                this._showPendingServicesSummary();
                this.goToStep(4, true);
            }
        }

        // -- Fresha-style "Add guests and services" review screen -----------------
        // Shown after every service selection. Lets users see who has what service,
        // add more services per person, add guests, or continue to date/time.
        _showGuestReviewScreen() {
            const self = this;
            if (!this.pendingServices || !this.pendingServices.length) {
                this.goToStep(4, true);
                return;
            }

            const maxGuests = this._maxGuestsTogether();

            // Group services by person
            const personGroups = {};
            const personOrder  = [];
            this.pendingServices.forEach(function(s) {
                const lbl = s.guestLabel || 'Me';
                if (!personGroups[lbl]) { personGroups[lbl] = []; personOrder.push(lbl); }
                personGroups[lbl].push(s);
            });

            const totalPrice    = this.pendingServices.reduce((sum, s) => {
                const p = s.options && s.options.length > 0
                    ? s.options.reduce((t, o) => t + (parseFloat(o.price) || 0), 0)
                    : (parseFloat(s.service && s.service.price) || 0);
                return sum + p;
            }, 0);
            const totalDuration = this._durationByGuest(this.pendingServices).totalDuration;
            const totalServices = this.pendingServices.length;

            // Avatar colors for people
            const avatarColors = [
                { bg: 'var(--y4-primary)', text: '#fff' },
                { bg: 'var(--y4-primary-soft)', text: 'var(--y4-primary-strong)' },
                { bg: '#f2f4f7', text: '#344054' },
            ];

            // Build person rows
            const personRows = personOrder.map(function(lbl, personIdx) {
                const services  = personGroups[lbl];
                const isMe      = lbl === 'Me';
                const displayLbl = self._displayGuestLabel(lbl);
                const safeDisplayLbl = self._esc(displayLbl);
                const safeDataLbl = self._esc(lbl);
                const initial   = isMe ? (yatoonData.clientInitial || 'Y') : displayLbl.charAt(0).toUpperCase();
                const safeInitial = self._esc(initial);
                const addServiceFor = lbl === 'Me' ? 'me' : displayLbl;
                const color     = avatarColors[personIdx % avatarColors.length];
                const guestIdx  = isMe ? 0 : (personOrder.indexOf(lbl));

                const svcLines = services.map(function(s, svcIdx) {
                    const svcName = self._svcDisplayLabelHtml(s.service, s.options);
                    const staffName = s.staff ? s.staff.name : '';
                    const dur = self._serviceDuration(s);
                    const idx = self.pendingServices.indexOf(s);
                    return `<div class="sbs-gr-svc-row" style="display:flex;align-items:center;justify-content:space-between;gap:8px;padding:6px 0;${svcIdx < services.length - 1 ? 'border-bottom:1px solid #f3f4f6;' : ''}">
                        <div style="flex:1;min-width:0;">
                            <div style="font-size:14px;font-weight:500;color:#1a1a1a;line-height:1.3;">${svcName}</div>
                            <div style="font-size:12px;color:#9ca3af;margin-top:2px;">${dur} min${staffName ? ' &bull; ' + self.getFirstName(staffName) : ''}${s.designAttachmentId ? ' &bull; Reference photo uploaded' : ''}</div>
                        </div>
                        <button type="button" class="sbs-gr-remove-svc" data-idx="${idx}"
                            style="background:none;border:none;color:#d1d5db;font-size:18px;cursor:pointer;padding:4px;line-height:1;flex-shrink:0;"
                            aria-label="${__('Remove service')}" title="${__('Remove service')}">&times;</button>
                    </div>`;
                }).join('');

                return `<div class="sbs-gr-person-row" style="background:#fff;border:1px solid #f3f4f6;border-radius:14px;padding:14px 16px;display:flex;align-items:flex-start;gap:14px;">
                    <div style="flex-shrink:0;width:42px;height:42px;border-radius:50%;background:${color.bg};display:flex;align-items:center;justify-content:center;font-weight:700;font-size:16px;color:${color.text};">
                        ${safeInitial}
                    </div>
                    <div style="flex:1;min-width:0;">
                        <div style="font-weight:700;font-size:15px;color:#1a1a1a;margin-bottom:8px;">${lbl === 'Me' ? self._esc(yatoonData.clientName || 'Me') : safeDisplayLbl}</div>
                        <div class="sbs-gr-svc-list">${svcLines}</div>
                        <button type="button" class="sbs-gr-add-svc sbs-add-service-to-appt" data-person-label="${safeDataLbl}"
                            style="margin-top:10px;background:#fafafa;border:1.5px dashed #a7b0bf;border-radius:8px;padding:7px 14px;font-size:13px;color:#374151;font-weight:700;cursor:pointer;width:100%;text-align:left;">
                            Add service for ${self._esc(addServiceFor)}
                        </button>
                    </div>
                    ${personOrder.length > 1 ? `<button type="button" class="sbs-gr-remove-person" data-person="${safeDataLbl}"
                        style="background:none;border:none;color:#9ca3af;font-size:20px;cursor:pointer;padding:0 0 0 4px;line-height:1;flex-shrink:0;" title="Remove ${safeDisplayLbl}">&times;</button>` : ''}
                </div>`;
            }).join('');

            // Can we add more guests?
            const guestCount = personOrder.filter(l => l !== 'Me').length;
            const canAddGuest = maxGuests > 0 && guestCount < maxGuests;
            const nextGuestLabel = this._displayGuestLabel(this._getNextGuestLabel(personOrder));
            const addGuestBtn = canAddGuest ? `
                <button type="button" id="sbs-gr-add-guest"
                    style="display:flex;align-items:center;gap:10px;background:#fff;border:1.5px dashed #d1d5db;border-radius:14px;padding:14px 16px;cursor:pointer;width:100%;text-align:left;">
                    <div style="width:42px;height:42px;border-radius:50%;border:2px dashed #d1d5db;display:flex;align-items:center;justify-content:center;font-size:20px;color:#9ca3af;flex-shrink:0;">+</div>
                    <span style="font-size:14px;font-weight:600;color:#374151;">Add ${nextGuestLabel}</span>
                </button>` : '';

            const overlay = $(`<div id="sbs-gr-overlay" class="sbs-booking-overlay" style="
                position:fixed;inset:0;background:#f9fafb;z-index:9990;
                display:flex;flex-direction:column;overflow:hidden;">
                <!-- Header -->
                <div style="background:#fff;border-bottom:1px solid #f3f4f6;padding:16px 20px;display:flex;align-items:center;gap:12px;flex-shrink:0;">
                    <button type="button" id="sbs-gr-back" style="background:none;border:none;font-size:20px;cursor:pointer;color:#111;padding:0 4px;line-height:1;">&#8592;</button>
                    <div style="flex:1;">
                        <h2 style="margin:0;font-size:20px;font-weight:700;color:#1a1a1a;line-height:1.2;">Add guests and services</h2>
                        <p style="margin:4px 0 0;font-size:13px;color:#9ca3af;">Add up to ${maxGuests > 0 ? maxGuests : 4} guests; everyone starts together</p>
                    </div>
                    <button type="button" id="sbs-gr-close" style="background:none;border:none;font-size:22px;cursor:pointer;color:#9ca3af;padding:0 4px;line-height:1;">&times;</button>
                </div>

                <!-- Person list - scrollable -->
                <div style="flex:1;overflow-y:auto;padding:16px 20px;display:flex;flex-direction:column;gap:12px;">
                    <div id="sbs-gr-person-list" style="display:flex;flex-direction:column;gap:12px;">
                        ${personRows}
                    </div>
                    <div id="sbs-gr-add-guest-wrap">
                        ${addGuestBtn}
                    </div>
                </div>

                <!-- Footer bar -->
                <div style="background:#fff;border-top:1px solid #f3f4f6;padding:14px 20px;display:flex;align-items:center;justify-content:space-between;flex-shrink:0;">
                    <div>
                        <div style="font-size:18px;font-weight:700;color:#1a1a1a;">${ysFormatMoney(totalPrice)}</div>
                        <div style="font-size:12px;color:#9ca3af;">${totalServices} service${totalServices !== 1 ? 's' : ''} &bull; ${totalDuration} min</div>
                    </div>
                    <button type="button" id="sbs-gr-continue"
                        style="background:var(--yatoon-skin-primary-btn-bg,var(--y4-primary));color:#fff;border:none;border-radius:50px;padding:14px 32px;font-size:15px;font-weight:700;cursor:pointer;white-space:nowrap;">
                        Continue
                    </button>
                </div>
            </div>`);

            $('body').append(overlay);

            // Back
            overlay.on('click', '#sbs-gr-back', function() {
                overlay.remove();
                self.goToStep(3, true);
            });
            // Close (X) -> go to step 4, keep what we have
            overlay.on('click', '#sbs-gr-close', function() {
                overlay.remove();
                self._showPendingServicesSummary();
                self.goToStep(4, true);
            });

            // Continue
            overlay.on('click', '#sbs-gr-continue', function() {
                overlay.remove();
                self._showPendingServicesSummary();
                self.goToStep(4, true);
            });

            // Add guest
            overlay.on('click', '#sbs-gr-add-guest', function() {
                overlay.remove();
                self._addGuestTogether();
            });

            // Add service for person (delegated from document since it re-enters step 1)
            $(document).off('click.grAddSvc').on('click.grAddSvc', '.sbs-gr-add-svc', function() {
                const lbl = $(this).data('person-label') || null;
                overlay.remove();
                self._enterServiceSelection(lbl, true);
            });

            // Remove individual service row
            overlay.on('click', '.sbs-gr-remove-svc', function() {
                const idx = parseInt($(this).data('idx'));
                if (!isNaN(idx)) {
                    self._removePendingService(idx, function() {
                        overlay.remove();
                        if (self.pendingServices.length === 0) self.goToStep(1, true);
                        else self._showGuestReviewScreen();
                    });
                }
            });

            // Remove entire person
            overlay.on('click', '.sbs-gr-remove-person', function() {
                const lbl = $(this).data('person') || '';
                if (!lbl) return;
                // Remove all services for this person
                self.pendingServices = self.pendingServices.filter(s => (s.guestLabel || 'Me') !== lbl);
                self._saveSession();
                if (self.pendingServices.length === 0) {
                    overlay.remove();
                    self.goToStep(1, true);
                } else {
                    overlay.remove();
                    self._showGuestReviewScreen();
                }
            });
        }

        /**
         * Announce the active step to assistive technology.
         *
         * The wizard swaps its visible panel without a page navigation, so a
         * screen reader has nothing to read on transition. Writing the step
         * name into the polite live region gives the same "where am I now"
         * signal a sighted user gets from the progress bar. aria-hidden is
         * kept in sync at the same time so inactive panels are not read as
         * part of the page.
         */
        _announceStep(step) {
            try {
                const names = {
                    1: __('Choose service'),
                    2: __('Choose options'),
                    3: __('Choose staff'),
                    4: __('Choose date and time'),
                    5: __('Review and confirm'),
                };
                $('.sbs-form-step').attr('aria-hidden', 'true');
                $('.sbs-step-' + step).attr('aria-hidden', 'false');
                const region = document.getElementById('sbs-a11y-status');
                if (region && names[step]) {
                    window.clearTimeout(this._a11yStatusTimer);
                    region.textContent = '';
                    window.requestAnimationFrame(() => { region.textContent = names[step]; });
                    this._a11yStatusTimer = window.setTimeout(() => { region.textContent = ''; }, 1500);
                }
                const alertRegion = document.getElementById('sbs-a11y-alert');
                if (alertRegion) alertRegion.textContent = '';
            } catch (e) { /* announcement must never break navigation */ }
        }

        /**
         * Announce an error assertively.
         *
         * Validation failures are rendered as an injected .sbs-error div,
         * which a screen reader will not notice. Mirroring the text into the
         * assertive region interrupts and reads it, which is the correct
         * urgency for "your booking did not go through".
         */
        _announceError(message) {
            try {
                const region = document.getElementById('sbs-a11y-alert');
                if (region && message) {
                    region.textContent = '';
                    region.textContent = String(message);
                }
            } catch (e) { /* never break the error path itself */ }
        }

        goToStep(step, scrollToTop = false) {
            // 4.6.2: single choke point for the auto-advance back-guard. Every
            // backwards move in this widget - Back button, browser Back, the
            // progress bar, the summary Edit links, the mobile accordion - ends
            // up here, so blocking on "new step is lower than the current one"
            // covers all of them at once and cannot be forgotten by a future
            // navigation path. Without this, returning to step 3 with a staff
            // member still selected could bounce the customer straight back to
            // step 4 and make Back unusable.
            const previousStep = this.currentStep;
            if (step < previousStep) {
                this._blockAutoAdvance();
            } else if (this._autoAdvanceTimer) {
                // Leaving the step an advance was queued for: drop it.
                clearTimeout(this._autoAdvanceTimer);
                this._autoAdvanceTimer = null;
            }
            this.currentStep = step;
            // Account access is a convenience, not a booking requirement.
            // Keep the large banner on the landing/service step only; Step 5
            // has its own compact optional sign-in control beside guest fields.
            $('.sbs-portal-banner')
                .prop('hidden', step !== 1)
                .attr('aria-hidden', step === 1 ? 'false' : 'true');
            this._announceStep(step);
            this._saveSession(); // always persist current step
            this._syncDesignUploadCard();

            // Push a history entry so browser Back stays inside the booking widget
            if (step > 1) {
                history.pushState({ sbsStep: step }, '', window.location.href);
            } else {
                // On step 1, replace so there's always one baseline entry
                history.replaceState({ sbsStep: 1 }, '', window.location.href);
            }
            
            $('#sbs-main-title').show();
            this._updateMobileFlowHeader();
            this._updateStepAccordion();
            $('.sbs-booking-summary').toggleClass('sbs-checkout-card', step === 5);
            if (step !== 5) {
                $('#sbs-checkout-card-heading').remove();
            }
            
            if (step === 1) {
                $('#sbs-progress-bar').addClass('sbs-progress-bar-reserved').hide();
                // Show appointment-type selector only when no services are pending
                // (i.e. fresh start). Hide it once the user is actively adding services.
                const hasPendingOnStep1 = this.pendingServices && this.pendingServices.length > 0;
                const isAddingForPersonOnStep1 = !!this._nextGuestLabel;
                if (hasPendingOnStep1 || isAddingForPersonOnStep1) {
                    $('#sbs-appt-type-wrap').hide();
                } else {
                    // Only show if max-guests > 0 (the outer ready() block already hid it if 0)
                    const maxG = parseInt($('#sbs-appt-type-wrap').data('max-guests') || 0, 10);
                    if (maxG > 0) $('#sbs-appt-type-wrap').show();
                }
            } else {
                $('#sbs-progress-bar').removeClass('sbs-progress-bar-reserved').show();
                $('.sbs-prog-step').each(function() {
                    const s = parseInt($(this).data('go'));
                    $(this).removeClass('active completed clickable');
                    if (s === step) $(this).addClass('active');
                    if (s < step)  $(this).addClass('completed clickable');
                    // 4.6.1: the step indicator communicated position through
                    // CSS classes only, so a screen-reader user had no way to
                    // tell which of the five steps they were on.
                    if (s === step) {
                        $(this).attr('aria-current', 'step');
                    } else {
                        $(this).removeAttr('aria-current');
                    }
                    // Never set disabled - we handle navigation in the click handler
                    $(this).prop('disabled', false);
                });
                $('.sbs-prog-line').each(function(i) {
                    $(this).toggleClass('done', i < step - 1);
                });
            }
            
            if (step === 2) {
                $('#sbs-sidebar-next-btn').show();
                $('#sbs-cart-next-btn').show();
            } else {
                $('#sbs-sidebar-next-btn').hide();
                $('#sbs-cart-next-btn').hide();
            }

            // Cart bar visibility always driven by cart contents, not step number
            this._updateMobileCartBar();
            
            $('.sbs-form-step').removeClass('active');
            $(`.sbs-step-${step}`).addClass('active');

            // Update step bubbles: completed = green check, active = primary color, future = grey
            $('.sbs-booking-steps .sbs-step').each(function() {
                const s = parseInt($(this).data('step'));
                $(this).removeClass('active completed');
                if (s === step) $(this).addClass('active');
                if (s < step)  $(this).addClass('completed');
            });
            // Animate progress bar between bubbles (0% at step1, 100% at step5)
            const totalSteps = 5;
            const pct = step <= 1 ? 0 : Math.round(((step - 1) / (totalSteps - 1)) * 100);
            $('#sbs-step-progress').css('width', pct + '%');
            
            // Only scroll when explicitly requested (e.g. progress bar click), NOT on service/option clicks
            if (step > 1 && scrollToTop) {
                if (step === 3) this._scrollToStaffChoices();
                else this._scrollToFormTop();
            }
            
            if (step === 4) {
                this.updateSummary();
                const self = this;
                // 4.6.2: batch-fetch the density for the visible calendar range
                // as soon as the customer arrives on the date step. force=true
                // because the staff/service basket may have changed since the
                // last visit to this step even when the dates on screen did not.
                this.loadAvailabilityDensity(true);
                // -- FIX: Always clear stale time slots when entering step 4 --
                // This prevents clients from booking a time that was loaded for
                // a different staff/service combination.
                $('.sbs-time-slots').html(
                    '<div class="sbs-loading"><span class="spinner"></span> ' + __('Loading available times...') + '</div>'
                );
                setTimeout(function() {
                    // Re-select the currently highlighted date (or default to today/first available)
                    let $selectedBtn = $('.sbs-day-button.selected:not(.disabled)');
                    if (!$selectedBtn.length) $selectedBtn = $('.sbs-day-button.today:not(.disabled)');
                    if (!$selectedBtn.length) $selectedBtn = $('.sbs-day-button:not(.disabled)').first();
                    if ($selectedBtn.length) {
                        $('.sbs-day-button').removeClass('selected');
                        $selectedBtn.addClass('selected');
                        const date = $selectedBtn.data('date');
                        if (date) self.loadAvailableTimes(date, true);
                    }
                }, 150);

                window._sbsGroupIntent = false;
            }
            
            if (step === 1 || (step >= 3 && step !== 4)) {
                // Restore static sidebar rows (hidden on step 5), hide step-5-only total
                $('#sbs-summary-row-service, #sbs-summary-row-staff, #sbs-summary-row-datetime, #sbs-summary-row-duration, #sbs-summary-row-total').show();
                $('#sbs-step5-total').hide();
                this.updateSummary();
            }
            if (step === 5) {
                this.renderCart();
                this.updateSummary();
                this.updateSummaryForCheckout();
                this._renderGuestContactFields();
                this._updateLegalBoxes();
                // -- Always reset the submit button when entering step 5 ------
                // Prevents the button staying disabled after Back ->re-enter
                $('.sbs-btn-submit').prop('disabled', false);
                this._applyConfirmBtn();
            }
        }

        _selectedCheckoutServices() {
            return (this.bookingCart || []).flatMap(function(item) {
                if (item.services && item.services.length) {
                    return item.services.map(function(s) { return s.service || {}; });
                }
                return [item.service || {}];
            });
        }

        _needsPolicyBox() {
            const serviceNeedsPolicy = this._selectedCheckoutServices().some(function(s) {
                return !!s.requirePolicy;
            });
            return !!(yatoonData.policyEnabled || serviceNeedsPolicy);
        }

        _needsPolicyAck() {
            const serviceNeedsPolicy = this._selectedCheckoutServices().some(function(s) {
                return !!s.requirePolicy;
            });
            return !!((yatoonData.policyEnabled && yatoonData.policyRequireAck) || serviceNeedsPolicy);
        }

        _needsConsent() {
            const serviceNeedsConsent = this._selectedCheckoutServices().some(function(s) {
                return !!s.requireConsent;
            });
            return !!(yatoonData.consentEnabled || serviceNeedsConsent);
        }

        _updateLegalBoxes() {
            const needsPolicyBox = this._needsPolicyBox();
            const needsPolicyAck = this._needsPolicyAck();
            const needsConsent = this._needsConsent();

            $('.sbs-policy-box').toggle(needsPolicyBox);
            $('#sbs-policy-acknowledged').prop('required', needsPolicyAck);
            if (!needsPolicyBox) {
                $('#sbs-policy-acknowledged').prop('checked', false);
            }

            $('.sbs-consent-box').toggle(needsConsent);
            $('#sbs-consent-accepted').prop('required', needsConsent);
            $('#sbs-consent-signature').prop('required', needsConsent);
            if (!needsConsent) {
                $('#sbs-consent-accepted').prop('checked', false);
                $('#sbs-consent-signature').val('');
            }
        }

        _getPreloadedServiceOptions(serviceId) {
            const map = (typeof yatoonData !== 'undefined' && yatoonData.serviceOptions) ? yatoonData.serviceOptions : null;
            if (!map) return null;
            return map[String(serviceId)] || map[serviceId] || null;
        }

        _renderServiceOptionsPayload(payload) {
            const self = this;
            const container = $('.sbs-options-container');
            self._parkDesignUploadCard();
            if (!payload || !Array.isArray(payload.options)) {
                return false;
            }
            if (
                self.selectedService
                && Object.prototype.hasOwnProperty.call(payload, 'service_allow_design_upload')
            ) {
                // The server is authoritative. This also repairs restored browser
                // sessions created before the upload flag was stored client-side.
                self.selectedService.allowDesignUpload = parseInt(payload.service_allow_design_upload, 10) === 1;
                self._saveSession();
            }
            if (payload.options.length === 0) {
                self._optionHasMainGroup = false;
                self.selectedOptions = [];
                if (self.selectedService && self.selectedService.allowDesignUpload && $('.yatoon-design-upload-card').length) {
                    container.html('<div class="sbs-option-empty-note">No additional options are required. You may add a reference photo below.</div>');
                    self._ensureOptionActionSummary();
                    self._syncDesignUploadCard();
                    self._scrollToFormTop();
                    return true;
                }
                return false;
            }
                        // Group options by group_name. null/'' = "Main" group (single-select).
                        const groups = {};
                        payload.options.forEach(function(option) {
                            const g = option.group_name || '';
                            if (!groups[g]) groups[g] = [];
                            groups[g].push(option);
                        });
                        self._optionHasMainGroup = Object.keys(groups).some(function(groupKey) {
                            const key = (groupKey || '').trim().toLowerCase();
                            return !(key === 'add-on' || key === 'add on' || /^add[\s\-_]?on$/i.test(key));
                        });
                        const orderedGroupKeys = Object.keys(groups).sort(function(a, b) {
                            const aAddon = /^add[\s\-_]?on$/i.test((a || '').trim()) || (a || '').trim().toLowerCase() === 'add on';
                            const bAddon = /^add[\s\-_]?on$/i.test((b || '').trim()) || (b || '').trim().toLowerCase() === 'add on';
                            if (aAddon === bAddon) return 0;
                            return aAddon ? 1 : -1;
                        });
                        const mainOptionsForPrice = orderedGroupKeys
                            .filter(function(groupKey) {
                                const key = (groupKey || '').trim().toLowerCase();
                                return !(key === 'add-on' || key === 'add on' || /^add[\s\-_]?on$/i.test(key));
                            })
                            .flatMap(function(groupKey) { return groups[groupKey] || []; });

                        const serviceName = self._esc((self.selectedService && self.selectedService.name) || __('Service'));
                        const serviceDesc = self._esc((self.selectedService && self.selectedService.description) || '');
                        const serviceImg  = self._esc((self.selectedService && self.selectedService.image_url) || '');
                        const labels = (typeof yatoonData !== 'undefined' && yatoonData.bookingLabels) ? yatoonData.bookingLabels : {};
                        const labelText = function(key, fallback) {
                            return self._esc(labels[key] || fallback);
                        };
                        const basePrice   = parseFloat((self.selectedService && self.selectedService.price) || payload.base_price || 0);
                        const baseDur     = parseInt((self.selectedService && self.selectedService.duration) || 0);
                        let priceText = basePrice > 0 ? ysFormatMoney(basePrice) : __('Price varies');
                        let durationText = baseDur ? `${baseDur} min+` : '';
                        if (mainOptionsForPrice.length > 0) {
                            const mainPrices = mainOptionsForPrice
                                .map(function(option) { return parseFloat(option.absolute_price); })
                                .filter(function(price) { return !isNaN(price) && price > 0; });
                            const mainDurations = mainOptionsForPrice
                                .map(function(option) { return parseInt(option.duration_minutes); })
                                .filter(function(duration) { return !isNaN(duration) && duration > 0; });
                            if (mainPrices.length > 0) {
                                const minPrice = Math.min.apply(null, mainPrices);
                                const maxPrice = Math.max.apply(null, mainPrices);
                                priceText = minPrice === maxPrice ? ysFormatMoney(minPrice) : __('from') + ' ' + ysFormatMoney(minPrice);
                            }
                            if (mainDurations.length > 0) {
                                const minDuration = Math.min.apply(null, mainDurations);
                                const maxDuration = Math.max.apply(null, mainDurations);
                                durationText = minDuration === maxDuration ? `${minDuration} min` : `${minDuration}-${maxDuration} min`;
                            }
                        }
                        let html = `
                            <div class="sbs-service-detail-panel">
                                ${serviceImg ? `<div class="sbs-service-detail-image"><img src="${serviceImg}" alt="${serviceName}" loading="lazy" style="display:block;width:100%;height:auto;"></div>` : ''}
                                <div class="sbs-service-detail-copy">
                                    <h3 class="sbs-service-detail-title">${serviceName}</h3>
                                    <div class="sbs-service-detail-meta">
                                        <span>${durationText}</span>
                                        <span>${priceText}</span>
                                    </div>
                                    ${serviceDesc ? `<p class="sbs-service-detail-desc">${serviceDesc}</p>` : ''}
                                </div>
                            </div>
                            <div class="sbs-related-options-head">
                                <div>
                                    <div class="sbs-related-eyebrow">${labelText('relatedEyebrow', __('Related choices'))}</div>
                                    <h4>${labelText('relatedTitle', __('Choose what fits your visit'))}</h4>
                                </div>
                                <span>${labelText('relatedHint', __('Tap to select'))}</span>
                            </div>
                        `;

                        orderedGroupKeys.forEach(function(groupKey) {
                            const isAddon = /^add[\s\-_]?on$/i.test(groupKey.trim()) || groupKey.trim().toLowerCase() === 'add on';
                            const groupLabel = isAddon ? labelText('addonGroupLabel', __('Recommended add-ons')) : labelText('mainGroupLabel', __('Choose one option'));
                            const groupHelp  = isAddon ? labelText('addonGroupHelp', __('Optional extras you can add to this appointment.')) : labelText('mainGroupHelp', __('Pick the main style or variation for this service.'));
                            let addonRank = 0;

                            html += `
                                <section class="sbs-option-section ${isAddon ? 'sbs-option-section-addon' : 'sbs-option-section-main'}">
                                    <div class="sbs-option-section-head">
                                        <h4>${groupLabel}</h4>
                                        <p>${groupHelp}</p>
                                    </div>
                                    <div class="sbs-option-group" data-group="${self._esc(groupKey)}" data-multi="${isAddon ? '1' : '0'}">
                            `;

                            groups[groupKey].forEach(function(option) {
                                if (isAddon) addonRank++;
                                const absolutePrice = parseFloat(option.absolute_price);
                                const priceVaries = absolutePrice <= 0 || /price varies/i.test(String(option.name || ''));
                                const priceDisplay  = priceVaries ? __('Price varies') : ysFormatMoney(absolutePrice);

                                let displayName = option.name;
                                if (option.name.toLowerCase() === 'regular' || option.name === self.selectedService.name) {
                                    displayName = self.selectedService.name;
                                }
                                const safeName = self._esc(displayName);
                                const safeDesc = self._esc(option.description || '');
                                const badgeChoice = String(option.badge_label || '').toLowerCase();
                                let badge = '';
                                if (isAddon) {
                                    if (badgeChoice === 'recommended') {
                                        badge = labelText('recommendedBadge', __('Recommended'));
                                    } else if (badgeChoice === 'popular') {
                                        badge = labelText('popularBadge', __('Popular'));
                                    } else if (badgeChoice === 'addon') {
                                        badge = labelText('addonBadge', __('Add-on'));
                                    } else if (badgeChoice !== 'none') {
                                        badge = addonRank === 1 ? labelText('recommendedBadge', __('Recommended')) : (addonRank === 2 ? labelText('popularBadge', __('Popular')) : labelText('addonBadge', __('Add-on')));
                                    }
                                }

                                const inputType = isAddon ? 'checkbox' : 'radio';
                                html += `
                                    <div class="sbs-option-item ${isAddon ? 'sbs-option-addon' : 'sbs-option-main'}"
                                         data-price="${option.price_modifier}"
                                         data-absolute-price="${absolutePrice}"
                                         data-duration="${option.duration_minutes}"
                                         data-group="${groupKey}"
                                         data-linked-service-id="${option.linked_service_id || ''}"
                                         data-allow-design-upload="${parseInt(option.allow_design_upload || 0)}"
                                         data-square-id="${option.square_id || ''}">
                                        <input type="${inputType}" name="sbs-option-${self._esc(groupKey)}" class="sbs-option-checkbox" value="${option.id}" id="option-${option.id}">
                                        <label for="option-${option.id}" class="sbs-option-info">
                                            ${badge ? `<span class="sbs-option-badge">${badge}</span>` : ''}
                                            <div class="sbs-option-name">${safeName}</div>
                                            ${safeDesc ? `<div class="sbs-option-description">${safeDesc}</div>` : ''}
                                            <div class="sbs-option-duration">${option.duration_minutes} min</div>
                                        </label>
                                        <span class="sbs-option-price ${priceVaries ? 'is-variable' : ''}">${priceDisplay}</span>
                                        <span class="sbs-option-check" aria-hidden="true">&#10003;</span>
                                    </div>
                                `;
                            });

                            html += '</div></section>';
                        });

                        container.html(html);
                        self._ensureOptionActionSummary();

                        // -- EDIT MODE: restore previously selected options --------------
                        // When the user clicks EDIT on a pending service, _editPendingService
                        // pre-populates self.selectedOptions with the existing choices.
                        // We must reflect that state in the UI checkboxes - otherwise the
                        // user sees an empty form, re-selects everything, and ends up with
                        // duplicates (old options + new options stacked on top).
                        if (self.selectedOptions && self.selectedOptions.length > 0) {
                            const existingIds = self.selectedOptions.map(o => o.id);
                            existingIds.forEach(function(optId) {
                                const $cb = container.find('.sbs-option-checkbox[value="' + optId + '"]');
                                if ($cb.length) {
                                    $cb.prop('checked', true);
                                    $cb.closest('.sbs-option-item').addClass('selected');
                                }
                            });
                        }
                        self._updateOptionActionSummary();
                        self._syncDesignUploadCard();

                        self._scrollToFormTop();
            return true;
        }

        loadServiceOptions() {
            const self = this;
            const container = $('.sbs-options-container');
            if (!self.selectedService || !self.selectedService.id) return;
            const requestServiceId = self.selectedService.id;
            const requestSeq = ++self._optionsSeq;
            if (self._optionsRequest && self._optionsRequest.readyState !== 4) {
                try { self._optionsRequest.abort(); } catch(e) {}
            }
            self._parkDesignUploadCard();
            container.html('<div class="sbs-loading"><span class="spinner"></span> ' + self._esc(__('Loading options...')) + '</div>');
            self.goToStep(2);

            const preloaded = self._getPreloadedServiceOptions(requestServiceId);
            if (preloaded) {
                self._optionsRequest = null;
                if (self._renderServiceOptionsPayload(preloaded)) {
                    return;
                }
                container.html('');
                self.selectedOptions = [];
                self.goToStep(3);
                self.loadAvailableStaff();
                return;
            }
            
            self._optionsRequest = $.ajax({
                url: yatoonData.ajax_url,
                type: 'POST',
                timeout: 30000,
                data: {
                    action:     'yatoon_get_service_options',
                    nonce:      yatoonData.nonce,
                    service_id: requestServiceId
                },
                success: function(response) {
                    if (requestSeq !== self._optionsSeq || !self.selectedService || String(self.selectedService.id) !== String(requestServiceId)) return;
                    if (response && response.success && response.data && Array.isArray(response.data.options) && response.data.options.length > 0) {
                        self._renderServiceOptionsPayload(response.data);
                    } else {
                        container.html('');
                        self.selectedOptions = [];
                        self.goToStep(3);
                        self.loadAvailableStaff();
                    }
                },
                error: function(xhr, status) {
                    if (requestSeq !== self._optionsSeq || status === 'abort' || (xhr && xhr.status === 0)) return;
                    container.html(
                        '<div class="sbs-error">' + self._esc(__('Failed to load options. Please tap Back and choose the service again.')) + '</div>'
                    );
                    self._ensureOptionActionSummary();
                },
                complete: function() {
                    if (requestSeq === self._optionsSeq && self._optionsRequest && self._optionsRequest.readyState === 4) {
                        self._optionsRequest = null;
                    }
                }
            });
        }

        _bindSummaryEditButtons() {
            const self = this;
            $(document).on('click', '.sbs-summary-edit-btn', function() {
                const step = parseInt($(this).data('goto'));
                if (!step) return;
                if (step === 3) {
                    self.goToStep(3);
                    self.loadAvailableStaff();
                } else {
                    self.goToStep(step, true);
                }
            });
        }

        _getPreloadedServiceStaff(serviceId) {
            const map = (typeof yatoonData !== 'undefined' && yatoonData.serviceStaff) ? yatoonData.serviceStaff : null;
            if (!map) return null;
            return map[String(serviceId)] || map[serviceId] || null;
        }

        _renderStaffList(staffList) {
            const self = this;
            const container = $('.sbs-staff-grid');
            if (!Array.isArray(staffList) || staffList.length === 0) {
                container.html('<p>' + self._esc(__('No staff available. Please try again later.')) + '</p>');
                return false;
            }

            const eligibleIds = staffList.filter(s => parseInt(s.id) !== 0).map(s => Number(s.id));
            if (self.selectedService) self.selectedService._eligibleStaffIds = eligibleIds;
            if (self.selectedService) self._staffCache[self.selectedService.id] = staffList;

            // -- Time-aware staff conflict detection ------------------------------
            // If other services are already queued up (this guest's earlier services in
            // the same visit, or another guest's service running in parallel at the same
            // start time), hide any named staff member who is still occupied during this
            // service's real-time window. They automatically reappear once whatever they
            // were doing ends before this service's window starts.
            const myLabel = self._nextGuestLabel || 'Me';
            const myStart = self._nextOffsetForGuest(myLabel);
            const myDur   = self._serviceDuration({ service: self.selectedService, options: self.selectedOptions });
            const myEnd   = myStart + myDur;
            const busyIds = self._computeBusyStaffIds(myStart, myEnd, null);

            const visibleStaffList = busyIds.size
                ? staffList.filter(s => parseInt(s.id) === 0 || !busyIds.has(String(s.id)))
                : staffList;
            const namedVisible = visibleStaffList.filter(s => parseInt(s.id) !== 0);

            if (self.selectedService && self.selectedService.allowStaffSelect === false) {
                self.selectedStaff = { id: 0, name: __('Any staff') };
                self._commitSelectedStaff();
                return true;
            }

            // Every named staff member is busy during this window - don't silently fall
            // back to "Any staff" (that could double-book someone); let the customer know.
            if (busyIds.size > 0 && namedVisible.length === 0) {
                container.html('<p>No staff are free for this service at that time. Please choose a different service order, or pick another time.</p>');
                return false;
            }

            // 4.6.2: the "only one possible staff member, skip step 3" shortcut
            // is right on the way FORWARD and wrong on the way back. Pressing
            // Back from step 4 lands here, and 4.6.1 immediately re-committed
            // the same lone staff member and threw the customer straight back to
            // step 4 - Back was unusable at a single-stylist salon. When the
            // auto-advance guard is armed (i.e. we got here by going backwards)
            // fall through to the normal render instead: the one card is drawn
            // and pre-selected below, so the customer sees a real step 3 and can
            // keep going back. Forward navigation never arms the guard, so the
            // skip still happens where it helps.
            if (visibleStaffList.length === 1 && Date.now() >= self._autoAdvanceBlockedUntil) {
                const staff = visibleStaffList[0];
                self.selectedStaff = { id: staff.id, name: staff.name, image_url: staff.image_url, price_override: staff.price_override };
                self._applyStaffPrice();
                self._commitSelectedStaff();
                return true;
            }

            let html = '';
            visibleStaffList.forEach(function(staff) {
                const displayName = self.getFirstName(staff.name);
                const isAny = parseInt(staff.id) === 0 || String(staff.name || '').toLowerCase().indexOf('any') !== -1;
                html += `
                    <div class="sbs-staff-card ${isAny ? 'sbs-staff-card-any' : ''}" role="button" tabindex="0" aria-pressed="false" aria-label="Choose ${self._esc(displayName)}" data-staff-id="${staff.id}" data-staff-name="${self._esc(staff.name)}" data-price-override="${staff.price_override === null || typeof staff.price_override === 'undefined' ? '' : Number(staff.price_override)}">
                        ${staff.image_url ? `<div class="sbs-staff-image"><img src="${self._esc(staff.image_url)}" alt="${self._esc(displayName)}" loading="lazy"></div>` : `<div class="sbs-staff-image sbs-staff-initial">${isAny ? 'Any' : self._esc(displayName.charAt(0))}</div>`}
                        <div class="sbs-staff-name">${self._esc(displayName)}</div>
                        ${isAny ? `<div class="sbs-staff-bio">Fastest available for an opening</div>` : (staff.bio ? `<div class="sbs-staff-bio">${self._esc(staff.bio)}</div>` : '') }
                    </div>
                `;
            });

            container.html(html);
            const selectedStaffId = self.selectedStaff ? String(self.selectedStaff.id) : '';
            const $selectedCard = selectedStaffId
                ? container.find('.sbs-staff-card').filter(function() {
                    return String($(this).data('staff-id')) === selectedStaffId;
                }).first()
                : $();
            const $anyCard = container.find('.sbs-staff-card-any').first();
            if ($selectedCard.length) {
                self._selectStaffCard($selectedCard);
            } else if (self._instantStaffId) {
                const $instantStaff = container.find('.sbs-staff-card').filter(function() {
                    return String($(this).data('staff-id')) === String(self._instantStaffId);
                }).first();
                if ($instantStaff.length) self._selectStaffCard($instantStaff);
                else if ($anyCard.length) self._selectStaffCard($anyCard);
            } else if ($anyCard.length) {
                self._selectStaffCard($anyCard);
            }
            self._instantStaffId = null;
            self._scrollToStaffChoices();
            return true;
        }

        loadAvailableStaff() {
            const self = this;
            const container = $('.sbs-staff-grid');
            if (!self.selectedService || !self.selectedService.id) return;
            const requestServiceId = self.selectedService.id;
            const requestSeq = ++self._staffSeq;
            if (self._staffRequest && self._staffRequest.readyState !== 4) {
                try { self._staffRequest.abort(); } catch(e) {}
            }
            container.html('<div class="sbs-loading"><span class="spinner"></span> ' + self._esc(__('Loading staff...')) + '</div>');
            self.goToStep(3);

            const preloaded = self._getPreloadedServiceStaff(requestServiceId);
            if (preloaded && Array.isArray(preloaded.staff)) {
                self._staffRequest = null;
                self._renderStaffList(preloaded.staff);
                return;
            }
            
            self._staffRequest = $.ajax({
                url: yatoonData.ajax_url,
                type: 'POST',
                timeout: 30000,
                data: {
                    action:     'yatoon_get_available_staff',
                    nonce:      yatoonData.nonce,
                    service_id: requestServiceId
                },
                success: function(response) {
                    if (requestSeq !== self._staffSeq || !self.selectedService || String(self.selectedService.id) !== String(requestServiceId)) return;
                    if (response && response.success && response.data && Array.isArray(response.data.staff)) {
                        self._renderStaffList(response.data.staff);
                    } else {
                        container.html('<p>' + self._esc(__('No staff available. Please try again later.')) + '</p>');
                    }
                },
                error: function(xhr, status) {
                    if (requestSeq !== self._staffSeq || status === 'abort' || (xhr && xhr.status === 0)) return;
                    container.html('<div class="sbs-error">' + self._esc(__('Could not load staff. Please tap Back and choose the service again.')) + '</div>');
                },
                complete: function() {
                    if (requestSeq === self._staffSeq && self._staffRequest && self._staffRequest.readyState === 4) {
                        self._staffRequest = null;
                    }
                }
            });
        }

        /**
         * 4.6.2: debounced entry point for the time-slot lookup.
         *
         * There was no debounce anywhere in this file. De-duplication relied on
         * the sequence number plus abort() below: a burst of five rapid date
         * taps still fired five POSTs, and aborting only drops the RESPONSE --
         * PHP had already begun a full availability scan for each one. On a
         * salon with several staff that is the most expensive endpoint in the
         * plugin, being hit as fast as a customer can tap.
         *
         * Which call sites pass immediate = true, and why:
         *
         *   - Anything the customer directly and deliberately asked for: the
         *     day-button click, the Today/Tomorrow shortcuts, the "find next
         *     opening" jump, the Back button landing on step 4, and entering
         *     step 4. These are one-shot and the customer is watching the slot
         *     list, so a 250ms stall would read as lag for no benefit. They also
         *     cannot burst - a second click supersedes the first, which the
         *     existing sequence/abort pair already handles.
         *
         *   - Everything else debounces: staff swapped in the pending-services
         *     banner, services reordered with the up/down arrows, a service
         *     removed from the cart, accordion step jumps. These fire from
         *     repeatable controls where the customer plausibly makes several
         *     adjustments in a row, and each one invalidates the previous
         *     query. Collapsing those into one request is exactly the case
         *     debouncing exists for.
         *
         * @param {string}  date      Y-m-d.
         * @param {boolean} immediate Skip the debounce window.
         */
        loadAvailableTimes(date, immediate) {
            const self = this;
            if (self._timesDebounceTimer) {
                clearTimeout(self._timesDebounceTimer);
                self._timesDebounceTimer = null;
            }
            if (immediate === true) {
                self._loadAvailableTimesNow(date);
                return;
            }
            self._timesDebounceTimer = setTimeout(function() {
                self._timesDebounceTimer = null;
                self._loadAvailableTimesNow(date);
            }, 250);
        }

        // 4.6.2: the original loadAvailableTimes() body, unchanged, moved behind
        // the debounce above. Everything that must run without delay calls
        // loadAvailableTimes(date, true) rather than reaching in here.
        _loadAvailableTimesNow(date) {
            const self = this;
            const container = $('.sbs-time-slots');
            $('#sbs-next-openings').hide().empty();
            self._updateDateAvailabilityMeter(date, null);

            // -- FIX: Multi-service support ------------------------------------
            // When pendingServices has items (user added multiple services/staff),
            // we must query using ALL staff IDs so the backend finds slots where
            // every staff member is free at the same time.
            let activeSvc, activeStaff, activeOptions, allStaffIds, allOptionIds;
            // Square must receive every selected variation, including add-ons,
            // so Step 4 validates the exact same appointment as confirmation.
            let mainOptionIds = [], addonOptionIds = [];

            // Per-staff option IDs: [[optId,...], [optId,...]] - same index order as allStaffIds
            let allOptionIdsPerStaff = [];

            if (self.pendingServices.length > 0) {
                activeSvc     = self.pendingServices[0].service;
                activeStaff   = self.pendingServices[0].staff;
                activeOptions = self.pendingServices[0].options;
                // NOTE: Do NOT use .filter(Boolean) here - that would strip id=0 ("Any staff"),
                // which must be passed to the backend so it knows to apply Case B expansion logic.
                allStaffIds   = self.pendingServices.map(s => s.staff ? s.staff.id : 0);
                allOptionIds  = self.pendingServices.flatMap(s => s.options.map(o => o.id));
                // For multi-service, split main vs addon per service.
                // IMPORTANT: if filtering out addons leaves an empty list, fall back to ALL options -                 // some services mark their primary option (e.g. "Full Set") as is_addon by mistake,
                // and sending empty option_ids causes the backend to pick the wrong Square variation.
                addonOptionIds = self.pendingServices.flatMap(s => s.options.filter(o => o.is_addon).map(o => o.id));
                mainOptionIds  = self.pendingServices.flatMap(s => {
                    const main = s.options.filter(o => !o.is_addon).map(o => o.id);
                    return main.length > 0 ? main : s.options.map(o => o.id); // fallback: all options
                });
                allOptionIdsPerStaff = self.pendingServices.map(s => s.options.map(o => o.id));
                // If malformed catalog data marks every selected option as an
                // add-on, the fallback above promotes those IDs to primary
                // variations. They must not also be sent as add-on segments.
                const promotedMainIds = new Set(mainOptionIds.map(id => String(id)));
                addonOptionIds = addonOptionIds.filter(id => !promotedMainIds.has(String(id)));
            } else {
                activeSvc     = self.selectedService;
                activeStaff   = self.selectedStaff;
                activeOptions = self.selectedOptions;
                allStaffIds   = activeStaff ? [activeStaff.id] : [];
                allOptionIds  = activeOptions.map(o => o.id);
                // KEY FIX: Only main (non-addon) options determine which Square variation to query.
                // Add-ons are extra time/price but NOT separate Square service variations.
                // Using addon variation IDs in search_availability returns wrong duration results.
                // HOWEVER: if filtering out addons leaves nothing, fall back to all options -                 // the primary option may be incorrectly tagged as addon in some services.
                mainOptionIds  = activeOptions.filter(o => !o.is_addon).map(o => o.id);
                if (mainOptionIds.length === 0) mainOptionIds = activeOptions.map(o => o.id);
                addonOptionIds = activeOptions.filter(o => o.is_addon).map(o => o.id);
                const promotedMainIds = new Set(mainOptionIds.map(id => String(id)));
                addonOptionIds = addonOptionIds.filter(id => !promotedMainIds.has(String(id)));
                allOptionIdsPerStaff = [mainOptionIds];
            }

            if (!activeSvc) {
                container.html('<p style="color:#c00;text-align:center;padding:20px;">' + self._esc(__('Please select a service first.')) + '</p>');
                return;
            }
            const requestSeq = ++self._timesSeq;
            if (self._timesRequest && self._timesRequest.readyState !== 4) {
                try { self._timesRequest.abort(); } catch(e) {}
            }
            container.html('<div class="sbs-loading"><span class="spinner"></span> ' + __('Loading available times...') + '</div>');
            self._timesRequest = $.ajax({
                url: yatoonData.ajax_url,
                type: 'POST',
                timeout: 15000,
                data: {
                    action:          'yatoon_get_available_times',
                    nonce:           yatoonData.nonce,
                    service_id:      activeSvc.id,
                    staff_id:        activeStaff ? activeStaff.id : 0,
                    // Multi-staff intersection: all staff IDs + every selected
                    // variation, including add-ons, in appointment order.
                    all_staff_ids:    allStaffIds.length > 1 ? JSON.stringify(allStaffIds) : '',
                    all_option_ids:   allStaffIds.length > 1 ? JSON.stringify(allOptionIdsPerStaff) : '',
                    // Per-staff service IDs - used by backend as fallback when option variation lookup fails
	                    all_service_ids:  allStaffIds.length > 1 ? JSON.stringify(self.pendingServices.map(s => s.service ? s.service.id : 0)) : '',
	                    all_guest_labels: allStaffIds.length > 1 ? JSON.stringify(self.pendingServices.map(s => s.guestLabel || 'Me')) : '',
	                    all_durations:    allStaffIds.length > 1 ? JSON.stringify(self.pendingServices.map(s => self._serviceDuration(s))) : '',
                    // KEY FIX: Pass confirmed cart appointments' staff busy windows to backend.
                    // This lets the backend exclude already-occupied named staff when assigning
                    // "Any staff" slots - prevents booking unavailable staff when they're already booked.
                    // Format: [{staff_id, start_at, end_minutes}, ...]
                    cart_busy_staff: (function() {
                        const windows = [];
                        self.bookingCart.forEach(function(ci) {
                            if (!ci.dateTime || !ci.dateTime.start_at) return;
                            const ciSvcs = ci.services
                                || [{ staff: ci.staff, options: ci.options || [], service: ci.service }];
                            const offsets = self._serviceOffsetsByGuest(ciSvcs);
                            ciSvcs.forEach(function(sv) {
                                const opts    = sv.options || [];
                                const optsDur = opts.reduce(function(d, o) { return d + (parseInt(o.duration) || 0); }, 0);
                                const baseDur = parseInt(sv.service && sv.service.duration) || 0;
                                const dur     = Math.max(1, optsDur > 0 ? optsDur : baseDur);
                                // Compute actual start_at for this service segment
                                const segStartMs = new Date(ci.dateTime.start_at).getTime() + offsets.shift() * 60000;
                                const segStartAt = new Date(segStartMs).toISOString();
                                if (!sv.staff || sv.staff.id === 0) {
                                    // BUGFIX: Any staff cart items - mark all eligible staff as busy.
                                    // Without this, PHP has no way to know these staff are occupied,
                                    // allowing the same time slot to be booked again.
                                    const eligibleIds = (sv.eligibleStaffIds)
                                        || (sv.service && sv.service._eligibleStaffIds)
                                        || [];
                                    eligibleIds.forEach(function(eid) {
                                        windows.push({ staff_id: Number(eid), start_at: segStartAt, duration: dur });
                                    });
                                    return;
                                }
                                windows.push({ staff_id: Number(sv.staff.id), start_at: segStartAt, duration: dur });
                            });
                        });
                        return windows.length > 0 ? JSON.stringify(windows) : '';
                    })(),
                    date:            date,
                    // KEY: only send MAIN option IDs - addons don't have their own Square variation duration
                    option_ids:      mainOptionIds.length ? JSON.stringify(mainOptionIds) : '',
                    // Add-on option IDs - sent separately so backend can do multi-segment Square availability query
                    addon_option_ids: addonOptionIds.length ? JSON.stringify(addonOptionIds) : '',
                    // Add-on extra minutes - backend uses this to filter out slots that don't have
                    // enough buffer after the main service ends (e.g. Fill-in 40min + Remove 20min = 60min needed)
                    addon_duration:  addonOptionIds.length
                        ? activeOptions.filter(o => addonOptionIds.map(String).includes(String(o.id))).reduce((s, o) => s + (parseInt(o.duration) || 0), 0)
                        : 0,
                    // Total duration across ALL pending services (for latest-end-time cutoff on server)
                    total_duration: (function() {
                        if (self.pendingServices.length > 0) {
                            return self._durationByGuest(self.pendingServices).totalDuration;
                        }
                        // Single service
                        const mainDur = activeOptions.filter(o => !o.is_addon).reduce((s, o) => s + (parseInt(o.duration) || 0), 0);
                        const addDur  = activeOptions.filter(o =>  o.is_addon).reduce((s, o) => s + (parseInt(o.duration) || 0), 0);
                        return (mainDur || parseInt(activeSvc.duration) || 0) + addDur;
                    })()
                },
                success: function(response) {
                    if (requestSeq !== self._timesSeq) return;
                    const responseTimes = response.success && response.data.times ? response.data.times : [];
                    const visibleTimes = self._filterPastSlotsForDate(responseTimes, date);
                    if (visibleTimes.length > 0) {
                        self._updateDateAvailabilityMeter(date, visibleTimes.length);
                        // Availability conflict checks
                        // SMART BUSY-WINDOW CALCULATION
                        //
                        // For each service in the NEW appointment (pendingServices):
                        //   - Named staff (id != 0): their time window is [slotStart + offset, + dur].
                        //     If that window overlaps a busy entry in the cart ->show busy warning.
                        //   - Any staff (id == 0): check if AT LEAST ONE eligible staff member is
                        //     free during [slotStart + offset, + dur]. If yes ->slot is available
                        //     (no busy warning). Only show busy if ALL eligible staff are occupied.
                        //
                        // "Busy" in the cart means a named staff member has a confirmed service
                        // in Appointment 1 that overlaps the candidate window.
                        // Availability conflict checks

                        // Step 1: Build a map of every named staff's busy intervals from the cart.
                        // staffBusyMap[staffId] = [{startMs, endMs}, ...]
                        // BUGFIX: Also include "Any staff" cart items - when a slot is booked with
                        // Any staff, ALL eligible staff for that service are considered busy during
                        // that window. Otherwise a second appointment could claim the same slot
                        // because the Any-staff item was skipped when building busyMap.
                        const staffBusyMap = {};
                        self.bookingCart.forEach(function(ci) {
                            if (!ci.dateTime || !ci.dateTime.start_at) return;
                            const ciStartMs = new Date(ci.dateTime.start_at).getTime();
                            const ciSvcs = ci.services
                                || [{ staff: ci.staff, options: ci.options || [], service: ci.service }];
                            const offsets = self._serviceOffsetsByGuest(ciSvcs);
                            ciSvcs.forEach(function(sv) {
                                const opts    = sv.options || [];
                                const optsDur = opts.reduce(function(d,o){ return d+(parseInt(o.duration)||0); }, 0);
                                const baseDur = parseInt(sv.service && sv.service.duration) || 0;
                                const dur     = Math.max(1, optsDur > 0 ? optsDur : baseDur);
                                const sMs     = ciStartMs + offsets.shift() * 60000;
                                const eMs     = sMs + dur * 60000;
                                if (!sv.staff || sv.staff.id === 0) {
                                    // Any staff: mark ALL eligible staff for this service as busy.
                                    // Use eligibleStaffIds stored on the cart item's service, or
                                    // fall back to the service's _eligibleStaffIds if available.
                                    const eligibleIds = (sv.eligibleStaffIds)
                                        || (sv.service && sv.service._eligibleStaffIds)
                                        || [];
                                    eligibleIds.forEach(function(eid) {
                                        const sid = Number(eid);
                                        if (!staffBusyMap[sid]) staffBusyMap[sid] = [];
                                        staffBusyMap[sid].push({ startMs: sMs, endMs: eMs });
                                    });
                                    return;
                                }
                                const sid = Number(sv.staff.id);
                                if (!staffBusyMap[sid]) staffBusyMap[sid] = [];
                                staffBusyMap[sid].push({ startMs: sMs, endMs: eMs });
                            });
                        });

                        // Helper: does a candidate window [wStart, wEnd) overlap any busy interval for staffId?
                        function _isBusy(staffId, wStartMs, wEndMs) {
                            const intervals = staffBusyMap[Number(staffId)] || [];
                            return intervals.some(function(iv) {
                                return wStartMs < iv.endMs && wEndMs > iv.startMs;
                            });
                        }

                        // Step 2: Build per-service window descriptors for the NEW appointment.
                        // newSvcWindows[i] = { staffId, offsetMs, durationMs, eligibleStaffIds, label }
                        const newSvcWindows = [];
                        if (self.pendingServices.length > 0) {
                            const offsets = self._serviceOffsetsByGuest(self.pendingServices);
                            self.pendingServices.forEach(function(ps) {
                                const opts    = ps.options || [];
                                const optsDur = opts.reduce(function(d,o){ return d+(parseInt(o.duration)||0); }, 0);
                                const baseDur = parseInt(ps.service && ps.service.duration) || 0;
                                const dur     = Math.max(1, optsDur > 0 ? optsDur : baseDur);
                                const staffId = (ps.staff && ps.staff.id) ? Number(ps.staff.id) : 0;
                                newSvcWindows.push({
                                    staffId:          staffId,
                                    offsetMs:         offsets.shift() * 60000,
                                    durationMs:       dur * 60000,
                                    eligibleStaffIds: ps.eligibleStaffIds || [],
                                    label:            staffId !== 0 ? self.getFirstName(ps.staff.name) : __('Any Staff Available')
                                });
                            });
                        } else if (self.selectedService) {
                            // Single-service (no pendingServices yet)
                            const opts    = self.selectedOptions || [];
                            const optsDur = opts.reduce(function(d,o){ return d+(parseInt(o.duration)||0); }, 0);
                            const baseDur = parseInt((self.selectedService||{}).duration) || 0;
                            const dur     = Math.max(1, optsDur > 0 ? optsDur : baseDur);
                            const staffId = (self.selectedStaff && self.selectedStaff.id) ? Number(self.selectedStaff.id) : 0;
                            newSvcWindows.push({
                                staffId:          staffId,
                                offsetMs:         0,
                                durationMs:       dur * 60000,
                                eligibleStaffIds: (self.selectedService && self.selectedService._eligibleStaffIds) || [],
                                label:            staffId !== 0 ? self.getFirstName(self.selectedStaff.name) : __('Any Staff Available')
                            });
                        }

                        // Legacy: namedStaffInNew kept for the cart-item time-overlap guard below
                        const namedStaffInNew = newSvcWindows.filter(w => w.staffId !== 0).map(w => w.staffId);
                        // busyWindows kept for compatibility with click handler (data-busy-staff-ids)
                        const busyWindows = []; // populated in slot loop below via _isBusy()

                        const newDurForRender = Math.max(1, (function() {
                            if (self.pendingServices.length > 0) {
                                return self._durationByGuest(self.pendingServices).totalDuration;
                            }
                            const opts = self.selectedOptions || [];
                            return opts.reduce(function(s,o){return s+(parseInt(o.duration)||0);}, 0)
                                || parseInt((self.selectedService||{}).duration||0);
                        })());

                        let html = '<div class="sbs-time-groups">';
                        let currentBucket = '';
                        visibleTimes.forEach(function(time) {
                            const slotStartMs = new Date(time.start_at).getTime();
                            const bucket = self._timeBucket(time.display, time.start_at, time.time);
                            if (bucket !== currentBucket) {
                                if (currentBucket) html += '</div>';
                                currentBucket = bucket;
                                html += `<div class="sbs-time-group"><div class="sbs-time-group-title">${bucket}</div>`;
                            }

                            // -- Evaluate each service window in the new appointment ----------
                            // For named staff: conflict if their window overlaps any cart busy interval.
                            // For Any staff:   conflict only if ALL eligible staff are busy ->slot blocked.
                            //                  If some are free ->slot is fine, system assigns a free one.
                            const warningLabels    = []; // visible "staff busy" hints (named staff only)
                            const conflictStaffIds = []; // staff IDs to pass to click handler
                            let   hardBlock        = false; // true = ALL eligible staff busy ->truly unavailable

                            newSvcWindows.forEach(function(sw) {
                                const wStartMs = slotStartMs + sw.offsetMs;
                                const wEndMs   = wStartMs + sw.durationMs;

                                if (sw.staffId !== 0) {
                                    // -- Named staff ----------------------------------------
                                    if (_isBusy(sw.staffId, wStartMs, wEndMs)) {
                                        const label = sw.label + ' busy (Appt 1)';
                                        warningLabels.push(label);
                                        conflictStaffIds.push(sw.staffId);
                                    }
                                } else {
                                    // -- Any staff ------------------------------------------
                                    // Check each eligible staff member.
                                    // If at least one is free ->no problem (slot stays open).
                                    // If none are free ->hard block (show as unavailable).
                                    let eligible = sw.eligibleStaffIds;

                                    // BUGFIX: If eligibleStaffIds is empty (not yet populated),
                                    // fall back to every staff member tracked in staffBusyMap.
                                    // This prevents skipping the conflict check entirely when
                                    // only one staff is working on that day.
                                    if (eligible.length === 0) {
                                        eligible = Object.keys(staffBusyMap).map(Number);
                                    }
                                    if (eligible.length === 0) return; // truly no data ->skip

                                    const freeStaff  = eligible.filter(sid => !_isBusy(sid, wStartMs, wEndMs));
                                    const busyStaff  = eligible.filter(sid =>  _isBusy(sid, wStartMs, wEndMs));

                                    if (freeStaff.length === 0) {
                                        // All eligible staff busy ->genuinely no one available
                                        hardBlock = true;
                                        warningLabels.push(__('All staff busy (Appt 1)'));
                                    }
                                    // Some busy but others free ->no warning needed; system will pick a free one
                                }
                            });

                            if (hardBlock) {
                                html += `<div class="sbs-time-slot sbs-time-slot-unavail" role="button" tabindex="-1" data-time="${time.time}" data-start-at="${time.start_at}" title="No staff available at this time" aria-disabled="true">
                                    <span class="sbs-slot-time">${time.display}</span>
                                    <span class="sbs-slot-tag unavail">Unavailable</span>
                                </div>`;
                            } else if (warningLabels.length > 0) {
                                const combinedLabel = warningLabels.join(', ');
                                const staffIdsAttr  = conflictStaffIds.join(',');
                                html += `<div class="sbs-time-slot sbs-time-slot-busy" role="button" tabindex="0" data-time="${time.time}" data-start-at="${time.start_at}" data-busy="${combinedLabel}" data-busy-staff-ids="${staffIdsAttr}" title="${combinedLabel} - tap to reassign">
                                    <span class="sbs-slot-time">${time.display}</span>
                                    <span class="sbs-slot-tag busy">Reassign</span>
                                </div>`;
                            } else {
                                const staffMapAttr = time.staff_map ? ` data-staff-map='${JSON.stringify(time.staff_map)}'` : '';
                                html += `<div class="sbs-time-slot" role="button" tabindex="0" data-time="${time.time}" data-start-at="${time.start_at}"${staffMapAttr}>
                                    <span class="sbs-slot-time">${time.display}</span>
                                    <span class="sbs-slot-check" aria-hidden="true">&#10003;</span>
                                </div>`;
                            }
                        });
                        if (currentBucket) html += '</div>';
                        html += '</div>';
                        container.html(html);
                        self._renderNextOpenings(visibleTimes, date);
                        $('#sbs-sync-status').html(visibleTimes.length + ' times available from Square');
                    } else {
                        self._updateDateAvailabilityMeter(date, 0);
                        // Server said no availability (could be early fast-return)
                        const msg = responseTimes.length > 0
                            ? __('No remaining available times today.')
                            : ((response.data && response.data.message) ? response.data.message : null);
                        $('#sbs-next-openings').hide().empty();
                        container.html(self._noTimesHtml(msg, date));
                        $('#sbs-sync-status').html('');
                    }
                },
                error: function(xhr, status) {
                    if (requestSeq !== self._timesSeq || status === 'abort' || (xhr && xhr.status === 0)) return;
                    // Check if it's a timeout or a real error with a clean message
                    let isNoAvail = false;
                    let errMsg = null;
                    if (status === 'timeout') {
                        errMsg = __('Network timeout while checking availability.');
                        isNoAvail = true;
                    } else {
                        try {
                            const r = JSON.parse(xhr.responseText);
                            if (r && r.data && r.data.message) {
                                errMsg = r.data.message;
                                isNoAvail = !!(r.data && r.data.no_availability);
                            }
                        } catch(e) {}
                    }
                    container.html(self._noTimesHtml(errMsg, date));
                    self._updateDateAvailabilityMeter(date, 0);
                    $('#sbs-next-openings').hide().empty();
                    $('#sbs-sync-status').html('');
                },
                complete: function() {
                    if (requestSeq === self._timesSeq && self._timesRequest && self._timesRequest.readyState === 4) {
                        self._timesRequest = null;
                    }
                }
            });
        }

        _renderNextOpenings(times, date) {
            const $wrap = $('#sbs-next-openings');
            if (!$wrap.length || !Array.isArray(times) || times.length === 0) return;

            const openTimes = times.filter(function(time) {
                return time && time.display && time.start_at;
            }).slice(0, 3);
            if (openTimes.length === 0) {
                $wrap.hide().empty();
                return;
            }

            const dateParts = String(date || '').split('-');
            let dateLabel = __('Selected date');
            if (dateParts.length === 3) {
                const dateObj = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
                dateLabel = dateObj.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' });
            }

            const chips = openTimes.map(function(time, index) {
                const label = index === 0 ? __('Earliest') : __('Open');
                return `<button type="button" class="sbs-next-opening-chip" data-start-at="${time.start_at}" data-time="${time.time || ''}">
                    <span>${label}</span><strong>${time.display}</strong>
                </button>`;
            }).join('');

            $wrap.html(`
                <div class="sbs-next-openings-head">
                    <span>${__('Recommended openings')}</span>
                    <strong>${dateLabel}</strong>
                </div>
                <div class="sbs-next-openings-list">${chips}</div>
            `).show();
        }

        _availabilityLevel(count) {
            if (count === null || typeof count === 'undefined') return 'checking';
            if (count <= 0) return 'none';
            if (count < 4) return 'low';
            if (count < 8) return 'medium';
            return 'high';
        }

        /**
         * 4.6.2: fullness classes for a day cell, on top of the density meter.
         *
         * The meter bar alone is a fine secondary cue but a poor primary one -
         * it is small, and at density 0 an empty bar looks identical to "not
         * loaded yet". These give the stylesheet something unambiguous to hang
         * a "sold out" / "almost gone" treatment on. Class names only; the
         * visual design lives in the CSS, which is owned elsewhere.
         *
         * @param {number|null|undefined} count
         * @returns {string[]}
         */
        _dayFullnessClasses(count) {
            if (count === null || typeof count === 'undefined') return [];
            if (count <= 0) return ['sbs-day-full'];
            if (count < 4)  return ['sbs-day-busy'];
            return [];
        }

        /**
         * 4.6.2: accessible name for a date button.
         *
         * The week view rendered "Sep / Mon / 5" as three separate spans, so a
         * screen reader announced the button as "Sep Mon 5" with no year, and
         * the month view announced a bare "5". Neither said anything about
         * availability, which is the one fact the button exists to convey - a
         * blind customer had to open every day to find out it was full.
         *
         * @param {string}                dateStr Y-m-d.
         * @param {number|null|undefined} count   Remaining slots, if known.
         */
        _dateButtonAriaLabel(dateStr, count) {
            const parts = String(dateStr || '').split('-').map(Number);
            let dateLabel = String(dateStr || '');
            if (parts.length === 3 && !parts.some(isNaN)) {
                const d = new Date(parts[0], parts[1] - 1, parts[2]);
                dateLabel = d.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
            }
            if (count === null || typeof count === 'undefined') {
                return dateLabel;
            }
            const availability = count > 0
                // Translators: %d is the number of bookable time slots left.
                ? __('%d time slots available').replace('%d', count)
                : __('Fully booked');
            return `${dateLabel}, ${availability}`;
        }

        _updateDateAvailabilityMeter(date, count) {
            if (!date) return;
            this._dateAvailability[date] = count;
            const level = this._availabilityLevel(count);
            const known = !(count === null || typeof count === 'undefined');
            const fullnessClasses = this._dayFullnessClasses(count);
            const $buttons = $(`.sbs-day-button[data-date="${date}"]`);
            $buttons
                .removeClass('sbs-availability-checking sbs-availability-none sbs-availability-low sbs-availability-medium sbs-availability-high')
                // 4.6.2: clear BOTH fullness classes before re-applying, so a day
                // that refills (staff swapped, cart item removed) drops its stale
                // "sold out" styling instead of keeping it for the session.
                .removeClass('sbs-day-full sbs-day-busy')
                .addClass(`sbs-availability-${level}`)
                .attr('data-availability-count', known ? count : '')
                // 4.6.2: this used to APPEND the availability phrase to whatever
                // aria-label was already there and strip the previous phrase with
                // a regex of hardcoded English. Under any translation the regex
                // missed, so the label grew a new clause on every refresh
                // ("Mon 5, 3 times available, 0 times available, ..."). Build the
                // whole label from the date and the count instead.
                .attr('aria-label', this._dateButtonAriaLabel(date, known ? count : null));
            if (fullnessClasses.length) $buttons.addClass(fullnessClasses.join(' '));
        }

        // -- 4.6.2: calendar availability density --------------------------------
        //
        // _dateAvailability was only ever written for the ONE day whose slots had
        // just been fetched (see the _updateDateAvailabilityMeter calls inside
        // _loadAvailableTimesNow), so the .sbs-date-availability-meter bars that
        // the week and month views already render were blank on every other cell.
        // The customer had to open days one at a time to discover which had
        // openings, and two or three misses in a row is a classic abandonment
        // point.
        //
        // The server exposes yatoon_get_availability_density, which answers a
        // whole visible range in one call. These methods batch-fetch the range,
        // fill _dateAvailability, and paint the cells that are already on screen.
        //
        // This is an ENHANCEMENT, not a requirement: every failure path is silent
        // and simply leaves the calendar looking exactly as it did in 4.6.1.
        // Never toast an error from this code - the customer did not ask for the
        // heat map and cannot act on its failure.

        /**
         * Service ids the density query should account for.
         * The endpoint caps at 5, so cap here too rather than shipping ids the
         * server will silently drop.
         */
        _densityServiceIds() {
            const ids = [];
            const push = (id) => {
                const n = parseInt(id, 10);
                if (n > 0 && ids.indexOf(n) === -1) ids.push(n);
            };
            (this.pendingServices || []).forEach((ps) => { if (ps && ps.service) push(ps.service.id); });
            if (this.selectedService) push(this.selectedService.id);
            return ids.slice(0, 5);
        }

        /**
         * Staff id to scope the density query to.
         *
         * Only meaningful when the whole visit is with one named person. With
         * several different staff, or with "Any staff" (id 0), send 0 and let the
         * server answer for the salon - a per-staff number would be wrong for at
         * least one of the services in the basket.
         */
        _densityStaffId() {
            const pending = this.pendingServices || [];
            if (pending.length > 0) {
                const ids = pending
                    .map((ps) => (ps && ps.staff ? parseInt(ps.staff.id, 10) || 0 : 0))
                    .filter((id) => id > 0);
                return (ids.length === pending.length && new Set(ids).size === 1) ? ids[0] : 0;
            }
            return this.selectedStaff ? (parseInt(this.selectedStaff.id, 10) || 0) : 0;
        }

        /**
         * The Y-m-d range currently on screen, or null when none is visible.
         *
         * Month view is capped at 31 days by construction. The server truncates
         * anything longer, and a truncated answer would silently leave the tail
         * of the grid blank, so never send more than that.
         */
        _visibleDateRange() {
            const fmt = (d) => `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
            let start = null;
            let end   = null;

            if ($('.sbs-month-view').hasClass('active') && this._monthDate) {
                const y = this._monthDate.getFullYear();
                const m = this._monthDate.getMonth();
                start = new Date(y, m, 1);
                end   = new Date(y, m + 1, 0);
            } else if (this._weekStart) {
                start = new Date(this._weekStart);
                end   = new Date(this._weekStart);
                end.setDate(end.getDate() + 6);
            } else {
                return null;
            }
            start.setHours(0, 0, 0, 0);
            end.setHours(0, 0, 0, 0);

            // Past days are disabled in both views, so asking about them only
            // buys the server work nobody will look at.
            const today = ysSiteNow();
            today.setHours(0, 0, 0, 0);
            if (end < today) return null;
            if (start < today) start = today;

            const maxEnd = new Date(start);
            maxEnd.setDate(maxEnd.getDate() + 30); // 31 days inclusive
            if (end > maxEnd) end = maxEnd;

            return { start: fmt(start), end: fmt(end) };
        }

        /**
         * Fetch the density for the visible range and paint it.
         *
         * @param {boolean} force Re-fetch even when the request key is unchanged
         *                        (staff swap, cart edit - same dates, new answer).
         */
        loadAvailabilityDensity(force) {
            const self = this;
            if (typeof yatoonData === 'undefined' || !yatoonData || !yatoonData.ajax_url) return;

            const serviceIds = self._densityServiceIds();
            if (!serviceIds.length) return;      // nothing chosen yet - nothing to ask about
            const range = self._visibleDateRange();
            if (!range) return;

            const staffId    = self._densityStaffId();
            const locationId = parseInt(yatoonData.locationId, 10) || 0;
            const key = [range.start, range.end, staffId, locationId, serviceIds.join(',')].join('|');
            // Week/month prev-next and the view toggle can all re-ask for a range
            // that is already painted. Skip the round trip unless the caller knows
            // the answer changed.
            if (!force && key === self._densityKey) return;
            self._densityKey = key;

            // Same supersede pattern as the options/staff/times lookups: bump a
            // sequence number, abort whatever is still in flight, and drop any
            // response that is no longer the newest. Without it, paging quickly
            // through months repaints the grid with a stale month's numbers.
            const requestSeq = ++self._densitySeq;
            if (self._densityRequest && self._densityRequest.readyState !== 4) {
                try { self._densityRequest.abort(); } catch (e) {}
            }

            self._densityRequest = $.ajax({
                url: yatoonData.ajax_url,
                type: 'POST',
                timeout: 20000,
                data: {
                    action:      'yatoon_get_availability_density',
                    nonce:       yatoonData.nonce,
                    start_date:  range.start,
                    end_date:    range.end,
                    staff_id:    staffId,
                    location_id: locationId,
                    service_ids: serviceIds.join(',')
                },
                success: function(response) {
                    if (requestSeq !== self._densitySeq) return;
                    if (!response || !response.success || !response.data || !response.data.density) {
                        // A refusal still arrives as HTTP 200 (wp_send_json_error),
                        // e.g. the endpoint's public rate limit. Forget the cached
                        // key so the next navigation retries instead of treating
                        // this range as permanently answered.
                        self._densityKey = '';
                        return;
                    }
                    const density = response.data.density;
                    Object.keys(density).forEach(function(dateStr) {
                        const count = parseInt(density[dateStr], 10);
                        if (isNaN(count)) return;
                        self._dateAvailability[dateStr] = count;
                    });
                    self._applyDensityToCalendar();
                },
                error: function() {
                    // Silent by design. A missing heat map is a cosmetic loss; an
                    // error toast mid-booking is a reason to leave. Clear the
                    // cached key so the next navigation retries rather than
                    // trusting a fetch that never landed.
                    if (requestSeq === self._densitySeq) self._densityKey = '';
                },
                complete: function() {
                    if (requestSeq === self._densitySeq && self._densityRequest && self._densityRequest.readyState === 4) {
                        self._densityRequest = null;
                    }
                }
            });
        }

        /**
         * 4.6.2: paint _dateAvailability onto the day buttons already in the DOM.
         *
         * Deliberately NOT a re-render of the week/month view: those functions
         * rebuild their HTML from scratch and would drop the .selected class off
         * the customer's chosen day (and with it the highlight next to the slot
         * list they are reading). Updating attributes in place keeps selection,
         * focus and scroll position intact. Freshly rendered views pick the same
         * data up on their own, because both read _dateAvailability while
         * building each cell.
         */
        _applyDensityToCalendar() {
            const self = this;
            $('.sbs-day-button[data-date]').each(function() {
                const $btn = $(this);
                const date = String($btn.attr('data-date') || '');
                if (!date || !Object.prototype.hasOwnProperty.call(self._dateAvailability, date)) return;
                const count = self._dateAvailability[date];
                if (count === null || typeof count === 'undefined') return;
                const fullness = self._dayFullnessClasses(count);
                $btn
                    .removeClass('sbs-availability-checking sbs-availability-none sbs-availability-low sbs-availability-medium sbs-availability-high')
                    .removeClass('sbs-day-full sbs-day-busy')
                    .addClass('sbs-availability-' + self._availabilityLevel(count))
                    .attr('data-availability-count', count)
                    .attr('aria-label', self._dateButtonAriaLabel(date, count));
                if (fullness.length) $btn.addClass(fullness.join(' '));
            });
        }

        _availabilitySelectionKey() {
            const services = this.pendingServices && this.pendingServices.length
                ? this.pendingServices
                : (this.selectedService ? [{ service: this.selectedService, options: this.selectedOptions || [], staff: this.selectedStaff }] : []);
            return services.map((item) => {
                const svc = item.service ? item.service.id : 0;
                const staff = item.staff ? item.staff.id : 0;
                const opts = (item.options || []).map(o => o.id).sort().join(',');
                return [svc, staff, opts].join(':');
            }).join('|');
        }

        _isTodayDate(dateStr) {
            if (!dateStr) return false;
            const now = new Date();
            const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
            return String(dateStr) === today;
        }

        _filterPastSlotsForDate(times, dateStr) {
            if (!Array.isArray(times) || !times.length) return [];
            if (!this._isTodayDate(dateStr)) return times;
            const nowMs = Date.now();
            return times.filter(function(time) {
                if (!time || !time.start_at) return true;
                const slotMs = new Date(time.start_at).getTime();
                return isNaN(slotMs) || slotMs > nowMs;
            });
        }

        _dateLabel(dateStr) {
            if (!dateStr) return 'this date';
            const parts = String(dateStr).split('-').map(Number);
            if (parts.length !== 3 || parts.some(isNaN)) return 'this date';
            const d = new Date(parts[0], parts[1] - 1, parts[2]);
            if (this._isTodayDate(dateStr)) return 'today';
            return d.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' });
        }

        // -- Find next available date and jump straight to it ------------------
        // $btn: the jQuery "Go to next available" button (for loading state).
        _findAndGoNextAvailable($btn) {
            const self = this;

            // Build request params - pendingServices (multi) takes priority
            let serviceId = 0, staffId = 0, optionIds = '';
            let allStaffIds = '', allOptionIdsPerStaff = '';

            if (self.pendingServices && self.pendingServices.length > 0) {
                const pending        = self.pendingServices;
                serviceId            = pending[0].service ? pending[0].service.id : 0;
                staffId              = pending[0].staff   ? pending[0].staff.id   : 0;
                optionIds            = JSON.stringify(pending[0].options.map(o => o.id));
                allStaffIds          = JSON.stringify(pending.map(s => s.staff ? s.staff.id : 0));
                allOptionIdsPerStaff = JSON.stringify(pending.map(s => s.options.map(o => o.id)));
                var allGuestLabels   = JSON.stringify(pending.map(s => s.guestLabel || 'Me'));
            } else {
                serviceId = self.selectedService ? self.selectedService.id : 0;
                staffId   = self.selectedStaff   ? self.selectedStaff.id   : 0;
                // KEY: only main (non-addon) options for Square availability query
                const mainOpts = self.selectedOptions ? self.selectedOptions.filter(o => !o.is_addon) : [];
                optionIds = mainOpts.length ? JSON.stringify(mainOpts.map(o => o.id)) : '';
                var allGuestLabels = '';
            }

            if (!serviceId) return;

            // "From" date = currently selected day, or today
            let fromDate = $('.sbs-day-button.selected').data('date') || $('.sbs-day-button.active').data('date');
            if (!fromDate) {
                const t = new Date();
                fromDate = `${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,'0')}-${String(t.getDate()).padStart(2,'0')}`;
            }

            // Show spinner on button
            if ($btn && $btn.length) {
                $btn.prop('disabled', true).html(__('Searching...'));
            }

            $.ajax({
                url:     yatoonData.ajax_url,
                type:    'POST',
                timeout: 45000,
                data: {
                    action:         'yatoon_get_next_available',
                    nonce:          yatoonData.nonce,
                    service_id:     serviceId,
                    staff_id:       staffId,
                    from_date:      fromDate,
                    option_ids:     optionIds,
                    all_staff_ids:  allStaffIds,
                    all_option_ids: allOptionIdsPerStaff,
                    all_guest_labels: allGuestLabels,
                    addon_duration: (function() {
                        const opts = self.pendingServices && self.pendingServices.length > 0
                            ? self.pendingServices[0].options
                            : (self.selectedOptions || []);
                        return opts.filter(o => o.is_addon).reduce((s, o) => s + (parseInt(o.duration) || 0), 0);
                    })(),
                    total_duration: (function() {
                        if (self.pendingServices && self.pendingServices.length > 0) {
                            return self._durationByGuest(self.pendingServices).totalDuration;
                        }
                        const opts = self.selectedOptions || [];
                        return opts.reduce((s,o)=>s+(parseInt(o.duration)||0), 0) || parseInt((self.selectedService||{}).duration||0);
                    })()
                },
                success: function(response) {
                    if ($btn && $btn.length) $btn.prop('disabled', false).html(__('Find next opening'));

                    if (response.success && response.data.date) {
                        const nextDate = response.data.date; // e.g. "2026-03-05"
                        self._showToast(__('Next opening found. Choose a time below.'), 'success');

                        // Directly jump the week view to the week containing nextDate,
                        // then select that day and load its times - no repeated clicking.
                        const parts  = nextDate.split('-');
                        const target = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
                        target.setHours(0, 0, 0, 0);

                        // Set _weekStart directly to the target date so today's/next date is visible first.
                        const targetWeekSunday = new Date(target);

                        if (self._weekStart && self._renderWeekView) {
                            // Direct jump: set week, re-render, then select the day
                            self._weekStart.setTime(targetWeekSunday.getTime());
                            self._renderWeekView();
                            // After render, select the day button and load times
                            const $dayBtn = $(`.sbs-day-button[data-date="${nextDate}"]`);
                            if ($dayBtn.length) {
                                $('.sbs-day-button').removeClass('selected');
                                $dayBtn.addClass('selected');
                                self.loadAvailableTimes(nextDate, true);
                            } else {
                                // Fallback: load times directly
                                self.loadAvailableTimes(nextDate, true);
                            }
                        } else {
                            // Fallback: click-based navigation
                            let safetyLimit = 26;
                            const _navigate = function() {
                                const $dayBtn = $(`.sbs-day-button[data-date="${nextDate}"]`);
                                if ($dayBtn.length) {
                                    $('.sbs-day-button').removeClass('selected');
                                    $dayBtn.addClass('selected');
                                    self.loadAvailableTimes(nextDate, true);
                                } else if (safetyLimit-- > 0) {
                                    const $last = $('.sbs-day-button').last();
                                    ($last.data('date') < nextDate ? $('.sbs-week-next') : $('.sbs-week-prev')).trigger('click');
                                    setTimeout(_navigate, 300);
                                } else {
                                    self.loadAvailableTimes(nextDate, true);
                                }
                            };
                            setTimeout(_navigate, 100);
                        }
                    } else {
                        // Nothing in the next 60 days
                        if ($btn && $btn.length) $btn.prop('disabled', false).text(__('No availability found'));
                        self._showToast(__('No upcoming openings found. Join the waitlist and the salon can notify you when a spot opens.'), 'info');
                    }
                },
                error: function() {
                    if ($btn && $btn.length) $btn.prop('disabled', false).html(__('Find next opening'));
                }
            });
        }

        _noTimesHtml(msg, dateStr) {
            const rawMsg = String(msg || '').trim();
            const lower = rawMsg.toLowerCase();
            const isNetwork = lower.includes('network') || lower.includes('timeout') || lower.includes('request failed');
            const isConnection = lower.includes('square') || lower.includes('vagaro') || lower.includes('connection') || lower.includes('api');
            const isClosed = lower.includes('closed') || lower.includes('passed') || lower.includes('business hours');
            const staffName = this.selectedStaff && parseInt(this.selectedStaff.id) !== 0 ? this._esc(this.getFirstName(this.selectedStaff.name)) : null;
            const serviceName = this.selectedService && this.selectedService.name ? this._esc(this.selectedService.name) : 'this service';
            const pendingCount = this.pendingServices ? this.pendingServices.length : 0;
            const serviceLabel = pendingCount > 1 ? `your ${pendingCount}-service appointment` : serviceName;
            const dateLabel = this._dateLabel(dateStr);

            let label = __('No times found');
            let headline = rawMsg || `No available times for ${dateLabel}.`;
            let subtext = yatoonData.availabilityHelp || __('Try another date, choose another staff member, edit your services, or text us and we can help.');

            if (isNetwork) {
                label = __('Network error');
                headline = __('We could not load times just now.');
                subtext = __('Please try again. If it keeps happening, text us and we can help you book.');
            } else if (isConnection) {
                label = __('Calendar connection issue');
                headline = __('We could not check the live booking calendar.');
                subtext = __('Square/Vagaro may be temporarily unavailable. Try again or text us for help.');
            } else if (staffName) {
                label = __('Staff unavailable');
                headline = `${staffName} has no availability ${dateLabel}.`;
            } else if (!isClosed) {
                label = __('No availability');
                headline = `No available times for ${serviceLabel} ${dateLabel}.`;
            }

            const chooseStaffBtn = staffName
                ? `<button type="button" class="sbs-try-other-staff">Choose another staff</button>`
                : `<button type="button" class="sbs-availability-choose-staff">Choose staff</button>`;
            const waitlistBtn = yatoonData.waitlistEnabled
                ? `<button type="button" class="sbs-join-waitlist">Join Waitlist</button>`
                : '';
            const textUsBtn = yatoonData.textUsEnabled
                ? `<button type="button" class="sbs-text-us-inline">Text us</button>`
                : '';

            return `
                <div class="sbs-empty-state sbs-availability-empty">
                    <div class="sbs-empty-illustration">
                        <svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
                            <rect x="8" y="12" width="48" height="44" rx="6" fill="#f3f4f6" stroke="#e5e7eb" stroke-width="2"/>
                            <rect x="8" y="12" width="48" height="12" rx="6" fill="#e5e7eb"/>
                            <rect x="18" y="32" width="8" height="8" rx="2" fill="#d1d5db"/>
                            <rect x="30" y="32" width="8" height="8" rx="2" fill="#d1d5db"/>
                            <rect x="42" y="32" width="8" height="8" rx="2" fill="#d1d5db"/>
                            <rect x="18" y="44" width="8" height="8" rx="2" fill="#d1d5db"/>
                            <rect x="30" y="44" width="8" height="8" rx="2" fill="#d1d5db"/>
                            <circle cx="50" cy="50" r="10" fill="#fef3c7" stroke="#fcd34d" stroke-width="2"/>
                            <text x="50" y="54" text-anchor="middle" font-size="12" font-weight="700" fill="#92400e">!</text>
                        </svg>
                    </div>
                    <span class="sbs-availability-label">${this._esc(label)}</span>
                    <p class="sbs-empty-title">${this._esc(headline)}</p>
                    <p class="sbs-empty-subtext">${this._esc(subtext)}</p>
                    <div class="sbs-empty-actions sbs-availability-actions">
                        <button type="button" class="sbs-try-next-date">Find next opening</button>
                        ${chooseStaffBtn}
                        ${textUsBtn}
                        ${waitlistBtn}
                    </div>
                </div>`;
        }

        _calcDurationPrice() {
            if (this.selectedOptions.length > 0) {
                return { duration: this.selectedOptions[0].duration, price: this.selectedOptions[0].price, hasExact: true };
            }
            return { duration: 0, price: this.selectedService ? this.selectedService.price : 0, hasExact: false };
        }

        // FIX: Summary now shows category + service name, with X remove buttons
        updateSummary() {
            this._updateStepAccordion();
            const hasPending  = this.pendingServices.length > 0;
            const hasService  = hasPending || !!this.selectedService;
            const $summaryBox = $('.sbs-booking-summary');
            const self        = this;
            // -- Confirmed cart items section (shown at top during steps 1-4) --
            let cartSectionHtml = '';
            if (this.bookingCart.length > 0) {
                let cartRows = '';
                this.bookingCart.forEach(function(item, idx) {
                    const title    = __('Appointment') + ' ' + (idx + 1);
                    const services = item.services || [{ service: item.service, options: item.options, staff: item.staff }];
                    const svcSummary = services.map(function(s) {
                        const label     = self._svcDisplayLabelHtml(s.service, s.options);
                        const staffName = s.staff ? self.getFirstName(s.staff.name) : '-';
                        return '<div style="font-size:12px;color:#374151;"><strong>' + label + '</strong><br><span style="color:#6b7280;">' + staffName + '</span></div>';
                    }).join('');
                    const parts = item.dateTime.date.split('-');
                    const dObj  = new Date(parts[0], parts[1]-1, parts[2]);
                    const dStr  = dObj.toLocaleDateString(undefined, { month:'short', day:'numeric' });
                    cartRows += '<div class="sbs-checkout-appt-card" style="border:1px solid #e5e7eb;border-radius:8px;padding:10px 12px;margin-bottom:8px;background:#fff;">'
                        + '<div class="sbs-checkout-appt-head" style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px;">'
                        + '<span class="sbs-checkout-appt-title" style="font-size:12px;font-weight:700;color:#111;">' + title + '</span>'
                        + '<div class="sbs-checkout-actions" style="display:flex;gap:4px;">'
                        + '<button type="button" class="sbs-sidebar-cart-edit" data-index="' + idx + '" style="background:#e0e7ff;color:#4338ca;border:none;padding:2px 8px;border-radius:4px;cursor:pointer;font-size:11px;font-weight:600;">Edit</button>'
                        + '<button type="button" class="sbs-sidebar-cart-remove" data-index="' + idx + '" style="background:#fee2e2;color:#dc2626;border:none;padding:2px 8px;border-radius:4px;cursor:pointer;font-size:11px;font-weight:600;">Remove</button>'
                        + '</div></div>'
                        + svcSummary
                        + '<div class="sbs-checkout-time" style="font-size:11px;color:#9ca3af;margin-top:4px;">' + dStr + ' at ' + item.dateTime.display + '</div>'
                        + '</div>';
                });
                const MAX_APPTS  = 2;
                const canAddMore = this.bookingCart.length < MAX_APPTS;
                // On Step 5 (Review & Book), don't show the "In Progress" header -                 // there is no appointment being built, just confirmed ones.
                const isStep5 = this.currentStep === 5;
                cartSectionHtml  = '<div style="margin-bottom:12px;">'
                    + '<div style="font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;font-weight:600;margin-bottom:8px;">Confirmed (' + this.bookingCart.length + '/' + MAX_APPTS + ')</div>'
                    + cartRows
                    + (!isStep5 && canAddMore ? '<div style="border-top:1px solid #f3f4f6;margin:10px 0;"></div>'
                        + '<div style="font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;font-weight:600;margin-bottom:8px;">Appointment ' + (this.bookingCart.length + 1) + ' - In Progress</div>'
                        : (!isStep5 ? '<div style="font-size:12px;color:#dc2626;text-align:center;padding:6px;background:#fee2e2;border-radius:6px;">Maximum 2 appointments reached</div>' : ''))
                    + '</div>';
            }

            if (hasPending) {
                // Multi-service mode
                let rowsHtml = '';
                // Sort for display: group by person (Me first, then guests in first-seen order)
                // Keep original idx so edit/remove buttons still point to correct pendingServices slot
                const pendingWithIdx = this.pendingServices.map(function(s, idx) { return { s: s, idx: idx }; });
                const seenLabels = [];
                pendingWithIdx.forEach(function(item) {
                    const lbl = item.s.guestLabel || 'Me';
                    if (seenLabels.indexOf(lbl) === -1) seenLabels.push(lbl);
                });
                seenLabels.sort(function(a, b) {
                    if (a === 'Me') return -1;
                    if (b === 'Me') return 1;
                    return 0;
                });
                const sortedForDisplay = [];
                seenLabels.forEach(function(lbl) {
                    pendingWithIdx.forEach(function(item) {
                        if ((item.s.guestLabel || 'Me') === lbl) sortedForDisplay.push(item);
                    });
                });
                sortedForDisplay.forEach(function(item) {
                    const s = item.s; const idx = item.idx;
                    const svcLabel   = self._svcDisplayLabelHtml(s.service, s.options);
                    const svcPrice   = self._optionsPrice(s.service, s.options);
                    const svcDur     = s.options.length > 0 ? s.options.reduce(function(d,o){ return d+(o.duration||0); }, 0) : (s.service.duration||0);
                    const gLabel     = s.guestLabel || 'Me';
                    const safeGLabel = self._esc(self._displayGuestLabel(gLabel));
                    const hasPeople  = self.pendingServices.some(function(ps){ return ps.guestLabel && ps.guestLabel !== 'Me'; });
                    const personBadge = (hasPeople || gLabel !== 'Me')
                        ? `<span style="display:inline-block;background:${gLabel === 'Me' ? '#dbeafe' : '#dcfce7'};color:${gLabel === 'Me' ? '#1d4ed8' : '#16a34a'};border-radius:4px;font-size:10px;font-weight:700;padding:1px 6px;margin-right:4px;">${safeGLabel}</span>`
                        : '';
                    const personSep = personBadge ? `<span style="color:#9ca3af;margin-right:4px;">-</span>` : '';
                    const staffSelectHtml = self._buildStaffSelect(s, idx);
                    rowsHtml += `
                        <div class="sbs-summary-item sbs-summary-pending-row" data-pending-idx="${idx}"
                             style="flex-direction:column;align-items:stretch;border-bottom:1px solid #f3f4f6;padding-bottom:10px;margin-bottom:6px;">
                            <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:6px;">
                                <div style="flex:1;min-width:0;">
                                    <div style="font-size:13px;font-weight:600;color:#111;line-height:1.3;">${personBadge}${personSep}${svcLabel}</div>
                                    <div style="font-size:12px;color:#6b7280;margin-top:3px;">${staffSelectHtml} &nbsp;&middot;&nbsp; ${svcDur}min &nbsp;&middot;&nbsp; ${ysFormatMoney(svcPrice)}</div>
                                </div>
                                <div style="display:flex;gap:4px;flex-shrink:0;margin-top:2px;">
                                    <button type="button" class="sbs-pending-edit-btn" data-idx="${idx}"
                                            style="background:#f0fdf4;color:#15803d;border:1px solid #bbf7d0;border-radius:5px;padding:3px 8px;font-size:11px;font-weight:600;cursor:pointer;">Edit</button>
                                    <button type="button" class="sbs-pending-remove-btn sbs-summary-remove-service-btn" data-idx="${idx}"
                                            aria-label="${__('Remove this service')}" title="${__('Remove service')}">${__('Remove')}</button>
                                </div>
                            </div>
                        </div>`;
                });

                const totalDur   = this._durationByGuest(this.pendingServices).totalDuration;
                const totalPrice = this.pendingServices.reduce(function(sum,s){ return sum + self._optionsPrice(s.service, s.options); }, 0);

                let dtRow = '';
                if (this.selectedDateTime) {
                    const dtParts = this.selectedDateTime.date.split('-');
                    const dtObj   = new Date(dtParts[0], dtParts[1]-1, dtParts[2]);
                    const dtStr   = dtObj.toLocaleDateString(undefined, { weekday:'long', month:'long', day:'numeric' });
                    dtRow = `
                        <div class="sbs-summary-item" style="margin-top:4px;">
                            <div style="flex:1;">
                                <div style="font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;font-weight:600;margin-bottom:2px;">Date &amp; Time</div>
                                <span style="font-size:13px;color:#111;">${dtStr} at ${this.selectedDateTime.display}</span>
                            </div>
                            <div style="display:flex;gap:4px;">
                                <button type="button" class="sbs-summary-edit-btn" data-goto="4"
                                        style="background:#f3f4f6;color:#374151;border:none;border-radius:5px;padding:3px 8px;font-size:11px;font-weight:600;cursor:pointer;">Edit</button>
                            </div>
                        </div>`;
                } else {
                    dtRow = `
                        <div class="sbs-summary-item" style="margin-top:4px;">
                            <div style="flex:1;">
                                <div style="font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;font-weight:600;margin-bottom:2px;">Date &amp; Time</div>
                                <span style="font-size:13px;color:#9ca3af;">Not selected</span>
                            </div>
                        </div>`;
                }

                // 4.4.3: this panel rebuild writes its own total, so it needs
                // the tax line too - otherwise it silently overwrites the row
                // added by updateSummary() and shows the pre-tax figure again.
                const taxA = this._taxParts(totalPrice);
                const summaryHtml = `
                    <div style="font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;font-weight:600;margin-bottom:10px;">
                        ${this._selectionSummaryLabel()}
                    </div>
                    ${rowsHtml}
                    ${dtRow}
                    ${taxA.row}
                    <div class="sbs-summary-item sbs-summary-total" style="border-top:2px solid var(--sbs-line,#f3f4f6);margin-top:10px;padding-top:12px;">
                        <div>
                            <div style="font-weight:600;font-size:15px;">${__('Total')}</div>
                            <div style="font-size:12px;color:var(--sbs-muted,#6b7280);">${__('Appointment Length: %s min').replace('%s', totalDur)}</div>
                        </div>
                        <span id="sbs-summary-total" style="font-size:18px;font-weight:700;color:var(--primary-color,#ec4899);">${ysFormatMoney(taxA.total)}</span>
                    </div>
                    ${this._depositRowHtml(taxA.total)}`;

                $summaryBox.html(cartSectionHtml + summaryHtml);
                // Bind inline staff dropdown change
                $summaryBox.off('change.inlineStaff').on('change.inlineStaff', '.sbs-inline-staff-sel', function() {
                    const svcId   = $(this).data('svc-id');
                    const newId   = parseInt($(this).val());
                    const newName = $(this).find('option:selected').data('name');
                    const idx = parseInt($(this).data('pending-idx'));
                    // Prefer the exact pending-service index so duplicate services can each choose different staff.
                    const ps = (!Number.isNaN(idx) && self.pendingServices[idx])
                        ? self.pendingServices[idx]
                        : self.pendingServices.find(function(s) { return String(s.service.id) === String(svcId); });
                    if (ps) {
                        ps.staff = { id: newId, name: newName };
                        self.selectedDateTime = null;
                        $('#sbs-selected-start-at').val('');
                        $('.sbs-time-slot').removeClass('selected');
                        self._saveSession();
                        self.updateSummary();
                        self._showPendingServicesSummary();
                        // 4.6.2: as above - refresh the calendar heat map for the
                        // newly chosen staff member, not just the open day.
                        if (self.currentStep === 4) self.loadAvailabilityDensity(true);
                        // Reload available times for the new staff selection
                        const selDate = $('.sbs-day-button.selected').data('date');
                        if (selDate && self.currentStep === 4) {
                            self.loadAvailableTimes(selDate);
                        }
                    }
                });
                $summaryBox.off('click.pending').on('click.pending', '.sbs-pending-edit-btn', function() {
                    self._editPendingService(parseInt($(this).data('idx')));
                }).on('click.pending', '.sbs-pending-remove-btn', function() {
                    self._removePendingService(parseInt($(this).data('idx')));
                });

            } else if (this.currentStep === 5 && this.bookingCart.length > 0) {
                // Step 5 (Review & Book): only show confirmed cart items, no in-progress rows
                $summaryBox.html(cartSectionHtml);
                // updateSummaryForCheckout() will append the total row
            } else {
                // Single service in-progress (steps 1-3)
                const calcResult     = this._calcDurationPrice();
                const duration       = calcResult.duration;
                const price          = calcResult.price;
                const hasExact       = calcResult.hasExact;
                const selectedStaffName = this.selectedStaff ? this._esc(this.getFirstName(this.selectedStaff.name)) : __('Not selected');
                let serviceDisplay   = __('No services added yet');
                if (this.selectedService) {
                    serviceDisplay = this.selectedService.category
                        ? this.selectedService.category + ' - ' + this.selectedService.name
                        : this.selectedService.name;
                }

                let optRow = '';
                if (this.selectedOptions.length > 0) {
                    optRow = `
                        <div class="sbs-summary-item" id="sbs-summary-options-container">
                            <div style="flex:1;">
                                <div style="font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;font-weight:600;margin-bottom:2px;">Options</div>
                                <span id="sbs-summary-options" style="font-size:14px;">${this.selectedOptions.map(function(o){return o.name;}).join(', ')}</span>
                            </div>
                            <div style="display:flex;gap:4px;align-items:center;">
                                <button type="button" class="sbs-summary-edit-btn" data-goto="2"
                                        style="background:#f3f4f6;color:#374151;border:none;border-radius:5px;padding:3px 8px;font-size:11px;font-weight:600;cursor:pointer;">Edit</button>
                            </div>
                        </div>`;
                }

                const dtColor  = this.selectedDateTime ? '#111' : '#9ca3af';
                let dtText     = __('Not selected');
                let dtEditBtn  = '';
                if (this.selectedDateTime) {
                    const dtParts2 = this.selectedDateTime.date.split('-');
                    const dtObj2   = new Date(dtParts2[0], dtParts2[1]-1, dtParts2[2]);
                    dtText    = dtObj2.toLocaleDateString(undefined, { weekday:'long', month:'long', day:'numeric' }) + ' at ' + this.selectedDateTime.display;
                    dtEditBtn = `
                        <div style="display:flex;gap:4px;align-items:center;">
                            <button type="button" class="sbs-summary-edit-btn" data-goto="4"
                                    style="background:#f3f4f6;color:#374151;border:none;border-radius:5px;padding:3px 8px;font-size:11px;font-weight:600;cursor:pointer;">Edit</button>
                        </div>`;
                }

                $summaryBox.html(cartSectionHtml + `
                    <div class="sbs-summary-item" id="sbs-summary-row-service">
                        <div style="flex:1;">
                            <div style="font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;font-weight:600;margin-bottom:2px;">Service</div>
                            <span id="sbs-summary-service" style="color:${hasService ? '#1a1a1a' : '#9ca3af'};font-size:14px;">${serviceDisplay}</span>
                        </div>
                        ${hasService ? `
                        <div style="display:flex;gap:4px;align-items:center;">
                            <button type="button" class="sbs-summary-edit-btn" data-goto="1"
                                    style="background:#f3f4f6;color:#374151;border:none;border-radius:5px;padding:3px 8px;font-size:11px;font-weight:600;cursor:pointer;">Edit</button>
                            <button type="button" class="sbs-summary-remove-btn sbs-summary-remove-service-btn" data-remove="service"
                                    aria-label="${__('Remove selected service')}" title="${__('Remove service')}">${__('Remove')}</button>
                        </div>` : ''}
                    </div>
                    ${optRow}
                    <div class="sbs-summary-item" id="sbs-summary-row-staff">
                        <div style="flex:1;">
                            <div style="font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;font-weight:600;margin-bottom:2px;">Staff</div>
                            <span id="sbs-summary-staff" style="color:${this.selectedStaff ? '#1a1a1a' : '#9ca3af'};font-size:14px;">${selectedStaffName}</span>
                        </div>
                        ${this.selectedStaff ? `
                        <div style="display:flex;gap:4px;align-items:center;">
                            <button type="button" class="sbs-summary-edit-btn" data-goto="3"
                                    style="background:#f3f4f6;color:#374151;border:none;border-radius:5px;padding:3px 8px;font-size:11px;font-weight:600;cursor:pointer;">Edit</button>
                        </div>` : ''}
                    </div>
                    <div class="sbs-summary-item" id="sbs-summary-row-datetime">
                        <div style="flex:1;">
                            <div style="font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;font-weight:600;margin-bottom:2px;">Date &amp; Time</div>
                            <span id="sbs-summary-datetime" style="color:${dtColor};font-size:14px;">${dtText}</span>
                        </div>
                        ${dtEditBtn}
                    </div>
                    <div class="sbs-summary-item">
                        <div style="flex:1;">
                            <div style="font-size:11px;text-transform:uppercase;letter-spacing:.05em;color:#9ca3af;font-weight:600;margin-bottom:2px;">Appointment Length</div>
                            <span id="sbs-summary-duration" style="font-size:14px;">${hasExact && duration ? duration + ' minutes' : '-'}</span>
                        </div>
                    </div>
                    ${hasExact ? this._taxParts(price).row : ''}
                    <div class="sbs-summary-item sbs-summary-total" style="border-top:2px solid var(--sbs-line,#f3f4f6);margin-top:4px;padding-top:14px;">
                        <strong style="font-size:15px;">${__('Total')}</strong>
                        <span id="sbs-summary-total" style="font-size:18px;font-weight:700;color:var(--primary-color,#ec4899);">${hasExact ? ysFormatMoney(this._taxParts(price).total) : '-'}</span>
                    </div>
                    ${hasExact ? this._depositRowHtml(this._taxParts(price).total) : ''}`);
            }

            this._renderDrawer();
            this._updateMobileCartBar();

            // Bind sidebar cart edit/remove buttons
            $summaryBox.off('click.sidecart')
                .on('click.sidecart', '.sbs-sidebar-cart-edit', function() {
                    self.editCartItem(parseInt($(this).data('index')));
                })
                .on('click.sidecart', '.sbs-sidebar-cart-remove', function() {
                    const removeIndex = parseInt($(this).data('index'));
                    const $overlay = $('<div style="position:fixed;inset:0;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center;padding:20px;">'
                        + '<div style="background:#fff;border-radius:16px;padding:28px 24px;max-width:340px;width:100%;text-align:center;box-shadow:0 20px 60px rgba(0,0,0,0.3);">'
                        + '<div style="font-size:36px;margin-bottom:12px;">!</div>'
                        + '<h3 style="margin:0 0 8px;font-size:18px;font-weight:700;color:#111;">Remove Appointment ' + (removeIndex+1) + '?</h3>'
                        + '<p style="color:#6b7280;font-size:14px;margin:0 0 20px;line-height:1.5;">This will remove all services in this appointment. You cannot undo this action.</p>'
                        + '<div style="display:flex;gap:10px;">'
                        + '<button type="button" id="sbs-remove-cancel" style="flex:1;background:#f3f4f6;color:#374151;border:none;border-radius:8px;padding:12px;font-size:14px;font-weight:600;cursor:pointer;">Keep it</button>'
                        + '<button type="button" id="sbs-remove-confirm" style="flex:1;background:#dc2626;color:#fff;border:none;border-radius:8px;padding:12px;font-size:14px;font-weight:600;cursor:pointer;">Yes, Remove</button>'
                        + '</div></div></div>');
                    $('body').append($overlay);
                    $overlay.on('click', '#sbs-remove-cancel', function() { $overlay.remove(); });
                    $overlay.on('click', '#sbs-remove-confirm', function() {
                        $overlay.remove();
                        self.removeFromCart(removeIndex);
                    });
                });
        }


        // Edit a specific pending service - restore it to selectedService and go back to its options step
        _editPendingService(idx) {
            const s = this.pendingServices[idx];
            if (!s) return;
            this.pendingServices.splice(idx, 1);
            this.selectedService  = { ...s.service };
            this.selectedCategory = s.service.category || null;
            this.selectedOptions  = [...s.options];
            this.selectedStaff    = null;
            this.selectedDateTime = null;
            this.updateSummary();
            this.goToStep(2);
            this.loadServiceOptions();
        }

        // Remove a specific pending service with confirmation
        _removePendingService(idx, onRemoved) {
            const self = this;
            const $overlay = $(`
                <div id="sbs-confirm-overlay" style="position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:99999;display:flex;align-items:center;justify-content:center;padding:16px;">
                  <div style="background:#fff;border-radius:12px;padding:28px 24px;max-width:340px;width:100%;text-align:center;box-shadow:0 8px 32px rgba(0,0,0,.2);">
                    <div style="font-size:28px;margin-bottom:12px;">!</div>
                    <h3 style="margin:0 0 8px;font-size:18px;font-weight:700;">Remove this service?</h3>
                    <p style="margin:0 0 20px;color:#6b7280;font-size:14px;">You'll need to re-add it if you change your mind.</p>
                    <div style="display:flex;gap:12px;">
                      <button type="button" id="sbs-confirm-cancel" style="flex:1;padding:12px;background:#f3f4f6;color:#374151;border:none;border-radius:8px;cursor:pointer;font-size:14px;font-weight:600;">Keep it</button>
                      <button type="button" id="sbs-confirm-delete" style="flex:1;padding:12px;background:#dc2626;color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:14px;font-weight:600;">Yes, remove</button>
                    </div>
                  </div>
                </div>`);
            $('body').append($overlay);
            $overlay.on('click', '#sbs-confirm-cancel', function() { $overlay.remove(); });
            $overlay.on('click', '#sbs-confirm-delete', function() {
                $overlay.remove();
                self.pendingServices.splice(idx, 1);
                self.selectedDateTime = null;
                $('#sbs-selected-start-at').val('');
                self._saveSession();
                if (typeof onRemoved === 'function') {
                    onRemoved();
                    return;
                }
                if (self.pendingServices.length === 0) {
                    // No services left - clear all state and go back to step 1
                    self.selectedService  = null;
                    self.selectedCategory = null;
                    self.selectedOptions  = [];
                    self.selectedStaff    = null;
                    // Deselect any highlighted service cards in the list
                    $('.sbs-service-item, .sbs-service-card').removeClass('selected');
                    self.updateSummary();
                    self.goToStep(1);
                } else {
                    self.updateSummary();
                    self._showPendingServicesSummary();
                    // If already on the date/time step, refresh available times immediately
                    if (self.currentStep === 4) {
                        // 4.6.2: the basket shrank, so days that were full may now
                        // fit. Refresh the whole visible range, not just the open
                        // day, or the calendar keeps showing sold-out cells that
                        // are no longer sold out.
                        self.loadAvailabilityDensity(true);
                        const selectedDate = $('.sbs-day-button.selected').data('date') || $('#sbs-date-picker').val();
                        if (selectedDate) {
                            self.selectedDateTime = null; // clear old selection since services changed
                            self.loadAvailableTimes(selectedDate);
                        }
                    }
                }
            });
        }
        
        updateSummaryForCheckout() {
            // On Step 5, the sidebar is fully rendered by updateSummary() (dynamic cart HTML).
            // Hide ALL static #sbs-summary-* rows so they don't show stale data.
            $('#sbs-summary-row-service, #sbs-summary-options-container, #sbs-summary-row-staff, #sbs-summary-row-datetime, #sbs-summary-row-duration').hide();
            $('#sbs-summary-row-total').hide();
            $('.sbs-booking-summary').addClass('sbs-checkout-card');

            if (this.bookingCart.length === 0) {
                $('#sbs-sidebar-edit-cart').hide();
                $('#sbs-checkout-card-heading').remove();
                return;
            }

            // Remove any previously injected heading (no longer needed - PHP provides it)
            $('#sbs-checkout-card-heading').remove();

            // Show combined total from all cart items
            let totalPrice = 0;
            let totalDuration = 0;
            this.bookingCart.forEach(item => {
                totalPrice    += this.getItemPrice(item);
                totalDuration += this._getItemVisitDuration(item);
            });

            // Inject a step-5-only total row after the cart HTML (or update if exists)
            // 4.4.3: the page renders TWO .sbs-booking-summary containers (a
            // desktop sidebar and a mobile panel; only one is visible at a
            // time). jQuery's .append() with a single element CLONES it into
            // every match, so this produced two elements sharing the id
            // #sbs-step5-total -- and the copy that ended up visible was the
            // one left un-populated, so the customer saw no total at all.
            // Bind to the visible container, and clear any stale copies.
            $('#sbs-step5-total').remove();

            const $summaryHosts = $('.sbs-booking-summary');
            const $host = $summaryHosts.filter(':visible').first().length
                ? $summaryHosts.filter(':visible').first()
                : $summaryHosts.first();

            const $step5Total = $('<div id="sbs-step5-total" class="sbs-checkout-totals"></div>');
            $host.append($step5Total);
            // 4.4.3: the LAST screen before "Confirm Booking" must show what
            // will actually be charged. This row previously printed the pre-tax
            // figure, so on a tax-exclusive shop the final number the customer
            // saw was not the number they were billed.
            const taxC = this._taxParts(totalPrice);
            const checkoutTip = Math.max(0, parseFloat($('#sbs-tip-amount').val()) || 0);
            const taxRows = taxC.hasTax
                ? '<div class="sbs-checkout-total-adjustment">' + taxC.row.replace(/^<div[^>]*>/, '').replace(/<\/div>$/, '') + '</div>'
                    + (checkoutTip > 0 ? '<div class="sbs-checkout-total-adjustment sbs-step5-tip-row"><span>' + __('Tip') + '</span><span>' + ysFormatMoney(checkoutTip) + '</span></div>' : '')
                : '';
            $step5Total.html(
                taxRows
                + '<div class="sbs-checkout-total-primary"><strong>' + __('Total') + '</strong><span class="sbs-checkout-total-amount">' + ysFormatMoney(taxC.total + checkoutTip) + '</span></div>'
                + '<div class="sbs-checkout-total-duration"><span>Appointment Length</span><strong>' + totalDuration + ' min</strong></div>'
                + '<div class="sbs-checkout-total-note" hidden></div>'
            ).show();

            this._refreshServerPriceQuote($step5Total, totalDuration, checkoutTip);

            $('#sbs-sidebar-edit-cart').show();
        }

        _refreshServerPriceQuote($totalRow, totalDuration, checkoutTip) {
            if (!(typeof yatoonData !== 'undefined' && yatoonData.dynamicPricingEnabled) || !this.bookingCart.length) return;
            const services = [];
            this.bookingCart.forEach(item => {
                const rows = item.services || [{ service: item.service, options: item.options || [], staff: item.staff }];
                const guestOffsets = {};
                const itemStartMs = item.dateTime && item.dateTime.start_at
                    ? new Date(item.dateTime.start_at).getTime()
                    : NaN;
                rows.forEach(row => {
                    const guestLabel = row.guestLabel || item.guestLabel || 'Me';
                    const offset = guestOffsets[guestLabel] || 0;
                    services.push({
                        service_id: row.service ? row.service.id : 0,
                        staff_id: row.staff ? row.staff.id : 0,
                        option_ids: (row.options || []).map(option => option.id),
                        guest_label: guestLabel,
                        start_at: Number.isFinite(itemStartMs) ? new Date(itemStartMs + offset * 60000).toISOString() : ''
                    });
                    guestOffsets[guestLabel] = offset + this._serviceDuration(row);
                });
            });
            const requestKey = JSON.stringify(services)
                + '|' + ($('#sbs-customer-email').val() || '')
                + '|' + ($('#sbs-customer-phone').val() || '')
                + '|' + ($('#sbs-coupon-code').val() || '')
                + '|' + ($('#sbs-gift-card-code-confirmed').val() || '')
                + '|' + (yatoonData.locationId || 0);
            if (this._dynamicQuoteKey === requestKey && typeof this._dynamicQuoteSubtotal === 'number') {
                this._renderServerPriceQuote($totalRow, this._dynamicQuoteSubtotal, totalDuration, checkoutTip);
                return;
            }
            this._dynamicQuoteKey = requestKey;
            $.post(yatoonData.ajax_url || yatoonData.ajaxUrl, {
                action: 'yatoon_dynamic_pricing_quote',
                nonce: yatoonData.nonce,
                services_json: JSON.stringify(services),
                start_at: services[0] ? services[0].start_at : '',
                yatoon_location_id: yatoonData.locationId || 0,
                customer_email: $('#sbs-customer-email').val() || '',
                customer_phone: $('#sbs-customer-phone').val() || '',
                coupon_code: $('#sbs-coupon-code').val() || '',
                gift_card_code: $('#sbs-gift-card-code-confirmed').val() || ''
            }).done(response => {
                if (!response || !response.success || !response.data) return;
                this._dynamicQuoteSubtotal = Math.max(0, parseFloat(response.data.subtotal) || 0);
                this._renderServerPriceQuote($totalRow, this._dynamicQuoteSubtotal, totalDuration, checkoutTip);
            });
        }

        _renderServerPriceQuote($totalRow, subtotal, totalDuration, checkoutTip) {
            if (!$totalRow || !$totalRow.length) return;
            const parts = this._taxParts(subtotal);
            const total = parts.total + checkoutTip;
            $totalRow.find('.sbs-checkout-total-amount').first().text(ysFormatMoney(total));
            $totalRow.find('.sbs-checkout-total-note').first().text(__('Includes current pricing rules')).prop('hidden', false);
        }

        _renderDrawer() {
            // No-op: drawer content is rendered fresh in _renderMobileCart when opened
        }

        _handleBrowserBack() {
            // Mirrors the Back button logic - called when browser Back is pressed
            const self = this;
            const prevStep = self.currentStep - 1;

            if (self.currentStep === 5 && self.bookingCart.length > 0) {
                const last = self.bookingCart.pop();
                self.pendingServices  = last.services ? [...last.services] : [];
                self.selectedDateTime = null;
                self._saveSession();
                self._showPendingServicesSummary();
                self.goToStep(4);
                return;
            }
            if (self.currentStep === 4) {
                if (self.pendingServices.length > 0) {
                    self._backFromTimeToStaff();
                    return;
                } else if (false && self.bookingCart.length > 0) {
                    self.selectedDateTime = null;
                    self._saveSession();
                    const $slots = $('.sbs-time-slots');
                    $slots.html('<div class="sbs-loading"><span class="spinner"></span> ' + __('Loading available times...') + '</div>');
                    const $selectedBtn = $('.sbs-day-button.selected:not(.disabled)');
                    if ($selectedBtn.length) {
                        const date = $selectedBtn.data('date');
                        if (date) self.loadAvailableTimes(date, true);
                    }
                    self._updateMobileCartBar();
                    return;
                } else if (self.bookingCart.length > 0) {
                    const last = self.bookingCart.pop();
                    self.pendingServices  = last.services ? [...last.services] : [];
                    self.selectedDateTime = null;
                    self._saveSession();
                    self._showPendingServicesSummary();
                    const $slots = $('.sbs-time-slots');
                    $slots.html('<div class="sbs-loading"><span class="spinner"></span> ' + __('Loading available times...') + '</div>');
                    const $selectedBtn = $('.sbs-day-button.selected:not(.disabled)');
                    if ($selectedBtn.length) {
                        const date = $selectedBtn.data('date');
                        if (date) self.loadAvailableTimes(date, true);
                    }
                    self._updateMobileCartBar();
                    return;
                }
            }
            if (prevStep >= 1) {
                self.goToStep(prevStep);
            }
        }

        _closeDrawer() {
            const $drawer = $('#sbs-mobile-drawer');
            $drawer.removeClass('open');
            // After transition ends, set display:none so it doesn't peek on iOS Safari
            setTimeout(function() {
                if (!$drawer.hasClass('open')) $drawer.css('display', 'none');
            }, 320);
            $('#sbs-drawer-backdrop').removeClass('open');
            $('#sbs-cart-expand-btn').removeClass('open');
        }

        initMobileCart() {
            const self = this;
            if (!$('#sbs-cart-back-btn').length) {
                $('#sbs-mobile-cart-bar').prepend('<button type="button" id="sbs-cart-back-btn" aria-label="' + __('Back') + '">' + __('Back') + '</button>');
            }
            $(document).on('click touchend', '#sbs-mobile-cart-bar', function(e) {
                if ($(e.target).closest('#sbs-cart-next-btn, #sbs-cart-back-btn').length) return;
                const lastControlTouch = $(this).data('sbsControlTouch') || 0;
                if (Date.now() - lastControlTouch < 700) return;
                const lastBarTouch = $(this).data('sbsLastTouch') || 0;
                if (e.type === 'touchend') {
                    e.preventDefault();
                    $(this).data('sbsLastTouch', Date.now());
                } else if (Date.now() - lastBarTouch < 700) {
                    return;
                }
                if ($('#sbs-mobile-drawer').hasClass('open')) {
                    self._closeDrawer();
                } else {
                    const $drawer = $('#sbs-mobile-drawer');
                    $drawer.css('display', 'block');
                    // Force reflow so transition fires
                    $drawer[0].offsetHeight;
                    $drawer.addClass('open');
                    $('#sbs-drawer-backdrop').addClass('open');
                    $('#sbs-cart-expand-btn').addClass('open');
                    self._renderMobileCart();
                }
            });
            $(document).on('keydown', '#sbs-cart-info', function(e) {
                if (e.key !== 'Enter' && e.key !== ' ') return;
                e.preventDefault();
                $('#sbs-mobile-cart-bar').trigger('click');
            });
            $(document).on('click', '#sbs-drawer-backdrop', function() { self._closeDrawer(); });
            $(document).on('click touchend', '#sbs-cart-back-btn', function(e) {
                e.preventDefault();
                e.stopPropagation();
                e.stopImmediatePropagation();
                $('#sbs-mobile-cart-bar').data('sbsControlTouch', Date.now());
                const lastTouch = $(this).data('sbsLastTouch') || 0;
                if (e.type === 'touchend') {
                    $(this).data('sbsLastTouch', Date.now());
                } else if (Date.now() - lastTouch < 700) {
                    return;
                }
                self._closeDrawer();
                const $back = $('.sbs-form-step.active .sbs-btn-back').first();
                if ($back.length) {
                    $back.trigger('click');
                } else if (self.currentStep > 1) {
                    self.goToStep(self.currentStep - 1, true);
                }
            });
            $(document).on('click touchend', '#sbs-cart-next-btn', function(e) {
                e.preventDefault();
                e.stopPropagation(); // don't open drawer
                const lastTouch = $(this).data('sbsLastTouch') || 0;
                if (e.type === 'touchend') {
                    $(this).data('sbsLastTouch', Date.now());
                } else if (Date.now() - lastTouch < 700) {
                    return;
                }
                if ($(this).prop('disabled')) return;
                if (self._blockWhileDesignUploadInProgress()) return;
                const btnText = $(this).text();
                const isCheckoutAction = btnText.indexOf('Review') !== -1 || btnText.indexOf('Book') !== -1 || btnText.indexOf('Booking') !== -1 || btnText.indexOf('Checkout') !== -1 || btnText.indexOf('Confirm') !== -1;
                if (isCheckoutAction && self.currentStep === 5) {
                    self._requestCustomerFormSubmit();
                } else if (isCheckoutAction) {
                    self.goToStep(5);
                } else if (self.currentStep === 1 && self.pendingServices.length > 0) {
                    if (self._isGroupAppointmentFlow()) self._showGuestReviewScreen();
                    else self.goToStep(4, true);
                } else if (self.currentStep === 2) {
                    const needsMain = !!self._optionHasMainGroup;
                    const hasMain = (self.selectedOptions || []).some(function(o) { return !o.is_addon; });
                    if (needsMain && !hasMain) {
                        self._showToast(__('Please choose one main option before continuing.'), 'error');
                        self._showMainOptionRequired();
                        return;
                    }
                    self.loadAvailableStaff();
                } else if (self.currentStep === 3) {
                    if (self.selectedService) {
                        self._commitSelectedStaff();
                    } else if (self.pendingServices.length > 0) {
                        self.goToStep(4);
                    }
                }
            });
            $(document).on('click touchend', '.sbs-mobile-cart-remove', function(e) {
                e.preventDefault();
                e.stopPropagation();
                const lastTouch = $(this).data('sbsLastTouch') || 0;
                if (e.type === 'touchend') {
                    $(this).data('sbsLastTouch', Date.now());
                } else if (Date.now() - lastTouch < 700) {
                    return;
                }
                const index = parseInt($(this).data('index'));
                self._confirmRemove(index, true);
            });
            $(document).on('click', '.sbs-mobile-cart-edit', function() {
                const index = parseInt($(this).data('index'));
                self._closeDrawer();
                self.editCartItem(index);
            });
        }
        
        _renderMobileCart() {
            const self = this;
            const container = $('#sbs-mobile-drawer .sbs-booking-summary');

            // -- CASE A: Cart has completed appointments -----------------------
            if (this.bookingCart.length > 0) {
                let html = '<div style="padding:16px 16px 8px;">';
                let totalPrice = 0;
                let totalDuration = 0;

                this.bookingCart.forEach((item, index) => {
                    const price    = this.getItemPrice(item);
                    const duration = this._getItemVisitDuration(item);
                    totalPrice    += price;
                    totalDuration += duration;
                    const dateParts = item.dateTime.date.split('-');
                    const dateObj   = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
                    const dateStr   = dateObj.toLocaleDateString(undefined, { month:'short', day:'numeric' });

                    // Multi-service: list all services per cart item
                    const services = item.services || [{ service: item.service, options: item.options, staff: item.staff }];
                    let svcsHtml = '';
                    services.forEach(s => {
                        const svcLabel = this._svcDisplayLabelHtml(s.service, s.options);
                        const staffLabel = s.staff ? this.getFirstName(s.staff.name) : '-';
                        svcsHtml += `
                            <div style="margin-bottom:4px;">
                                <div style="font-weight:600;font-size:14px;color:#111;">${svcLabel}</div>
                                <div style="font-size:12px;color:#6b7280;">${staffLabel}</div>
                            </div>`;
                    });

                    html += `
                        <div class="sbs-drawer-appt-row" style="padding:14px 0;border-bottom:1px solid #f3f4f6;">
                          <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px;">
                            <div style="flex:1;min-width:0;">
                                ${svcsHtml}
                                <div style="font-size:12px;color:#6b7280;margin-top:4px;line-height:1.45;">${this._formatItemTimeRange(item, dateStr)}<br><span>Appointment Length: ${duration} min</span></div>
                            </div>
                            <div style="display:flex;flex-direction:column;align-items:flex-end;gap:6px;flex-shrink:0;">
                                <span style="font-weight:700;font-size:15px;color:#ec4899;">${ysFormatMoney(price)}</span>
                                <div style="display:flex;gap:6px;">
                                    <button type="button" class="sbs-mobile-cart-edit" data-index="${index}"
                                            style="background:#f0f9ff;color:#0369a1;border:1px solid #bae6fd;border-radius:6px;padding:4px 10px;font-size:12px;font-weight:600;cursor:pointer;">Edit</button>
                                    <button type="button" class="sbs-mobile-cart-remove" data-index="${index}"
                                            style="background:#fef2f2;color:#dc2626;border:1px solid #fecaca;border-radius:6px;padding:4px 10px;font-size:12px;font-weight:600;cursor:pointer;">Remove</button>
                                </div>
                            </div>
                          </div>
                        </div>`;
                });

                html += `</div>
                    <div style="padding:16px;border-top:1px solid #e5e7eb;">
                      <div class="sbs-checkout-totals sbs-checkout-totals-compact">
                        <div class="sbs-checkout-total-primary"><strong>${__('Total')}</strong><span class="sbs-checkout-total-amount">${ysFormatMoney(this._taxParts(totalPrice).total)}</span></div>
                        <div class="sbs-checkout-total-duration"><span>Appointment Length</span><strong>${totalDuration} min</strong></div>
                      </div>
                      <button type="button" id="sbs-drawer-checkout-btn"
                              style="width:100%;padding:14px;background:#111;color:#fff;border:none;border-radius:10px;font-size:16px;font-weight:700;cursor:pointer;letter-spacing:.02em;">
                          Review &amp; Book
                      </button>
                    </div>`;

                container.html(html);
                container.off('click', '#sbs-drawer-checkout-btn').on('click', '#sbs-drawer-checkout-btn', function() {
                    self._closeDrawer();
                    self.goToStep(5);
                });
                return;
            }

            // -- CASE B: pendingServices (multi-service in progress, no time yet) --
            if (this.pendingServices.length > 0) {
                const all = this.pendingServices;
                const totalDur = this._durationByGuest(all).totalDuration;
                const totalPric = all.reduce((s, sv) => s + this._optionsPrice(sv.service, sv.options), 0);

                let svcsHtml = '';
                all.forEach((s, idx) => {
                    const svcLabel = this._svcDisplayLabelHtml(s.service, s.options);
                    const staffLabel = s.staff ? this.getFirstName(s.staff.name) : '-';
                    const dur = s.options.length > 0
                        ? s.options.reduce((d, o) => d + (o.duration || 0), 0)
                        : (s.service.duration || 0);
                    const pric = this._optionsPrice(s.service, s.options);

                    svcsHtml += `
                        <div style="padding:12px 16px;border-bottom:1px solid #f3f4f6;">
                            <div style="display:flex;justify-content:space-between;align-items:flex-start;gap:8px;">
                                <div style="flex:1;min-width:0;">
                                    <div style="font-weight:600;font-size:13px;color:#111;">${svcLabel}</div>
                                    <div style="font-size:12px;color:#6b7280;">${staffLabel} - ${dur}min</div>
                                </div>
                                <div style="display:flex;flex-direction:column;align-items:flex-end;gap:4px;flex-shrink:0;">
                                    <span style="font-size:13px;font-weight:700;color:#ec4899;">${ysFormatMoney(pric)}</span>
                                    <div style="display:flex;gap:5px;">
                                        <button type="button" class="sbs-drawer-pending-edit" data-idx="${idx}"
                                                style="background:#f0fdf4;color:#15803d;border:1px solid #bbf7d0;border-radius:5px;padding:3px 8px;font-size:11px;font-weight:600;cursor:pointer;">Edit</button>
                                        <button type="button" class="sbs-drawer-pending-remove" data-idx="${idx}"
                                                style="background:#fef2f2;color:#dc2626;border:1px solid #fecaca;border-radius:5px;padding:3px 8px;font-size:11px;font-weight:600;cursor:pointer;">Remove</button>
                                    </div>
                                </div>
                            </div>
                        </div>`;
                });

                // Date & time row
                let dtHtml = '';
                if (this.selectedDateTime) {
                    const dp = this.selectedDateTime.date.split('-');
                    const d  = new Date(dp[0], dp[1]-1, dp[2]);
                    const ds = d.toLocaleDateString(undefined, { weekday:'short', month:'short', day:'numeric' });
                    dtHtml = `
                        <div style="padding:12px 16px;border-bottom:1px solid #f3f4f6;display:flex;justify-content:space-between;align-items:center;">
                            <div>
                                <div style="font-size:11px;text-transform:uppercase;color:#9ca3af;font-weight:600;margin-bottom:1px;">Date &amp; Time</div>
                                <div style="font-size:13px;font-weight:600;color:#111;">${ds} at ${this.selectedDateTime.display}</div>
                            </div>
                            <button type="button" class="sbs-drawer-progress-edit" data-step="4"
                                    style="background:none;border:1px solid #e5e7eb;border-radius:6px;padding:5px 10px;font-size:12px;cursor:pointer;color:#374151;">Change</button>
                        </div>`;
                } else {
                    dtHtml = `
                        <div style="padding:12px 16px;border-bottom:1px solid #f3f4f6;">
                            <div style="font-size:11px;text-transform:uppercase;color:#9ca3af;font-weight:600;margin-bottom:1px;">Date &amp; Time</div>
                            <div style="font-size:13px;color:#9ca3af;">Select a time to continue</div>
                        </div>`;
                }

                const hasTime = !!this.selectedDateTime;
                const reviewBtn = hasTime
                    ? `<button type="button" id="sbs-drawer-review-btn" style="width:100%;padding:13px;background:#111;color:#fff;border:none;border-radius:10px;font-size:15px;font-weight:700;cursor:pointer;margin-top:12px;">Review &amp; Book</button>`
                    : '';

                container.html(`
                    <div style="padding:14px 16px 6px;">
                        <h3 style="margin:0 0 2px;font-size:16px;font-weight:700;">Your appointment</h3>
                        <div style="font-size:12px;color:#9ca3af;">${all.length} service${all.length > 1 ? 's' : ''} &middot; Appointment Length: ${totalDur} min</div>
                    </div>
                    ${svcsHtml}
                    ${dtHtml}
                    <div style="padding:14px 16px;display:flex;justify-content:space-between;align-items:center;border-top:1px solid #f3f4f6;">
                        <span style="font-size:14px;color:#6b7280;">Appointment Length: ${totalDur} min</span>
                        <span style="font-size:17px;font-weight:700;color:#ec4899;">${ysFormatMoney(this._taxParts(totalPric).total)}</span>
                    </div>
                    ${hasTime ? `<div style="padding:0 16px 16px;">${reviewBtn}</div>` : ''}
                `);

                // Bind pending service edit/remove
                container.off('click.pendingDrawer').on('click.pendingDrawer', '.sbs-drawer-pending-edit', function() {
                    const idx = parseInt($(this).data('idx'));
                    self._closeDrawer();
                    self._editPendingService(idx);
                }).on('click.pendingDrawer', '.sbs-drawer-pending-remove', function() {
                    const idx2 = parseInt($(this).data('idx'));
                    self._closeDrawer();
                    self._removePendingService(idx2);
                }).on('click.pendingDrawer', '#sbs-drawer-review-btn', function() {
                    self._closeDrawer();
                    self.goToStep(5);
                }).on('click.pendingDrawer', '.sbs-drawer-progress-edit', function() {
                    const targetStep = parseInt($(this).data('step'));
                    self._closeDrawer();
                    self.goToStep(targetStep, true);
                });
                return;
            }

            // -- CASE C: Single service in-progress (steps 1-3) ---------------
            const priceVal = this._getCurrentPrice();
            const durVal   = this._getCurrentDuration();
            const rows = [];

            rows.push(`<div style="padding:16px 16px 8px;"><h3 style="margin:0 0 4px;font-size:16px;font-weight:700;">Your appointment</h3></div>`);

            if (this.selectedService) {
                const svcLabel = this.selectedService.category
                    ? this.selectedService.category + ' - ' + this.selectedService.name
                    : this.selectedService.name;
                rows.push(`
                    <div style="display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #f3f4f6;">
                      <div>
                        <div style="font-size:11px;text-transform:uppercase;color:#9ca3af;font-weight:600;letter-spacing:.05em;margin-bottom:2px;">Service</div>
                        <div style="font-weight:600;font-size:14px;">${svcLabel}</div>
                      </div>
                      <div style="display:flex;gap:6px;">
                        <button type="button" class="sbs-drawer-progress-edit" data-step="1"
                                style="background:none;border:1px solid #e5e7eb;border-radius:6px;padding:5px 10px;font-size:12px;cursor:pointer;color:#374151;">Edit</button>
                        <button type="button" class="sbs-drawer-del-service"
                                style="background:none;border:1px solid #fecaca;border-radius:6px;padding:5px 8px;font-size:12px;cursor:pointer;color:#dc2626;">Remove</button>
                      </div>
                    </div>`);
            } else {
                rows.push(`<div style="padding:12px 16px;border-bottom:1px solid #f3f4f6;color:#9ca3af;font-size:14px;">Select a service to get started</div>`);
            }

            if (this.selectedOptions.length > 0) {
                const optText = this.selectedOptions.map(o => o.name).join(', ');
                rows.push(`
                    <div style="display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #f3f4f6;">
                      <div>
                        <div style="font-size:11px;text-transform:uppercase;color:#9ca3af;font-weight:600;letter-spacing:.05em;margin-bottom:2px;">Options</div>
                        <div style="font-weight:500;font-size:14px;">${optText}</div>
                      </div>
                      <div style="display:flex;gap:6px;">
                        <button type="button" class="sbs-drawer-progress-edit" data-step="2"
                                style="background:none;border:1px solid #e5e7eb;border-radius:6px;padding:5px 10px;font-size:12px;cursor:pointer;color:#374151;">Edit</button>
                        <button type="button" class="sbs-drawer-del-options"
                                style="background:none;border:1px solid #fecaca;border-radius:6px;padding:5px 8px;font-size:12px;cursor:pointer;color:#dc2626;">Remove</button>
                      </div>
                    </div>`);
            }

            if (this.selectedStaff) {
                rows.push(`
                    <div style="display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid #f3f4f6;">
                      <div>
                        <div style="font-size:11px;text-transform:uppercase;color:#9ca3af;font-weight:600;letter-spacing:.05em;margin-bottom:2px;">Staff</div>
                        <div style="font-weight:500;font-size:14px;">with ${this.selectedStaff.name}</div>
                      </div>
                      <button type="button" class="sbs-drawer-progress-edit" data-step="3"
                              style="background:none;border:1px solid #e5e7eb;border-radius:6px;padding:5px 10px;font-size:12px;cursor:pointer;color:#374151;">Change</button>
                    </div>`);
            }

            if (priceVal > 0 || durVal > 0) {
                rows.push(`
                    <div style="padding:14px 16px;display:flex;justify-content:space-between;align-items:center;border-top:1px solid #f3f4f6;margin-top:4px;">
                      <span style="font-size:13px;color:#6b7280;">${durVal > 0 ? durVal + ' min' : ''}</span>
                      <span style="font-size:17px;font-weight:700;color:#ec4899;">${ysFormatMoney(this._taxParts(priceVal).total)}</span>
                    </div>`);
            }

            container.html(rows.join(''));

            container.off('click.drawerC').on('click.drawerC', '.sbs-drawer-progress-edit', function() {
                const targetStep = parseInt($(this).data('step'));
                self._closeDrawer();
                if (targetStep === 2) { self.goToStep(2, true); self.loadServiceOptions(); }
                else if (targetStep === 3) { self.goToStep(3); self.loadAvailableStaff(); }
                else { self.goToStep(targetStep, true); }
            }).on('click.drawerC touchend.drawerC', '.sbs-drawer-del-service', function(e) {
                e.preventDefault();
                e.stopPropagation();
                const lastTouch = $(this).data('sbsLastTouch') || 0;
                if (e.type === 'touchend') {
                    $(this).data('sbsLastTouch', Date.now());
                } else if (Date.now() - lastTouch < 700) {
                    return;
                }
                self._confirmDrawerRemoval('service');
            }).on('click.drawerC touchend.drawerC', '.sbs-drawer-del-options', function(e) {
                e.preventDefault();
                e.stopPropagation();
                const lastTouch = $(this).data('sbsLastTouch') || 0;
                if (e.type === 'touchend') {
                    $(this).data('sbsLastTouch', Date.now());
                } else if (Date.now() - lastTouch < 700) {
                    return;
                }
                self._confirmDrawerRemoval('options');
            });
        }

        _confirmDrawerRemoval(action) {
            const self = this;
            const isService = action === 'service';
            $('#sbs-confirm-overlay').remove();
            const title = isService ? __('Remove this service?') : __('Remove these options?');
            const body = isService
                ? __('This will clear the service, options, staff, and time for this appointment.')
                : __('This will clear the selected options for this service.');
            const $overlay = $(`
                <div id="sbs-confirm-overlay" style="
                    position:fixed;inset:0;background:rgba(17,24,39,.52);z-index:99999;
                    display:flex;align-items:center;justify-content:center;padding:18px;">
                  <div style="background:#fff;border-radius:18px;padding:24px;max-width:360px;width:100%;box-shadow:0 24px 70px rgba(17,24,39,.28);">
                    <h3 style="margin:0 0 8px;font-size:20px;font-weight:800;color:#111827;">${title}</h3>
                    <p style="margin:0 0 20px;color:#6b7280;font-size:14px;line-height:1.5;">${body}</p>
                    <div style="display:flex;gap:12px;">
                      <button type="button" id="sbs-confirm-cancel" style="flex:1;min-height:46px;background:#fff;color:#374151;border:1px solid #d8dde6;border-radius:999px;cursor:pointer;font-size:14px;font-weight:800;">Keep it</button>
                      <button type="button" id="sbs-confirm-delete" style="flex:1;min-height:46px;background:#dc2626;color:#fff;border:none;border-radius:999px;cursor:pointer;font-size:14px;font-weight:800;">Remove</button>
                    </div>
                  </div>
                </div>`);
            $('body').append($overlay);
            $overlay.on('click touchend', '#sbs-confirm-cancel', function(e) {
                e.preventDefault();
                e.stopPropagation();
                $overlay.remove();
            });
            $overlay.on('click touchend', '#sbs-confirm-delete', function(e) {
                e.preventDefault();
                e.stopPropagation();
                $overlay.remove();
                if (isService) {
                    self.selectedService = null;
                    self.selectedCategory = null;
                    self.selectedOptions = [];
                    self.selectedStaff = null;
                    self.selectedDateTime = null;
                    $('#sbs-selected-service-id, #sbs-selected-staff-id, #sbs-selected-start-at, #sbs-selected-options').val('');
                    self._closeDrawer();
                    self._saveSession();
                    self.goToStep(1, true);
                } else {
                    self.selectedOptions = [];
                    $('#sbs-selected-options').val('');
                    self._closeDrawer();
                    self.updateSummary();
                    self._updateMobileCartBar();
                    self._saveSession();
                }
            });
        }

        _squarePaymentsReady() {
            return !!(
                typeof yatoonData !== 'undefined' &&
                yatoonData.squarePaymentsEnabled &&
                yatoonData.squareApplicationId &&
                yatoonData.squareLocationId &&
                window.Square &&
                typeof window.Square.payments === 'function'
            );
        }

        _paymentProviderEnabled(provider) {
            if (typeof yatoonData === 'undefined') return false;
            if (provider === 'square') return !!yatoonData.squarePaymentsEnabled;
            if (provider === 'paypal') return !!yatoonData.paypalPaymentsEnabled;
            if (provider === 'stripe') return !!yatoonData.depositEnabled;
            return false;
        }

        _ensurePaymentSdk(provider) {
            const ready = {
                square: () => this._squarePaymentsReady(),
                paypal: () => this._paypalPaymentsReady(),
                stripe: () => this._stripePaymentsReady()
            }[provider];
            const url = {
                square: yatoonData.squareSdkUrl,
                paypal: yatoonData.paypalSdkUrl,
                stripe: yatoonData.stripeSdkUrl
            }[provider];

            if (ready && ready()) return Promise.resolve();
            if (!url) return Promise.reject(new Error(__('The secure payment service is not configured.')));
            this._paymentSdkPromises = this._paymentSdkPromises || {};
            if (this._paymentSdkPromises[provider]) return this._paymentSdkPromises[provider];

            this._paymentSdkPromises[provider] = new Promise(function(resolve, reject) {
                const script = document.createElement('script');
                script.src = url;
                script.async = true;
                script.dataset.yatoonPaymentSdk = provider;
                script.onload = function() {
                    if (ready && ready()) resolve();
                    else reject(new Error(__('The secure payment service did not initialize.')));
                };
                script.onerror = function() {
                    reject(new Error(__('The secure payment service could not be loaded. Check your connection and try again.')));
                };
                document.head.appendChild(script);
            }).catch((error) => {
                delete this._paymentSdkPromises[provider];
                throw error;
            });
            return this._paymentSdkPromises[provider];
        }

        _paypalPaymentsReady() {
            return !!(
                typeof yatoonData !== 'undefined' &&
                yatoonData.paypalPaymentsEnabled &&
                window.paypal &&
                typeof window.paypal.Buttons === 'function'
            );
        }

        _stripePaymentsReady() {
            return !!(
                typeof yatoonData !== 'undefined' &&
                yatoonData.depositEnabled &&
                yatoonData.stripePublishableKey &&
                typeof window.Stripe === 'function'
            );
        }

        _createStripePaymentForPayload(payload) {
            const self = this;
            return new Promise(function(resolve, reject) {
                if (!self._stripePaymentsReady()) {
                    reject(new Error(__('Stripe payment form is not ready. Please refresh and try again.')));
                    return;
                }
                const fd = new FormData();
                Object.keys(payload).forEach(function(key) {
                    const value = payload[key];
                    if (Array.isArray(value)) value.forEach(function(item) { fd.append(key + '[]', item); });
                    else fd.append(key, value == null ? '' : value);
                });
                fd.set('action', 'yatoon_stripe_create_intent');
                fd.set('nonce', yatoonData.nonce);
                fetch(yatoonData.ajax_url || yatoonData.ajaxUrl, { method: 'POST', body: fd, credentials: 'same-origin' })
                    .then(function(response) { return response.json(); })
                    .then(function(json) {
                        if (!json || !json.success || !json.data || !json.data.client_secret) {
                            throw new Error(json && json.data && json.data.message ? json.data.message : __('Could not start the Stripe payment.'));
                        }
                        let $box = $('#sbs-stripe-payment-box');
                        if (!$box.length) {
                            $box = $(`
                                <div id="sbs-stripe-payment-box" style="margin:16px 0;padding:16px;border:1px solid #ddd6fe;border-radius:14px;background:#f5f3ff;">
                                    <div style="font-size:14px;font-weight:800;color:#2e1065;margin-bottom:6px;">Secure Stripe Payment</div>
                                    <div id="sbs-stripe-payment-amount" style="font-size:13px;color:#475569;margin-bottom:12px;"></div>
                                    <div id="sbs-stripe-payment-element"></div>
                                    <button type="button" id="sbs-stripe-pay-button" style="width:100%;min-height:46px;margin-top:14px;border:0;border-radius:9px;background:#635bff;color:#fff;font-weight:800;cursor:pointer;">Pay deposit</button>
                                    <div id="sbs-stripe-payment-status" style="font-size:12px;color:#64748b;margin-top:10px;"></div>
                                </div>`);
                            const $form = $('#sbs-booking-form');
                            if ($form.length) $form.prepend($box);
                            else $('.sbs-step-5').prepend($box);
                        }
                        $('#sbs-stripe-payment-element').empty();
                        $('#sbs-stripe-payment-amount').text(__('Deposit due now:') + ' ' + (json.data.deposit_display || ''));
                        const stripe = window.Stripe(yatoonData.stripePublishableKey);
                        const elements = stripe.elements({ clientSecret: json.data.client_secret });
                        const paymentElement = elements.create('payment');
                        paymentElement.mount('#sbs-stripe-payment-element');
                        const $status = $('#sbs-stripe-payment-status');
                        $('#sbs-stripe-pay-button').off('.yatoonStripe').on('click.yatoonStripe', function() {
                            const $button = $(this).prop('disabled', true).text(__('Processing...'));
                            $status.text(__('Confirming your payment securely...'));
                            stripe.confirmPayment({
                                elements: elements,
                                redirect: 'if_required'
                            }).then(function(result) {
                                if (result.error) throw new Error(result.error.message || __('Stripe could not complete the payment.'));
                                if (!result.paymentIntent || result.paymentIntent.status !== 'succeeded') {
                                    throw new Error(__('Stripe payment is not complete.'));
                                }
                                $status.text(__('Payment approved. Confirming your appointment...'));
                                $box.hide();
                                resolve(result.paymentIntent.id);
                            }).catch(function(error) {
                                $button.prop('disabled', false).text(__('Pay deposit'));
                                $status.text(error.message || __('Stripe payment failed.'));
                                self._showToast(error.message || __('Stripe payment failed.'), 'error');
                                reject(error);
                            });
                        });
                        $box.show();
                        if ($box[0] && $box[0].scrollIntoView) $box[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
                    }).catch(reject);
            });
        }

        _createPayPalPaymentForPayload(payload) {
            const self = this;
            return new Promise(function(resolve, reject) {
                if (!self._paypalPaymentsReady()) {
                    reject(new Error(__('PayPal Checkout is not ready. Please refresh and try again.')));
                    return;
                }

                let $box = $('#sbs-paypal-payment-box');
                if (!$box.length) {
                    $box = $(`
                        <div id="sbs-paypal-payment-box" style="margin:16px 0;padding:16px;border:1px solid #bfdbfe;border-radius:14px;background:#eff6ff;">
                            <div style="font-size:14px;font-weight:800;color:#172554;margin-bottom:6px;">Secure PayPal Payment</div>
                            <div style="font-size:13px;color:#475569;margin-bottom:12px;">Choose PayPal below to pay the deposit and confirm your appointment.</div>
                            <div id="sbs-paypal-button-container"></div>
                            <div id="sbs-paypal-payment-status" style="font-size:12px;color:#64748b;margin-top:10px;"></div>
                        </div>`);
                    const $form = $('#sbs-booking-form');
                    if ($form.length) $form.prepend($box);
                    else $('.sbs-step-5').prepend($box);
                }
                const $status = $('#sbs-paypal-payment-status');
                const endpoint = yatoonData.ajax_url || yatoonData.ajaxUrl;

                function post(action, extra) {
                    const fd = new FormData();
                    Object.keys(payload).forEach(function(key) {
                        const value = payload[key];
                        if (Array.isArray(value)) value.forEach(function(item) { fd.append(key + '[]', item); });
                        else fd.append(key, value == null ? '' : value);
                    });
                    fd.set('action', action);
                    fd.set('nonce', yatoonData.nonce);
                    Object.keys(extra || {}).forEach(function(key) { fd.set(key, extra[key]); });
                    return fetch(endpoint, { method: 'POST', body: fd, credentials: 'same-origin' })
                        .then(function(response) { return response.json(); })
                        .then(function(json) {
                            if (!json || !json.success || !json.data) {
                                throw new Error(json && json.data && json.data.message ? json.data.message : __('PayPal payment failed.'));
                            }
                            return json.data;
                        });
                }

                $('#sbs-paypal-button-container').empty();
                window.paypal.Buttons({
                    style: { layout: 'vertical', shape: 'rect', label: 'paypal' },
                    createOrder: function() {
                        $status.text(__('Preparing secure PayPal Checkout...'));
                        return post('yatoon_paypal_create_order').then(function(data) {
                            if (data.amount_display) $status.text('Deposit due now: ' + data.amount_display);
                            return data.order_id;
                        });
                    },
                    onApprove: function(data) {
                        $status.text(__('Confirming your PayPal payment...'));
                        return post('yatoon_paypal_capture_order', { order_id: data.orderID }).then(function(result) {
                            $status.text(__('Payment approved. Confirming your appointment...'));
                            $box.hide();
                            resolve(result.order_id || data.orderID);
                        }).catch(function(error) {
                            $status.text(error.message || __('PayPal could not capture the payment.'));
                            self._showToast(error.message || __('PayPal could not capture the payment.'), 'error');
                            reject(error);
                        });
                    },
                    onCancel: function() {
                        const error = new Error(__('PayPal Checkout was cancelled.'));
                        $status.text(error.message);
                        reject(error);
                    },
                    onError: function(error) {
                        $status.text(__('PayPal Checkout could not be opened. Please try again.'));
                        self._showToast(error && error.message ? error.message : __('PayPal Checkout failed.'), 'error');
                        reject(error instanceof Error ? error : new Error(__('PayPal Checkout failed.')));
                    }
                }).render('#sbs-paypal-button-container').then(function() {
                    $box.show();
                    if ($box[0] && $box[0].scrollIntoView) $box[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
                }).catch(reject);
            });
        }

        _squareMethods() {
            const methods = (yatoonData && Array.isArray(yatoonData.squarePaymentMethods))
                ? yatoonData.squarePaymentMethods
                : ['card'];
            return methods.length ? methods : ['card'];
        }

        _previewSquarePayment(payload) {
            const fd = new FormData();
            Object.keys(payload).forEach(function(key) {
                const val = payload[key];
                if (Array.isArray(val)) {
                    val.forEach(function(itemVal) { fd.append(key + '[]', itemVal); });
                } else {
                    fd.append(key, val == null ? '' : val);
                }
            });
            fd.set('action', 'yatoon_square_preview_payment');
            return fetch((yatoonData.ajax_url || yatoonData.ajaxUrl), {
                method: 'POST',
                body: fd,
                credentials: 'same-origin'
            }).then(function(resp) { return resp.json(); }).then(function(json) {
                if (!json || !json.success || !json.data) {
                    const msg = json && json.data && json.data.message ? json.data.message : __('Could not calculate the Square payment amount.');
                    throw new Error(msg);
                }
                return json.data;
            });
        }

        _buildSquarePaymentRequest(payments, preview) {
            return payments.paymentRequest({
                countryCode: (yatoonData.squareCountryCode || 'US').toUpperCase(),
                currencyCode: (preview.currency || yatoonData.currency || 'USD').toUpperCase(),
                total: {
                    amount: preview.amount || '0.00',
                    label: yatoonData.squarePaymentMode === 'full' ? __('Appointment total') : __('Booking deposit'),
                }
            });
        }

        _ensureSquarePaymentCard(payload) {
            const self = this;
            const payloadKey = JSON.stringify({
                services_json: payload.services_json || '',
                service_id: payload.service_id || '',
                selected_options: payload.selected_options || [],
                coupon_code: payload.coupon_code || '',
                customer_email: payload.customer_email || '',
                customer_phone: payload.customer_phone || ''
            });
            if (!this._squareCardPromise || this._squarePaymentPayloadKey !== payloadKey) {
                this._squarePaymentPayloadKey = payloadKey;
                this._squareCardPromise = new Promise(function(resolve, reject) {
                    if (!self._squarePaymentsReady()) {
                        reject(new Error(__('Square payment form is not ready. Please refresh and try again.')));
                        return;
                    }

                    let $box = $('#sbs-square-payment-box');
                    if (!$box.length) {
                        const modeText = yatoonData.squarePaymentMode === 'full'
                            ? __('Pay securely now to confirm your appointment.')
                            : __('Pay your deposit securely now to confirm your appointment.');
                        $box = $(`
                            <div id="sbs-square-payment-box" class="sbs-square-payment-box" style="margin:16px 0;padding:16px;border:1px solid #dbeafe;border-radius:14px;background:#eff6ff;">
                                <div style="font-size:14px;font-weight:800;color:#1e3a8a;margin-bottom:6px;">Secure Square Payment</div>
                                <div style="font-size:13px;color:#475569;margin-bottom:12px;">${modeText}</div>
                                <div id="sbs-square-payment-amount" style="font-size:13px;font-weight:800;color:#0f172a;margin-bottom:12px;"></div>
                                <div id="sbs-square-method-tabs" style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:12px;"></div>
                                <button id="sbs-square-apple-pay-button" type="button" class="sbs-square-method-panel" data-method-panel="apple_pay" style="display:none;width:100%;min-height:44px;border-radius:8px;border:0;background:#000;color:#fff;font-weight:800;">Apple Pay</button>
                                <div id="sbs-square-google-pay" class="sbs-square-method-panel" data-method-panel="google_pay" style="display:none;"></div>
                                <div id="sbs-square-cash-app-pay" class="sbs-square-method-panel" data-method-panel="cash_app_pay" style="display:none;"></div>
                                <div id="sbs-square-gift-card-container" class="sbs-square-method-panel" data-method-panel="gift_card" style="display:none;"></div>
                                <div id="sbs-square-card-container"></div>
                                <div id="sbs-square-payment-status" style="font-size:12px;color:#64748b;margin-top:10px;"></div>
                            </div>
                        `);
                        const $form = $('#sbs-booking-form');
                        if ($form.length) $form.prepend($box);
                        else $('.sbs-step-5').prepend($box);
                    }

                    $('#sbs-square-payment-status').text(__('Loading secure Square card form...'));
                    self._previewSquarePayment(payload)
                        .then(function(preview) {
                            $('#sbs-square-payment-amount').text((preview.payment_mode === 'full' ? 'Pay now: ' : 'Deposit due now: ') + preview.amount_display);
                            return Promise.resolve(window.Square.payments(yatoonData.squareApplicationId, yatoonData.squareLocationId))
                                .then(function(payments) {
                                    const enabled = self._squareMethods();
                                    const state = { payments: payments, preview: preview, methods: {}, selected: enabled[0] || 'card' };
                                    const paymentRequest = self._buildSquarePaymentRequest(payments, preview);
                                    const methodLabels = {
                                        card: 'Card',
                                        apple_pay: __('Apple Pay'),
                                        google_pay: __('Google Pay'),
                                        cash_app_pay: __('Cash App'),
                                        gift_card: __('Gift Card')
                                    };
                                    const $tabs = $('#sbs-square-method-tabs').empty();
                                    enabled.forEach(function(method) {
                                        $('<button/>', {
                                            type: 'button',
                                            class: 'sbs-square-method-tab',
                                            'data-method': method,
                                            text: methodLabels[method] || method
                                        }).css({
                                            border: '1px solid #bfdbfe',
                                            background: method === state.selected ? '#1d4ed8' : '#fff',
                                            color: method === state.selected ? '#fff' : '#1e3a8a',
                                            borderRadius: '999px',
                                            padding: '8px 12px',
                                            fontWeight: '800',
                                            cursor: 'pointer'
                                        }).appendTo($tabs);
                                    });

                                    function selectMethod(method) {
                                        state.selected = method;
                                        $('.sbs-square-method-tab').each(function() {
                                            const active = $(this).data('method') === method;
                                            $(this).css({ background: active ? '#1d4ed8' : '#fff', color: active ? '#fff' : '#1e3a8a' });
                                        });
                                        $('.sbs-square-method-panel').hide();
                                        $('#sbs-square-card-container').toggle(method === 'card');
                                        $('[data-method-panel="' + method + '"]').show();
                                        $('#sbs-square-payment-status').text(method === 'apple_pay' || method === 'cash_app_pay'
                                            ? __('Tap the wallet button above to authorize payment, then booking will continue automatically.')
                                            : __('Payment details are encrypted by Square.'));
                                    }
                                    $(document).off('click.squareMethod').on('click.squareMethod', '.sbs-square-method-tab', function() {
                                        selectMethod($(this).data('method'));
                                    });

                                    const initTasks = [];
                                    if (enabled.includes('card')) {
                                        initTasks.push(payments.card().then(function(card) {
                                            state.methods.card = card;
                                            return card.attach('#sbs-square-card-container');
                                        }));
                                    }
                                    if (enabled.includes('gift_card') && typeof payments.giftCard === 'function') {
                                        initTasks.push(payments.giftCard().then(function(giftCard) {
                                            state.methods.gift_card = giftCard;
                                            return giftCard.attach('#sbs-square-gift-card-container');
                                        }).catch(function() {}));
                                    }
                                    if (enabled.includes('apple_pay') && typeof payments.applePay === 'function') {
                                        initTasks.push(payments.applePay(paymentRequest).then(function(applePay) {
                                            state.methods.apple_pay = applePay;
                                            $('#sbs-square-apple-pay-button').off('click.squareApple').on('click.squareApple', function(e) {
                                                e.preventDefault();
                                                self._handleSquareWalletButton(payload, applePay, 'apple_pay');
                                            });
                                        }).catch(function() {
                                            $('[data-method="apple_pay"], [data-method-panel="apple_pay"]').hide();
                                        }));
                                    }
                                    if (enabled.includes('google_pay') && typeof payments.googlePay === 'function') {
                                        initTasks.push(payments.googlePay(paymentRequest).then(function(googlePay) {
                                            state.methods.google_pay = googlePay;
                                            return googlePay.attach('#sbs-square-google-pay');
                                        }).catch(function() {
                                            $('[data-method="google_pay"], [data-method-panel="google_pay"]').hide();
                                        }));
                                    }
                                    if (enabled.includes('cash_app_pay') && typeof payments.cashAppPay === 'function') {
                                        initTasks.push(payments.cashAppPay(paymentRequest, {
                                            redirectURL: window.location.href,
                                            referenceId: 'yatoon-' + Date.now()
                                        }).then(function(cashAppPay) {
                                            state.methods.cash_app_pay = cashAppPay;
                                            cashAppPay.addEventListener('ontokenization', function(event) {
                                                const detail = event.detail || {};
                                                if (detail.error) {
                                                    self._showToast(__('Cash App Pay could not be authorized.'), 'error');
                                                    return;
                                                }
                                                if (detail.tokenResult && detail.tokenResult.status === 'OK') {
                                                    self._finishSquareTokenPayment(payload, detail.tokenResult.token, 'cash_app_pay');
                                                }
                                            });
                                            return cashAppPay.attach('#sbs-square-cash-app-pay');
                                        }).catch(function() {
                                            $('[data-method="cash_app_pay"], [data-method-panel="cash_app_pay"]').hide();
                                        }));
                                    }

                                    return Promise.all(initTasks).then(function() {
                                        self._squarePaymentState = state;
                                        selectMethod(state.methods[state.selected] ? state.selected : (state.methods.card ? 'card' : Object.keys(state.methods)[0]));
                                        resolve(state);
                                    });
                                });
                        })
                        .catch(function(err) {
                            self._squareCardPromise = null;
                            reject(err);
                        });
                });
            }
            return this._squareCardPromise;
        }

        _finishSquareTokenPayment(payload, token, method) {
            const self = this;
            $('#sbs-square-payment-status').text(__('Completing') + ' ' + (method || __('Square')) + ' ' + __('payment...'));
            return this._sendSquareTokenPayment(payload, token)
                .then(function(paymentId) {
                    self._squarePreparedPaymentId = paymentId;
                    $('#sbs-square-payment-status').text(__('Payment approved by Square. Confirming your booking...'));
                    self._isSubmitting = false;
                    self.submitBooking();
                    return paymentId;
                })
                .catch(function(err) {
                    self._showToast(err && err.message ? err.message : __('Square payment failed. Please try again.'), 'error');
                    throw err;
                });
        }

        _handleSquareWalletButton(payload, paymentMethod, method) {
            const self = this;
            $('#sbs-square-payment-status').text(__('Opening') + ' ' + (method === 'apple_pay' ? __('Apple Pay') : __('Square wallet')) + '...');
            return paymentMethod.tokenize()
                .then(function(result) {
                    if (!result || result.status !== 'OK' || !result.token) {
                        const detail = result && result.errors && result.errors.length
                            ? result.errors.map(function(e) { return e.message; }).join(' ')
                            : __('The wallet payment was not authorized.');
                        throw new Error(detail);
                    }
                    return self._finishSquareTokenPayment(payload, result.token, method);
                })
                .catch(function(err) {
                    $('#sbs-square-payment-status').text(err && err.message ? err.message : __('Wallet payment failed.'));
                    self._showToast(err && err.message ? err.message : __('Wallet payment failed. Please try again.'), 'error');
                });
        }

        _sendSquareTokenPayment(payload, token) {
            const fd = new FormData();
            Object.keys(payload).forEach(function(key) {
                const val = payload[key];
                if (Array.isArray(val)) {
                    val.forEach(function(itemVal) { fd.append(key + '[]', itemVal); });
                } else {
                    fd.append(key, val == null ? '' : val);
                }
            });
            fd.set('action', 'yatoon_square_create_payment');
            fd.set('source_id', token);
            return fetch((yatoonData.ajax_url || yatoonData.ajaxUrl), {
                method: 'POST',
                body: fd,
                credentials: 'same-origin'
            }).then(function(resp) { return resp.json(); }).then(function(json) {
                if (!json || !json.success || !json.data || !json.data.payment_id) {
                    const msg = json && json.data && json.data.message ? json.data.message : __('Square payment failed. Please try again.');
                    throw new Error(msg);
                }
                return json.data.payment_id;
            });
        }

        _createSquarePaymentForPayload(payload) {
            const self = this;
            if (!this._squarePaymentsReady()) {
                return Promise.reject(new Error(__('Square payment is enabled but the secure payment form is unavailable.')));
            }

            return this._ensureSquarePaymentCard(payload)
                .then(function(state) {
                    $('#sbs-square-payment-box').show();
                    const box = document.getElementById('sbs-square-payment-box');
                    if (box) box.scrollIntoView({ behavior: 'smooth', block: 'center' });
                    const method = state.selected || 'card';
                    const paymentMethod = state.methods[method];
                    if (!paymentMethod) {
                        throw new Error(__('This Square payment method is not available on this device. Please choose another method.'));
                    }
                    if (method === 'cash_app_pay') {
                        $('#sbs-square-payment-status').text(__('Tap the Cash App Pay button above to authorize payment.'));
                        return new Promise(function() {});
                    }
                    $('#sbs-square-payment-status').text(__('Securing your payment with Square...'));
                    return paymentMethod.tokenize();
                })
                .then(function(result) {
                    if (!result || result.status !== 'OK' || !result.token) {
                        const detail = result && result.errors && result.errors.length
                            ? result.errors.map(function(e) { return e.message; }).join(' ')
                            : __('Please check your card details and try again.');
                        throw new Error(detail);
                    }
                    return self._sendSquareTokenPayment(payload, result.token);
                })
                .then(function(paymentId) {
                    $('#sbs-square-payment-status').text(__('Payment approved by Square.'));
                    return paymentId;
                })
                .catch(function(err) {
                    $('#sbs-square-payment-status').text(err && err.message ? err.message : __('Square payment failed.'));
                    self._showToast(err && err.message ? err.message : __('Square payment failed. Please try again.'), 'error');
                    throw err;
                });
        }

        submitBooking() {
            const self = this;
            if (this._isSubmitting) {
                return;
            }
            if (this.designUploadInProgress) {
                this._showToast(__('Please wait for the reference photo to finish uploading.'), 'info');
                return;
            }

            // -- Turnstile: run the invisible check right now, at the moment of
            // submission, so the token is always fresh and never blocked by a
            // hidden step container. Runs once per submit attempt.
            if (!this._turnstileTokenFetched && typeof window.yatoonGetTurnstileToken === 'function') {
                this._turnstileTokenFetched = true;
                window.yatoonGetTurnstileToken().then(function(token) {
                    window.yatoonTurnstileToken = token;
                    self._turnstileTokenFetched = false;
                    self.submitBooking();
                });
                return;
            }

            // -- OTP Verification Gate ----------------------------------------
            const otpSection   = document.getElementById('sbs-otp-section');
            const verifyEmail  = (typeof yatoonData !== 'undefined') && yatoonData.verifyEmailOtp === '1';
            const verifySms    = (typeof yatoonData !== 'undefined') && yatoonData.verifySmsOtp   === '1';
            if (otpSection && (verifyEmail || verifySms)) {
                const alreadyVerified = otpSection.dataset.verified === '1';
                if (!alreadyVerified) {
                    const email  = (document.getElementById('sbs-customer-email') || {}).value || '';
                    const phone  = (document.getElementById('sbs-customer-phone') || {}).value || '';
                    const name   = (document.getElementById('sbs-customer-name')  || {}).value || '';
                    const channel = verifySms ? 'sms' : 'email';
                    otpSection.style.display = 'block';
                    document.getElementById('sbs-otp-prompt').textContent =
                        channel === 'sms'
                        ? __('We are sending a verification code to your phone. Please enter it below.')
                        : __('We are sending a verification code to') + ' ' + email + '. ' + __('Please enter it below.');
                    // Send OTP automatically if not yet sent
                    if (!otpSection.dataset.sent) {
                        otpSection.dataset.sent = '1';
                        jQuery.post(yatoonData.ajaxUrl, {
                            action:'yatoon_send_otp', nonce: yatoonData.nonce,
                            channel, email, phone, name
                        }, function(resp) {
                            const msg = document.getElementById('sbs-otp-msg');
                            if(msg) msg.textContent = resp.success ? resp.data.message : (resp.data && resp.data.message ? resp.data.message : __('Failed to send code.'));
                            const resend = document.getElementById('sbs-otp-resend-btn');
                            if(resend) resend.style.display = '';
                        });
                    }
                    // Verify button handler (one-time bind via data flag)
                    const verifyBtn = document.getElementById('sbs-otp-verify-btn');
                    if(verifyBtn && !verifyBtn.dataset.bound) {
                        verifyBtn.dataset.bound = '1';
                        verifyBtn.addEventListener('click', function() {
                            const code = (document.getElementById('sbs-otp-input') || {}).value || '';
                            const msgEl = document.getElementById('sbs-otp-msg');
                            jQuery.post(yatoonData.ajaxUrl, {
                                action:'yatoon_verify_otp', nonce: yatoonData.nonce,
                                channel, email, phone, code
                            }, function(resp) {
                                if(resp.success) {
                                    otpSection.dataset.verified = '1';
                                    otpSection.dataset.verifiedKey = resp.data && resp.data.verified_key ? resp.data.verified_key : '';
                                    otpSection.style.display = 'none';
                                    self.submitBooking(); // re-trigger now verified
                                } else {
                                    if(msgEl) msgEl.textContent = resp.data && resp.data.message ? resp.data.message : __('Incorrect code.');
                                    if(msgEl) msgEl.style.color = '#dc2626';
                                }
                            });
                        });
                    }
                    // Resend handler
                    const resendBtn = document.getElementById('sbs-otp-resend-btn');
                    if(resendBtn && !resendBtn.dataset.bound) {
                        resendBtn.dataset.bound = '1';
                        resendBtn.addEventListener('click', function() {
                            otpSection.dataset.sent = '';
                            const msgEl = document.getElementById('sbs-otp-msg');
                            if(msgEl) msgEl.textContent = '';
                            self.submitBooking();
                        });
                    }
                    return; // halt - wait for user to verify
                }
            }
            // -- End OTP Gate -------------------------------------------------

            this._isSubmitting = true;
            this._clearBookingRecovery();
            if (this.bookingCart.length === 0 && this.selectedDateTime && (this.pendingServices.length > 0 || this.selectedService)) {
                this.addToCart();
            }
            if (this.bookingCart.length === 0) {
                this._isSubmitting = false;
                this._showToast(__('Please add at least one service to your booking.'), 'error');
                return;
            }
            const submitBtn = $('.sbs-btn-submit');
            const cartNextBtn = $('#sbs-cart-next-btn');
            const sidebarNextBtn = $('#sbs-sidebar-next-btn');
            const origText  = submitBtn.text();
            const origCartText = cartNextBtn.text();
            const origSidebarText = sidebarNextBtn.text();
            const restoreSubmitButtons = () => {
                this._isSubmitting = false;
                submitBtn.prop('disabled', false).text(origText);
                cartNextBtn.prop('disabled', false).removeClass('sbs-submitting').text(origCartText || __('Confirm Booking'));
                sidebarNextBtn.prop('disabled', false).removeClass('sbs-submitting').text(origSidebarText || __('Confirm Booking'));
            };
            submitBtn.prop('disabled', true).text(__('Confirming your booking...'));
            cartNextBtn.prop('disabled', true).addClass('sbs-submitting').text('CONFIRMING...');
            sidebarNextBtn.prop('disabled', true).addClass('sbs-submitting').text('CONFIRMING...');
            this._updateLegalBoxes();

            const customerInfo = {
                customer_name:  $('#sbs-customer-name').val(),
                customer_email: $('#sbs-customer-email').val(),
                  customer_phone: $('#sbs-customer-phone').val(),
                  notes:          $('#sbs-customer-notes').val(),
                  coupon_code:    $('#sbs-coupon-code').length ? $('#sbs-coupon-code').val() : '',
                  consent_accepted: $('#sbs-consent-accepted').length ? ($('#sbs-consent-accepted').is(':checked') ? 1 : 0) : 0,
                consent_signature: $('#sbs-consent-signature').length ? $('#sbs-consent-signature').val() : '',
                policy_acknowledged: $('#sbs-policy-acknowledged').length ? ($('#sbs-policy-acknowledged').is(':checked') ? 1 : 0) : 0,
                tip_amount: parseFloat($('#sbs-tip-amount').val()) || 0,
                gift_card_code: $('#sbs-gift-card-code-confirmed').val() || '',
                custom_fields: (function() {
                    const cf = {};
                    $('[name^="cf["]').each(function() {
                        const key = $(this).attr('name').replace(/^cf\[|\]$/g,'');
                        if ($(this).is(':checkbox')) { if ($(this).is(':checked')) cf[key] = $(this).val(); }
                        else if ($(this).is(':radio')) { if ($(this).is(':checked')) cf[key] = $(this).val(); }
                        else cf[key] = $(this).val();
                    });
                    return JSON.stringify(cf);
                })()
            };
            const guestContacts = this._collectGuestContacts();
            // -- Phone validation on submit ---------------------------------
            const phoneMode = (typeof yatoonData !== 'undefined' && yatoonData.phoneMode) ? yatoonData.phoneMode : 'us_only';
            if (phoneMode === 'us_only' && customerInfo.customer_phone) {
                const phoneErr = validateUSPhone(customerInfo.customer_phone);
                if (phoneErr) {
                    restoreSubmitButtons();
                    showPhoneError(phoneErr);
                    document.getElementById('sbs-customer-phone') && document.getElementById('sbs-customer-phone').scrollIntoView({ behavior: 'smooth', block: 'center' });
                    return;
                }
            }

            if (this._needsPolicyAck() && !customerInfo.policy_acknowledged) {
                restoreSubmitButtons();
                this._showToast(__('Please review and acknowledge the cancellation / no-show policy before booking.'), 'error');
                return;
            }

            if (this._needsConsent() && (!customerInfo.consent_accepted || !customerInfo.consent_signature.trim())) {
                restoreSubmitButtons();
                this._showToast(__('Please read and sign the consent form before booking.'), 'error');
                return;
            }

            // Each cart item = one appointment (may contain multiple services).
            // CRITICAL: Submit appointments SEQUENTIALLY (not in parallel) so that the
            // server-side staff-overlap check sees previously committed bookings.
            // Parallel submission causes a race condition where two bookings for the
            // same staff at the same time both pass the DB conflict check simultaneously.
            const cartItems = this.bookingCart.slice(); // snapshot; each visible cart item is one appointment
            const responses = [];
            const bookingReference = this._newBookingReference();
            const ajaxUrl = (typeof yatoonData !== 'undefined' && (yatoonData.ajax_url || yatoonData.ajaxUrl))
                ? (yatoonData.ajax_url || yatoonData.ajaxUrl)
                : '';
            if (!ajaxUrl) {
                restoreSubmitButtons();
                this._showToast(__('Booking could not start because the WordPress AJAX URL is missing. Please refresh and try again.'), 'error');
                return;
            }
            if (!cartItems.length) {
                restoreSubmitButtons();
                this._showToast(__('Please choose a service and time before confirming.'), 'error');
                return;
            }

            const hasGroupedCartItem = cartItems.some(item => item && item.services && item.services.length > 1);
            const submissionGroupId = (cartItems.length > 1 || hasGroupedCartItem)
                ? ('yatoon-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10))
                : '';
            let uploadedDesignAttachmentId = this.designUploadAttachmentId || 0;
            let uploadedDesignToken = this.designUploadToken || '';

            const submitNext = (idx) => {
                if (idx >= cartItems.length) {
                    // All done
                    const allOk = responses.every(r => r && r.success);
                    if (allOk) {
                        const wooCheckout = responses.find(r => r && r.success && r.data && r.data.woocommerce_checkout_url);
                        if (wooCheckout && wooCheckout.data.woocommerce_checkout_url) {
                            this.bookingConfirmed = true;
                            this._clearSession();
                            window.location.assign(wooCheckout.data.woocommerce_checkout_url);
                            return;
                        }
                        this.bookingConfirmed = true;
                        $('.sbs-form-step').removeClass('active');
                        $('.sbs-step-success').show();
                        this._updateStepAccordion();
                        this._markBookingFlowComplete();
                        $('#sbs-success-count').text(cartItems.length);
                        const _isPlural = cartItems.length !== 1;
                        $('#sbs-success-booking-word').text(_isPlural ? __('Bookings') : __('Booking'));
                        $('#sbs-success-email-word').text(_isPlural ? 'emails' : 'email');
                        $('#sbs-success-have-word').text(_isPlural ? 'have' : 'has');
                        this._renderSuccessBookingSummary(cartItems);
                        const manageResponses = responses.filter(r => r && r.success && r.data && r.data.manage_url && String(r.data.manage_url).length);
                        const firstManageUrl = manageResponses.length && manageResponses[0].data ? manageResponses[0].data.manage_url : '';
                        const firstCalendarResponse = responses.find(r => r && r.success && r.data && r.data.calendar_url && String(r.data.calendar_url).length);
                        const firstCalendarUrl = firstCalendarResponse && firstCalendarResponse.data ? firstCalendarResponse.data.calendar_url : '';
                        const portalUrl = yatoonData.portalUrl || '';
                        const portalLabel = yatoonData.portalLabel || __('My Appointments');
                        const $successActions = $('#sbs-success-actions').empty();

                        $('<div/>', {
                            class: 'sbs-success-action-note',
                            text: yatoonData.successNote || __('Your appointment is booked. Save this page or use the manage link below if you need to change services, reschedule, or cancel.')
                        }).appendTo($successActions);

                        if (firstManageUrl && manageResponses.length === 1) {
                            // Single booking: one clear action button
                            $('<a/>', {
                                class: 'sbs-success-action-link primary',
                                href: firstManageUrl,
                                target: '_blank',
                                rel: 'noopener',
                                text: (typeof yatoonData !== 'undefined' && yatoonData.isPro) ? __('Change services, reschedule, or cancel') : __('Reschedule or cancel')
                            }).appendTo($successActions);
                        }
                        if (manageResponses.length > 1) {
                            const $manageList = $('<div/>', { class: 'sbs-success-manage-list' }).appendTo($successActions);
                            const hasGuestLabels = cartItems.some(item => item.guestLabel && item.guestLabel !== 'Me');
                            manageResponses.forEach((r, i) => {
                                const matchingItem = cartItems[i];
                                const guestLabel = (matchingItem && matchingItem.guestLabel && matchingItem.guestLabel !== 'Me') ? matchingItem.guestLabel : null;
                                let btnText;
                                if (guestLabel) {
                                    // Guest group booking: label by guest name
                                    btnText = __('Change or cancel for') + ' ' + guestLabel;
                                } else if (hasGuestLabels) {
                                    // Mixed: some guests, this one is "Me"
                                    btnText = __('Change or cancel my appointment');
                                } else {
                                    // Multi-service same person: label by service name
                                    const svcName = (r.data && r.data.service_name) ? r.data.service_name : (__('Appointment') + ' ' + (i + 1));
                                    btnText = 'Change or cancel: ' + svcName;
                                }
                                $('<a/>', {
                                    class: 'sbs-success-action-link compact',
                                    href: r.data.manage_url,
                                    target: '_blank',
                                    rel: 'noopener',
                                    text: btnText
                                }).appendTo($manageList);
                            });
                        }
                        if (firstCalendarUrl && !$('#sbs-success-cal-row').length) {
                            $('<a/>', {
                                class: 'sbs-success-action-link',
                                href: firstCalendarUrl,
                                text: __('Add to calendar')
                            }).appendTo($successActions);
                        }
                        if (portalUrl) {
                            $('<a/>', {
                                class: 'sbs-success-action-link',
                                href: portalUrl,
                                text: portalLabel
                            }).appendTo($successActions);
                        }
                        $('<button/>', {
                            type: 'button',
                            class: 'sbs-success-action-link sbs-success-book-again' + (!firstManageUrl ? ' primary' : ''),
                            text: __('Book again')
                        }).appendTo($successActions);
                        $successActions.show();

                        // Google Calendar + ICS calendar buttons
                        if (firstCalendarUrl) {
                            $('#sbs-cal-ics').attr('href', firstCalendarUrl);
                            const firstBookingData = responses.find(r => r && r.success && r.data && r.data.start_time);
                            const firstCartItem = cartItems[0] || null;
                            const fallbackStart = firstCartItem && firstCartItem.dateTime ? firstCartItem.dateTime.start_at : '';
                            const fallbackDuration = firstCartItem ? this._getItemVisitDuration(firstCartItem) : 0;
                            const fallbackStartDate = fallbackStart ? new Date(fallbackStart) : null;
                            const fallbackEnd = fallbackStartDate && !isNaN(fallbackStartDate.getTime()) && fallbackDuration
                                ? new Date(fallbackStartDate.getTime() + fallbackDuration * 60000).toISOString()
                                : '';
                            // The selected slot carries an RFC 3339 offset and
                            // is the authoritative instant. Prefer it to DB
                            // wall-clock strings returned by older endpoints.
                            // Convert BOTH boundaries to UTC; the previous fmt
                            // stripped the minus sign from -07:00 on start but
                            // used an ISO-Z fallback for end, producing an
                            // invalid mixed Google Calendar range.
                            const calendarStart = fallbackStart || (firstBookingData && firstBookingData.data.start_time ? firstBookingData.data.start_time : '');
                            const calendarEnd = fallbackEnd || (firstBookingData && firstBookingData.data.end_time ? firstBookingData.data.end_time : '');
                            if (calendarStart && calendarEnd) {
                                const googleUtcStamp = function(t) {
                                    const parsed = new Date(t);
                                    if (isNaN(parsed.getTime())) return '';
                                    return parsed.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}Z$/, 'Z');
                                };
                                const firstServices = firstCartItem ? (firstCartItem.services || [{ service: firstCartItem.service }]) : [];
                                const serviceTitle = firstServices.map(entry => entry.service && entry.service.name).filter(Boolean).join(', ');
                                const gTitle = encodeURIComponent((yatoonData.businessName || __('Appointment')) + (serviceTitle ? ' - ' + serviceTitle : ''));
                                const gStart = encodeURIComponent(googleUtcStamp(calendarStart));
                                const gEnd   = encodeURIComponent(googleUtcStamp(calendarEnd));
                                const siteTz = encodeURIComponent((yatoonData && yatoonData.siteTimezone) ? yatoonData.siteTimezone : Intl.DateTimeFormat().resolvedOptions().timeZone);
                                if (gStart && gEnd) {
                                    $('#sbs-cal-google').attr('href', 'https://calendar.google.com/calendar/render?action=TEMPLATE&text=' + gTitle + '&dates=' + gStart + '/' + gEnd + '&ctz=' + siteTz);
                                } else {
                                    $('#sbs-cal-google').attr('href', firstCalendarUrl);
                                }
                            } else {
                                $('#sbs-cal-google').attr('href', firstCalendarUrl);
                            }
                            $('#sbs-success-cal-row').show();
                        }

                        // Rebooking nudge - fade in after a beat, hide if admin left text blank
                        if (yatoonData.rebookingNudgeText) {
                            $('#sbs-rebooking-nudge .sbs-rebooking-text').text(yatoonData.rebookingNudgeText);
                            $('#sbs-rebooking-nudge .sbs-rebooking-btn').text(yatoonData.rebookingNudgeBtn || __('Book next visit'));
                            setTimeout(function() { $('#sbs-rebooking-nudge').fadeIn(400); }, 1200);
                        }

                        // Recurring setup - show if Pro and first booking has token
                        const firstBookingData = responses.find(r => r && r.success && r.data && r.data.booking_id);
                        if (firstBookingData && firstBookingData.data.manage_token && $('#sbs-recurring-setup').length) {
                            const recurringBookingId = firstBookingData.data.booking_id;
                            const recurringToken     = firstBookingData.data.manage_token;
                            let selectedWeeks = 2;

                            setTimeout(function() { $('#sbs-recurring-setup').fadeIn(400); }, 2000);

                            // Frequency buttons
                            $(document).on('click', '.sbs-freq-btn', function() {
                                $('.sbs-freq-btn').removeClass('active');
                                $(this).addClass('active');
                                selectedWeeks = parseInt($(this).data('weeks'));
                            });

                            // Confirm button
                            $(document).on('click', '#sbs-recurring-confirm', function() {
                                const $btn = $(this).prop('disabled', true).text(__('Setting up...'));
                                const count = parseInt($('#sbs-recurring-count').val()) || 6;
                                $.post(ajaxurl, {
                                    action:          'yatoon_setup_recurring',
                                    nonce:           yatoonData.nonce,
                                    booking_id:      recurringBookingId,
                                    manage_token:    recurringToken,
                                    frequency_weeks: selectedWeeks,
                                    total_count:     count,
                                }, function(resp) {
                                    const $result = $('#sbs-recurring-result');
                                    if (resp.success) {
                                        $btn.text('Done');
                                        $result.removeClass('error').text(resp.data.message).show();
                                    } else {
                                        $btn.prop('disabled', false).text(__('Try again'));
                                        $result.addClass('error').text((resp.data && resp.data.message) || __('Could not set up recurring.')).show();
                                    }
                                }).fail(function() {
                                    $btn.prop('disabled', false).text(__('Try again'));
                                    $('#sbs-recurring-result').addClass('error').text(__('Network error. Please try again.')).show();
                                });
                            });
                        }
                        self.bookingCart     = [];
                        self.pendingServices = [];
                        self._clearSession();
                        self._clearBookingRecovery();
                        self._bookingAttemptReference = '';
                        // Hide the mobile cart bar - booking is complete
                        $('#sbs-mobile-cart-bar').removeClass('sbs-bar-visible').css('display', 'none');
                        self._clearCartBarSpacing();
                        self._closeDrawer();
                        self._isSubmitting = false;
                        restoreSubmitButtons();
                        cartNextBtn.hide();
                        sidebarNextBtn.hide();
                    } else {
                        const failed  = responses.filter(r => !r || !r.success);
                        const errMsg  = failed.map(r => r && r.data && r.data.message ? r.data.message : '').filter(Boolean).join(' ');
                        const hint    = errMsg ? `\n\nDetails: ${errMsg}` : '';
                        const okCount = responses.length - failed.length;
                        self._showToast(`${okCount} booking(s) confirmed, but ${failed.length} failed. Please contact us.${hint}`, 'error');
                        const firstFailure = failed[0] && failed[0].data ? failed[0].data : {};
                        self._showBookingRecovery({
                            reference: firstFailure.reference || bookingReference,
                            failedIndex: okCount,
                            confirmedCount: okCount,
                            message: errMsg || __('One or more appointments were not confirmed.'),
                            safeToRetry: firstFailure.retry_safe === true
                        });
                        restoreSubmitButtons();
                    }
                    return;
                }

                const item     = cartItems[idx];
                const services = item.services || [{ service: item.service, options: item.options, staff: item.staff }];
                const staffMapRaw = item.dateTime && item.dateTime.staff_map ? item.dateTime.staff_map : null;
                const staffMap = (typeof staffMapRaw === 'string') ? (function() {
                    try { return JSON.parse(staffMapRaw); } catch(e) { return null; }
                })() : staffMapRaw;
                const remappedStaffMap = {};
                services.forEach(function(s, newIdx) {
                    const originalIdx = (typeof s._originalIndex !== 'undefined') ? s._originalIndex : newIdx;
                    if (staffMap && typeof staffMap[originalIdx] !== 'undefined') {
                        remappedStaffMap[newIdx] = staffMap[originalIdx];
                    }
                });
                const staffMapJson = Object.keys(remappedStaffMap).length
                    ? JSON.stringify(remappedStaffMap)
                    : (staffMapRaw ? (typeof staffMapRaw === 'string' ? staffMapRaw : JSON.stringify(staffMapRaw)) : '');
                const itemGuestLabel = item.guestLabel || (services[0] && services[0].guestLabel) || 'Me';
                const guestContact = guestContacts[itemGuestLabel] || {};
                let itemNotes = customerInfo.notes || '';
                if (itemGuestLabel !== 'Me') {
                    const guestBits = [`Guest: ${itemGuestLabel}`];
                    if (guestContact.name) guestBits.push(`Name: ${guestContact.name}`);
                    if (guestContact.phone) guestBits.push(`Phone: ${guestContact.phone}`);
                    itemNotes = `${itemNotes}${itemNotes ? '\n\n' : ''}${guestBits.join(' - ')}`;
                }

                // Each guest's services are sequential, while different guests
                // may run in parallel. Carry the actual segment start into the
                // server quote so time-window pricing matches the saved booking.
                const paymentGuestOffsets = {};
                const paymentStartMs = new Date(item.dateTime.start_at).getTime();
                const paymentServices = services.map(s => {
                    const guestLabel = s.guestLabel || item.guestLabel || 'Me';
                    const offset = paymentGuestOffsets[guestLabel] || 0;
                    const serviceStart = Number.isFinite(paymentStartMs)
                        ? new Date(paymentStartMs + offset * 60000).toISOString()
                        : item.dateTime.start_at;
                    paymentGuestOffsets[guestLabel] = offset + self._serviceDuration(s);
                    return {
                        service_id: s.service.id,
                        staff_id: s.staff ? s.staff.id : 0,
                        option_ids: s.options.map(o => o.id),
                        guest_label: guestLabel,
                        start_at: serviceStart,
                        design_attachment_id: parseInt(s.designAttachmentId || 0, 10) || 0,
                        design_upload_token: String(s.designUploadToken || '')
                    };
                });

                const payload = {
                    action:          'yatoon_create_booking',
                    nonce:           yatoonData.nonce,
                    client_reference: `${bookingReference}-${idx + 1}`,
                    start_at:        item.dateTime.start_at,
                    // staff_map: Square real-time staff assignment captured at Step 4.
                    // Key = service position index (0,1,2...), value = Square team_member_id.
                    // Passed to PHP so Pass2 can assign staff without re-querying Square.
                    staff_map_json:  staffMapJson,
                    services_json:   JSON.stringify(paymentServices),
                    // Legacy single-service fields (fallback for PHP)
                    service_id:       services[0].service.id,
                    staff_id:         services[0].staff ? services[0].staff.id : 0,
                    selected_options: services[0].options.map(o => o.id),
                    notes:            itemNotes,
                    guest_label:      itemGuestLabel,
                    guest_name:       guestContact.name || '',
                    guest_phone:      guestContact.phone || '',
                    booking_group_id: submissionGroupId,
                    coupon_code:      customerInfo.coupon_code,
                    // Tip/gift/custom fields belong to the complete checkout,
                    // not to every cart request. Apply monetary adjustments
                    // once so grouped services and guest carts cannot double
                    // charge them.
                    tip_amount:       idx === 0 ? customerInfo.tip_amount : 0,
                    gift_card_code:   idx === 0 ? customerInfo.gift_card_code : '',
                    custom_fields:    customerInfo.custom_fields,
                    customer_name:    customerInfo.customer_name,
                    customer_email:   customerInfo.customer_email,
                    customer_phone:   customerInfo.customer_phone,
                    otp_channel:      ((typeof yatoonData !== 'undefined') && yatoonData.verifySmsOtp === '1') ? 'sms' : 'email',
                    otp_verified_key: (document.getElementById('sbs-otp-section') && document.getElementById('sbs-otp-section').dataset.verifiedKey) ? document.getElementById('sbs-otp-section').dataset.verifiedKey : '',
                    consent_accepted: customerInfo.consent_accepted,
                    consent_signature: customerInfo.consent_signature,
                    policy_acknowledged: customerInfo.policy_acknowledged,
                    design_attachment_id: uploadedDesignAttachmentId || '',
                    design_upload_token: uploadedDesignToken || '',
                    website:          $('#sbs-website').val() || '',
                    form_rendered_at: $('#sbs-form-rendered-at').val() || '',
                    cf_turnstile_response: (typeof window.yatoonTurnstileToken !== 'undefined') ? window.yatoonTurnstileToken : '',
                    yatoon_test_booking: $('.sbs-booking-container').hasClass('yatoon-test-mode') ? '1' : ''
                };
                const designInput = document.getElementById('sbs-design-reference');
                const designFile = designInput && designInput.files && designInput.files[0] ? designInput.files[0] : null;
                let ajaxData = payload;
                let ajaxProcessData = true;
                let ajaxContentType = 'application/x-www-form-urlencoded; charset=UTF-8';
                if (designFile && !uploadedDesignAttachmentId) {
                    ajaxData = new FormData();
                    Object.keys(payload).forEach(function(key) {
                        const val = payload[key];
                        if (Array.isArray(val)) {
                            val.forEach(function(itemVal) {
                                ajaxData.append(key + '[]', itemVal);
                            });
                        } else {
                            ajaxData.append(key, val);
                        }
                    });
                    ajaxData.append('design_reference', designFile);
                    ajaxProcessData = false;
                    ajaxContentType = false;
                }

                const sendBookingRequest = function() {
                $.ajax({
                    url:  ajaxUrl,
                    type: 'POST',
                    timeout: 45000,
                    data: ajaxData,
                    processData: ajaxProcessData,
                    contentType: ajaxContentType,
                    success: function(resp, textStatus, xhr) {
                        try {
                            if (typeof resp === 'string') {
                                const jsonStart = resp.indexOf('{');
                                const jsonEnd = resp.lastIndexOf('}');
                                const jsonText = (jsonStart >= 0 && jsonEnd > jsonStart) ? resp.slice(jsonStart, jsonEnd + 1) : resp;
                                resp = JSON.parse(jsonText);
                            }
                            responses.push(resp);
                            if (!resp || !resp.success) {
                                // Stop on first failure - don't submit remaining appointments
                                const errMsg = resp && resp.data && resp.data.message ? resp.data.message : '';
                                const hint   = errMsg ? `\n\nDetails: ${errMsg}` : '';
                                const okCount = responses.length - 1;
                                self._showToast(`${okCount} booking(s) confirmed, but appointment ${idx + 1} failed. Please contact us.${hint}`, 'error');
                                const failureData = resp && resp.data ? resp.data : {};
                                self._showBookingRecovery({
                                    reference: failureData.reference || `${bookingReference}-${idx + 1}`,
                                    failedIndex: idx,
                                    confirmedCount: okCount,
                                    message: errMsg || __('The appointment was not confirmed.'),
                                    safeToRetry: failureData.retry_safe === true,
                                    uncertain: failureData.outcome_uncertain === true
                                });
                                restoreSubmitButtons();
                                return;
                            }
                            if (resp.data && resp.data.design_attachment_id) {
                                uploadedDesignAttachmentId = parseInt(resp.data.design_attachment_id, 10) || uploadedDesignAttachmentId;
                                self.designUploadAttachmentId = uploadedDesignAttachmentId;
                            }
                            if (resp.data && resp.data.design_upload_token) {
                                uploadedDesignToken = String(resp.data.design_upload_token || '');
                                self.designUploadToken = uploadedDesignToken;
                            }
                            submitNext(idx + 1);
                        } catch (err) {
                            console.error(__('Yatoon booking submit failed after server response:'), err);
                            if (self.bookingConfirmed || $('.sbs-step-success:visible').length > 0) {
                                return;
                            }
                            const responseText = xhr && xhr.responseText ? String(xhr.responseText) : (typeof resp === 'string' ? resp : '');
                            if (responseText && responseText.indexOf('"success":true') !== -1) {
                                responses.push({ success: true, data: {} });
                                submitNext(idx + 1);
                                return;
                            }
                            self._showToast(__('We could not read the booking confirmation response. Please check the booking list before trying again.'), 'error');
                            self._showBookingRecovery({
                                reference: `${bookingReference}-${idx + 1}`,
                                failedIndex: idx,
                                confirmedCount: responses.filter(r => r && r.success).length,
                                message: __('We could not verify the server response.'),
                                uncertain: true
                            });
                            restoreSubmitButtons();
                        }
                    },
                    error: function(xhr, status) {
                        responses.push({ success: false });
                        const okCount = responses.length - 1;
                        const reason = status === 'timeout'
                            ? 'the request timed out'
                            : 'a network error';
                        self._showToast(`${okCount} booking(s) confirmed, but appointment ${idx + 1} failed due to ${reason}. Please contact us.`, 'error');
                        self._showBookingRecovery({
                            reference: `${bookingReference}-${idx + 1}`,
                            failedIndex: idx,
                            confirmedCount: okCount,
                            message: status === 'timeout' ? __('The request timed out.') : __('The network connection was interrupted.'),
                            uncertain: true
                        });
                        restoreSubmitButtons();
                    }
                });
                };

                if (self._squarePreparedPaymentId) {
                    if (ajaxData instanceof FormData) {
                        ajaxData.set('square_payment_id', self._squarePreparedPaymentId);
                    } else {
                        ajaxData.square_payment_id = self._squarePreparedPaymentId;
                    }
                    self._squarePreparedPaymentId = '';
                    sendBookingRequest();
                } else if (self._paymentProviderEnabled('square')) {
                    self._ensurePaymentSdk('square')
                        .then(function() { return self._createSquarePaymentForPayload(payload); })
                        .then(function(paymentId) {
                            if (ajaxData instanceof FormData) {
                                ajaxData.set('square_payment_id', paymentId);
                            } else {
                                ajaxData.square_payment_id = paymentId;
                            }
                            sendBookingRequest();
                        })
                        .catch(function(error) {
                            self._showToast(error && error.message ? error.message : __('Square payment could not be started.'), 'error');
                            restoreSubmitButtons();
                        });
                } else if (self._paymentProviderEnabled('paypal')) {
                    self._ensurePaymentSdk('paypal')
                        .then(function() { return self._createPayPalPaymentForPayload(payload); })
                        .then(function(orderId) {
                            if (ajaxData instanceof FormData) {
                                ajaxData.set('paypal_order_id', orderId);
                            } else {
                                ajaxData.paypal_order_id = orderId;
                            }
                            sendBookingRequest();
                        })
                        .catch(function(error) {
                            self._showToast(error && error.message ? error.message : __('PayPal Checkout could not be started.'), 'error');
                            restoreSubmitButtons();
                        });
                } else if (self._paymentProviderEnabled('stripe')) {
                    self._ensurePaymentSdk('stripe')
                        .then(function() { return self._createStripePaymentForPayload(payload); })
                        .then(function(intentId) {
                            if (ajaxData instanceof FormData) {
                                ajaxData.set('deposit_intent_id', intentId);
                            } else {
                                ajaxData.deposit_intent_id = intentId;
                            }
                            sendBookingRequest();
                        })
                        .catch(function(error) {
                            self._showToast(error && error.message ? error.message : __('Stripe payment could not be started.'), 'error');
                            restoreSubmitButtons();
                        });
                } else {
                    sendBookingRequest();
                }
            };

            try {
                submitNext(0);
            } catch (err) {
                console.error(__('Yatoon booking submit error:'), err);
                this._showToast(__('Something went wrong before the booking could be sent. Please refresh and try again.'), 'error');
                restoreSubmitButtons();
            }
        }
        
        addToCart() {
            if (this._pendingAdd) return;
            this._pendingAdd = true;

            // Build one cart item from ALL pendingServices + shared dateTime
            const services = this.pendingServices.length > 0
                ? [...this.pendingServices]
                : [{ service: { ...this.selectedService },
                     options: [...this.selectedOptions],
                     staff: this.selectedStaff,
                     eligibleStaffIds: (this.selectedService && this.selectedService._eligibleStaffIds) || [],
                     designAttachmentId: this.designUploadAttachmentId || 0,
                     designUploadToken: this.designUploadToken || '',
                     designImageUrl: this.designUploadImageUrl || '',
                     designFileName: this.designUploadFileName || '' }];

            const cartItem = {
                services: services,      // array of {service, options, staff}
                dateTime: this.selectedDateTime,
                // Convenience aliases for single-service backward compat
                service:  services[0].service,
                options:  services[0].options,
                staff:    services[0].staff,
            };
            this.bookingCart.push(cartItem);

            // Reset everything for next appointment
            this.pendingServices  = [];
            this._nextGuestLabel  = null;
            this._guestCount      = 0;
            this.selectedService  = null;
            this.selectedCategory = null;
            this.selectedOptions  = [];
            this.selectedStaff    = null;
            this.selectedDateTime = null;

            this.renderCart();
            this._renderGuestContactFields();
            this._saveSession();
            setTimeout(() => { this._pendingAdd = false; }, 300);
        }
        
        renderCart() {
            const container = $('#sbs-booking-cart-list');
            if (!container.length) return;
            
            const MAX_APPOINTMENTS = 2;
            // Show/hide the Add Another Appointment button based on limit and mode
            const $addAnotherBtn = $('#sbs-add-another-appt-btn');
            if (!yatoonData.multiApptEnabled || this.bookingCart.length >= MAX_APPOINTMENTS) {
                $addAnotherBtn.hide();
            } else {
                $addAnotherBtn.show();
            }
            
            if (this.bookingCart.length === 0) {
                container.html(`<div style="text-align:center;padding:28px 16px;">
                    <svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin:0 auto 12px;display:block;">
                        <rect x="4" y="8" width="40" height="32" rx="5" fill="#f3f4f6" stroke="#e5e7eb" stroke-width="1.5"/>
                        <rect x="10" y="16" width="18" height="3" rx="1.5" fill="#d1d5db"/>
                        <rect x="10" y="22" width="12" height="3" rx="1.5" fill="#e5e7eb"/>
                        <circle cx="36" cy="24" r="7" fill="var(--primary-color, #ec4899)" opacity="0.15"/>
                        <path d="M33 24h6M36 21v6" stroke="var(--primary-color, #ec4899)" stroke-width="2" stroke-linecap="round"/>
                    </svg>
                    <p style="font-size:14px;font-weight:600;color:#374151;margin:0 0 4px;">Pick a service above</p>
                    <p style="font-size:12px;color:#9ca3af;margin:0;">Tap any service card to add it to your booking</p>
                </div>`);
                this._applyConfirmBtn();
                return;
            }
            
            let html = '';
            let totalPrice = 0;
            let totalDuration = 0;
            
            this.bookingCart.forEach((item, index) => {
                const price    = this.getItemPrice(item);
                const duration = this._getItemVisitDuration(item);
                totalPrice    += price;
                totalDuration += duration;
                const dateParts = item.dateTime.date.split('-');
                const dateObj   = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]);
                const dateStr   = dateObj.toLocaleDateString(undefined, { month:'short', day:'numeric' });
                const title = this.bookingCart.length === 1 ? __('Appointment details') : `Appointment ${index + 1}`;

                // Build services rows with guest labels + read-only staff confirmation.
                const services = item.services || [{ service: item.service, options: item.options, staff: item.staff }];
                const hasPeople = services.some(s => s.guestLabel && s.guestLabel !== 'Me');
                let servicesHtml = '';
                const self2 = this;
                services.forEach((s, si) => {
                    const svcLabel = self2._svcDisplayLabelHtml(s.service, s.options);
                    const gLabel   = s.guestLabel || '';
                    const safeGLabel = self2._esc(self2._displayGuestLabel(gLabel));
                    const badge    = (hasPeople || gLabel) && gLabel
                        ? `<span style="display:inline-block;background:${gLabel === 'Me' ? '#dbeafe' : '#dcfce7'};color:${gLabel === 'Me' ? '#1d4ed8' : '#16a34a'};border-radius:4px;font-size:10px;font-weight:700;padding:1px 5px;margin-right:4px;">${safeGLabel}</span>`
                        : '';
                    const badgeSep = badge ? `<span style="color:#9ca3af;margin-right:4px;">-</span>` : '';
                    const staffName = self2._staffDisplayName(s.staff && s.staff.name);
                    servicesHtml += `<div style="margin-bottom:8px;font-size:13px;color:#374151;" data-cart-idx="${index}" data-svc-idx="${si}">
                        <div class="sbs-review-service-row">${badge}${badgeSep}<strong class="sbs-review-service-name">${svcLabel}</strong></div>
                        <div style="margin-top:3px;"><span class="sbs-review-staff-name">${self2._esc(staffName)}</span></div>
                    </div>`;
                });

                html += `
                    <div class="sbs-cart-item" style="border:1px solid #e5e7eb;border-radius:8px;padding:16px;margin-bottom:12px;background:#fff;">
                        <div style="display:flex;justify-content:space-between;align-items:start;margin-bottom:10px;">
                            <h4 style="margin:0;font-size:16px;font-weight:600;">${title}</h4>
                            <div style="display:flex;gap:8px;">
                                <button type="button" class="sbs-cart-edit" data-index="${index}" style="background:#e0e7ff;color:#4338ca;border:none;padding:4px 12px;border-radius:4px;cursor:pointer;font-size:12px;font-weight:500;">EDIT</button>
                                <button type="button" class="sbs-cart-remove" data-index="${index}" style="background:#fee;color:#dc2626;border:none;padding:4px 12px;border-radius:4px;cursor:pointer;font-size:12px;">REMOVE</button>
                            </div>
                        </div>
                        ${servicesHtml}
                        <p style="margin:8px 0 0;font-size:13px;color:#6b7280;line-height:1.45;">${this._formatItemTimeRange(item, dateStr)}<br><span>Appointment Length: ${duration} min</span></p>
                        <div style="font-size:18px;font-weight:700;color:#ec4899;margin-top:8px;">${ysFormatMoney(price)}</div>
                    </div>`;
            });
            
            html += `<div class="sbs-checkout-totals sbs-cart-summary-totals">
                <div class="sbs-checkout-total-primary"><strong>${__('Total')}</strong><span class="sbs-checkout-total-amount">${ysFormatMoney(this._taxParts(totalPrice).total)}</span></div>
                <div class="sbs-checkout-total-duration"><span>Appointment Length</span><strong>${totalDuration} min</strong></div>
            </div>`;
            
            container.html(html);
            container.off('change.cartStaff');

            this._applyConfirmBtn();
            this._updateMobileCartBar();
        }
        
        _updateMobileCartBar() {
            // -- Determine if we have anything worth showing ------------------
            const hasPending  = this.pendingServices.length > 0;
            const hasService  = !!this.selectedService;
            const hasCart     = this.bookingCart.length > 0;
            const hasAnything = hasPending || hasService || hasCart;

            // Hide bar completely when nothing is selected yet.
            if (!hasAnything) {
                $('#sbs-mobile-cart-bar').removeClass('sbs-bar-visible').css('display', 'none');
                this._clearCartBarSpacing();
                return;
            }

            // -- Calculate totals ---------------------------------------------
            let totalPrice = 0, totalDuration = 0, nameText = '', metaText = '';
            const publicStep = this._publicStepMeta(this.currentStep);
            const stepLabel = __('Step') + ' ' + publicStep.current + '/' + publicStep.total;

            if (hasCart) {
                // Completed cart items
                let serviceCount = 0;
                this.bookingCart.forEach(item => {
                    totalPrice    += this.getItemPrice(item);
                    totalDuration += this._getItemVisitDuration(item);
                    serviceCount  += (item.services && item.services.length) ? item.services.length : 1;
                });
                nameText = __('Selected services');
                metaText = __('Tap to view cart');

            } else if (hasPending) {
                // Multi-service in progress
                this.pendingServices.forEach(s => {
                    totalPrice += this._optionsPrice(s.service, s.options);
                });
                totalDuration = this._durationByGuest(this.pendingServices).totalDuration;
                const pendingCount = this.pendingServices.length;
                nameText = __('Selected services');
                const pendingParts = [];
                if (totalPrice > 0)    pendingParts.push(ysFormatMoney(this._taxParts(totalPrice).total));
                if (totalDuration > 0) pendingParts.push('Appointment Length: ' + totalDuration + ' min');
                pendingParts.push(stepLabel);
                metaText = __('Tap to view cart');

            } else if (hasService) {
                // Single service in progress
                const { duration, price } = this._calcDurationPrice();
                const svc = this.selectedService;
                nameText = __('Selected service');
                const parts = [];
                if (price > 0)    parts.push(ysFormatMoney(price));
                if (duration > 0) parts.push(duration + ' min');
                parts.push(stepLabel);
                metaText = __('Tap to view cart');
                if (this.currentStep === 2) {
                    const needsMain = !!this._optionHasMainGroup;
                    const hasMain = (this.selectedOptions || []).some(function(o) { return !o.is_addon; });
                    if (needsMain && !hasMain) {
                        nameText = __('Choose an option');
                        metaText = __('Add-ons are optional after the main choice');
                    }
                }
            }

            // -- Show "Review & Book" button if time is selected --------------
            const hasTime = !!this.selectedDateTime;
            const $nextBtn = $('#sbs-cart-next-btn');
            const $sidebarNextBtn = $('#sbs-sidebar-next-btn');
            $nextBtn.add($sidebarNextBtn)
                .attr('aria-disabled', 'false')
                .removeClass('is-disabled');
            const needsMainOption = this.currentStep === 2 && !!this._optionHasMainGroup;
            const hasMainOption = (this.selectedOptions || []).some(function(o) { return !o.is_addon; });
            const itemCount = hasCart
                ? this.bookingCart.reduce((count, item) => count + ((item.services && item.services.length) ? item.services.length : 1), 0)
                : (hasPending ? this.pendingServices.length : 1);
            if (itemCount > 0 && this.currentStep >= 2 && !(this.currentStep === 2 && nameText === 'Choose an option')) {
                nameText = itemCount + (itemCount === 1 ? ' service' : ' services');
                const barServices = hasCart
                    ? this.bookingCart.flatMap((item) => item.services && item.services.length ? item.services : [{ service: item.service, guestLabel: item.guestLabel || 'Me' }])
                    : (hasPending ? this.pendingServices : [{ service: this.selectedService, guestLabel: this._nextGuestLabel || 'Me' }]);
                const peopleCount = this._peopleCount(barServices);
                metaText = this.currentStep === 5
                    ? __('Review details')
                    : (peopleCount > 1 ? (peopleCount + ' people · Tap cart') : __('Tap cart'));
            }
            $('#sbs-cart-back-btn').toggle(this.currentStep > 1);
            $('#sbs-cart-expand-btn').toggle(this.currentStep !== 2 && this.currentStep !== 5);
            $('#sbs-mobile-cart-bar')
                .removeClass('sbs-unified-step-1 sbs-unified-step-2 sbs-unified-step-3 sbs-unified-step-4 sbs-unified-step-5 sbs-needs-choice')
                .addClass('sbs-unified-step-' + this.currentStep)
                .toggleClass('sbs-needs-choice', needsMainOption && !hasMainOption);
            if ((hasPending || hasService) && hasTime) {
                $nextBtn.prop('disabled', false).text(__('Review & Book')).show();
                $sidebarNextBtn.prop('disabled', false).text(__('Review & Book')).show();
            } else if (hasCart) {
                $nextBtn.prop('disabled', false).text(this.currentStep === 5 ? __('Book Now') : __('Checkout')).show();
                $sidebarNextBtn.prop('disabled', false).text(this.currentStep === 5 ? __('Book Now') : __('Checkout')).show();
            } else if (this.currentStep === 1 && hasPending) {
                $nextBtn.prop('disabled', false).text(__('Continue')).show();
                $sidebarNextBtn.prop('disabled', false).text(__('Continue')).show();
            } else if (this.currentStep === 2 && hasService) {
                $nextBtn
                    .prop('disabled', false)
                    .attr('aria-disabled', needsMainOption && !hasMainOption ? 'true' : 'false')
                    .toggleClass('is-disabled', needsMainOption && !hasMainOption)
                    .text(needsMainOption && !hasMainOption ? __('Continue') : __('Continue'))
                    .show();
                $sidebarNextBtn
                    .prop('disabled', false)
                    .attr('aria-disabled', needsMainOption && !hasMainOption ? 'true' : 'false')
                    .toggleClass('is-disabled', needsMainOption && !hasMainOption)
                    .text(needsMainOption && !hasMainOption ? __('Continue') : __('Continue'))
                    .show();
            } else if (this.currentStep === 3 && hasService) {
                $nextBtn
                    .prop('disabled', !this.selectedStaff)
                    .text(__('Continue'))
                    .show();
                $sidebarNextBtn
                    .prop('disabled', !this.selectedStaff)
                    .text(__('Continue'))
                    .show();
            } else if (this.currentStep === 3 && hasPending) {
                $nextBtn.prop('disabled', false).text(__('Choose Time')).show();
                $sidebarNextBtn.prop('disabled', false).text(__('Choose Time')).show();
            } else if (this.currentStep === 4 && (hasPending || hasService)) {
                $nextBtn.prop('disabled', true).text(__('Pick a Time')).show();
                $sidebarNextBtn.prop('disabled', true).text(__('Pick a Time')).show();
            } else {
                $nextBtn.prop('disabled', false).hide();
                if (this.currentStep !== 2) {
                    $sidebarNextBtn.prop('disabled', false).hide();
                }
            }

            const unifiedLabel = this.currentStep === 4
                ? (hasTime ? __('Review booking') : __('Pick a time'))
                : (this.currentStep === 5 || hasTime || hasCart ? __('Confirm Booking') : __('Continue'));
            if ($nextBtn.is(':visible')) $nextBtn.text(unifiedLabel);
            if ($sidebarNextBtn.is(':visible')) $sidebarNextBtn.text(unifiedLabel);

            $('#sbs-cart-name').text(nameText || __('Booking in progress'));
            $('#sbs-cart-meta').text(metaText);
            $('#sbs-cart-avatar').html('<span class="sbs-cart-icon" aria-hidden="true"></span><span class="sbs-cart-count">' + itemCount + '</span>');
            $('#sbs-mobile-cart-bar').css('display', this.currentStep === 2 ? 'grid' : 'flex').addClass('sbs-bar-visible');

            // Push the widget's own content up so the fixed bar never covers it.
            this._syncCartBarSpacing();
        }

        // FIX (mobile Chrome overscroll blank-space bug): keeps the widget
        // container's bottom padding in sync with the *actual* rendered height
        // of the fixed mobile cart bar via ResizeObserver, instead of guessing
        // with a fixed setTimeout(300). Scoped to .sbs-booking-container
        // (not <body>), so it never inflates the surrounding page/theme and
        // never leaves stray, oversized blank space when mobile Chrome's
        // address bar hides/shows mid-scroll.
        _syncCartBarSpacing() {
            const barEl = document.getElementById('sbs-mobile-cart-bar');
            const $container = $('.sbs-booking-container');
            if (!barEl || !$container.length) return;

            const apply = () => {
                const barH = barEl.offsetHeight || 70;
                // CSS floats the bar ~10px off the bottom edge on mobile;
                // include that gap so nothing is under- or over-reserved.
                const bottomGap = 10;
                $container.css('padding-bottom', (barH + bottomGap) + 'px');
                document.documentElement.style.setProperty('--sbs-mobile-bar-height', barH + 'px');
            };

            apply();

            if (typeof ResizeObserver !== 'undefined') {
                if (this._cartBarRO) this._cartBarRO.disconnect();
                this._cartBarRO = new ResizeObserver(apply);
                this._cartBarRO.observe(barEl);
            } else {
                // Fallback for very old browsers without ResizeObserver
                setTimeout(apply, 300);
            }
        }

        // Undo _syncCartBarSpacing(): stop observing and remove any reserved
        // space (also cleans up stale `body` padding left by the old code).
        _clearCartBarSpacing() {
            if (this._cartBarRO) { this._cartBarRO.disconnect(); this._cartBarRO = null; }
            $('.sbs-booking-container').css('padding-bottom', '');
            $('body').css('padding-bottom', '');
        }
        
        // Confirm before removing - avoids accidental deletion
        _confirmRemove(index, isMobile) {
            const self = this;
            // Build a simple inline confirmation overlay
            const $overlay = $(`
                <div id="sbs-confirm-overlay" style="
                    position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:99999;
                    display:flex;align-items:center;justify-content:center;padding:16px;">
                  <div style="background:#fff;border-radius:12px;padding:28px 24px;max-width:340px;width:100%;text-align:center;box-shadow:0 8px 32px rgba(0,0,0,.2);">
                    <div style="font-size:28px;margin-bottom:12px;">!</div>
                    <h3 style="margin:0 0 8px;font-size:18px;font-weight:700;">Remove this appointment?</h3>
                    <p style="margin:0 0 20px;color:#6b7280;font-size:14px;">This can't be undone. You'll need to re-add the service if you change your mind.</p>
                    <div style="display:flex;gap:12px;">
                      <button type="button" id="sbs-confirm-cancel" style="flex:1;padding:12px;background:#f3f4f6;color:#374151;border:none;border-radius:8px;cursor:pointer;font-size:14px;font-weight:600;">Keep it</button>
                      <button type="button" id="sbs-confirm-delete" style="flex:1;padding:12px;background:#dc2626;color:#fff;border:none;border-radius:8px;cursor:pointer;font-size:14px;font-weight:600;">Yes, remove</button>
                    </div>
                  </div>
                </div>`);
            $('body').append($overlay);
            $overlay.on('click', '#sbs-confirm-cancel', function() { $overlay.remove(); });
            $overlay.on('click', '#sbs-confirm-delete', function() {
                $overlay.remove();
                self.removeFromCart(index);
                if (isMobile) self._renderMobileCart();
            });
        }

        // FIX: removeFromCart - proper cleanup, no localStorage persistence
        removeFromCart(index) {
            this.bookingCart.splice(index, 1);
            this._saveSession();
            this.renderCart();              // updates step-5 Your Appointments list
            this._updateMobileCartBar();
            this.updateSummary();           // ALWAYS sync the right sidebar (all steps)

            // If the user is on step 4 (date/time picker), reload the time slots
            // so busy-staff indicators reflect the updated cart immediately.
            if (this.currentStep === 4) {
                // 4.6.2: removing a cart item frees its staff, so the visible
                // range's density changed too.
                this.loadAvailabilityDensity(true);
                const selDate = $('.sbs-day-button.selected').data('date')
                    || $('.sbs-day-button.active').data('date');
                if (selDate) {
                    this.loadAvailableTimes(selDate);
                }
            }

            if (this.currentStep === 5) {
                if (this.bookingCart.length === 0) {
                    this._closeDrawer();
                    this.selectedService = null;
                    this.selectedCategory = null;
                    this.selectedOptions = [];
                    this.selectedStaff = null;
                    this.selectedDateTime = null;
                    this.pendingServices = [];
                    this.goToStep(1, true);
                    return;
                }
                this.updateSummaryForCheckout(); // also refresh checkout sidebar view
                // Reset submit button in case it was left disabled from a failed attempt
                const $btn = $('.sbs-btn-submit');
                if ($btn.prop('disabled')) {
                    $btn.prop('disabled', false);
                    this._applyConfirmBtn();
                }
            }
        }
        
        editCartItem(index) {
            const item = this.bookingCart[index];
            if (!item) return;
            // Restore item to "current selection" and let user re-book
            this.bookingCart.splice(index, 1);
            this.selectedService  = { ...item.service };
            this.selectedCategory = item.service.category || null;
            this.selectedOptions  = [...item.options];
            this.selectedStaff    = item.staff;
            const itemDesign = item.services && item.services[0] ? item.services[0] : item;
            this.designUploadAttachmentId = parseInt(itemDesign.designAttachmentId || 0, 10) || 0;
            this.designUploadToken = String(itemDesign.designUploadToken || '');
            this.designUploadImageUrl = String(itemDesign.designImageUrl || '');
            this.designUploadFileName = String(itemDesign.designFileName || '');
            this.selectedDateTime = null;
            this.renderCart();
            this.updateSummary();
            this.goToStep(2);
            this.loadServiceOptions();
        }

        initWeekView() {
            const self = this;
            // Expose on instance so external code (e.g. _findAndGoNextAvailable) can jump weeks
            self._weekStart = ysSiteNow();
            self._weekStart.setHours(0, 0, 0, 0);
            // Keep local alias for the closure below
            const getWeekStart = () => self._weekStart;

            // -- Month View --------------------------------------------------
            self._monthDate = ysSiteNow();
            self._monthDate.setDate(1);
            self._monthDate.setHours(0, 0, 0, 0);

            function renderMonthView() {
                const today = ysSiteNow(); today.setHours(0,0,0,0);
                const maxDate = ysSiteNow(); maxDate.setDate(maxDate.getDate() + 60);
                const year  = self._monthDate.getFullYear();
                const month = self._monthDate.getMonth();
                const monthNames = [__('January'),__('February'),__('March'),__('April'),__('May'),__('June'),
                                    __('July'),__('August'),__('September'),__('October'),__('November'),__('December')];
                const dayNames = [_x('Sun','weekday abbreviation'),_x('Mon','weekday abbreviation'),_x('Tue','weekday abbreviation'),_x('Wed','weekday abbreviation'),_x('Thu','weekday abbreviation'),_x('Fri','weekday abbreviation'),_x('Sat','weekday abbreviation')];
                const firstDay = new Date(year, month, 1).getDay();
                const daysInMonth = new Date(year, month + 1, 0).getDate();

                let html = `<div class="sbs-month-header">
                    <button type="button" class="sbs-month-prev">&lt;</button>
                    <span class="sbs-month-title">${monthNames[month]} ${year}</span>
                    <button type="button" class="sbs-month-next">&gt;</button>
                </div>
                <div class="sbs-month-grid">`;

                // Day name headers
                dayNames.forEach(d => {
                    html += `<div class="sbs-month-dayname">${d}</div>`;
                });

                // Empty cells before first day
                for (let i = 0; i < firstDay; i++) {
                    html += `<div class="sbs-month-cell empty"></div>`;
                }

                // Day cells
                for (let d = 1; d <= daysInMonth; d++) {
                    const date   = new Date(year, month, d);
                    const isPast = date < today;
                    const isFuture = date > maxDate;
                    const isToday = date.getTime() === today.getTime();
                    const y = date.getFullYear();
                    const m = String(date.getMonth() + 1).padStart(2,'0');
                    const ds = String(d).padStart(2,'0');
                    const dateStr = `${y}-${m}-${ds}`;
                    const disabled = isPast || isFuture;
                    const availability = self._dateAvailability[dateStr];
                    const availabilityClass = typeof availability !== 'undefined'
                        ? 'sbs-availability-' + self._availabilityLevel(availability)
                        : '';
                    // 4.6.2: fullness classes and a real accessible name, both
                    // driven by the batch-fetched density. Rendering them here as
                    // well as in _applyDensityToCalendar means a view rebuilt
                    // after the fetch (month paging, week paging) comes up already
                    // correct instead of flashing blank until the next repaint.
                    const fullnessClass = self._dayFullnessClasses(availability).join(' ');
                    const ariaLabel = self._dateButtonAriaLabel(
                        dateStr,
                        typeof availability === 'undefined' ? null : availability
                    );
                    const classes = ['sbs-month-cell sbs-day-button',
                        disabled ? 'disabled' : '',
                        availabilityClass,
                        fullnessClass,
                        isToday  ? 'today' : ''].filter(Boolean).join(' ');
                    html += `<button type="button" class="${classes}" data-date="${dateStr}" aria-label="${self._esc(ariaLabel)}" ${typeof availability === 'undefined' ? '' : `data-availability-count="${availability}"`} ${disabled ? 'disabled' : ''}>
                        <span class="sbs-month-day-number" aria-hidden="true">${d}</span>
                        <span class="sbs-date-availability-meter" aria-hidden="true"><span></span></span>
                    </button>`;
                }

                html += `</div>`;
                $('.sbs-month-view').html(html);
            }

            // 4.6.2: every control that changes which dates are on screen now
            // asks for that range's density. loadAvailabilityDensity() keys on
            // the range + staff + services, so paging back to a month already
            // fetched costs nothing.
            $(document).on('click', '.sbs-month-prev', function() {
                self._monthDate.setMonth(self._monthDate.getMonth() - 1);
                renderMonthView();
                self.loadAvailabilityDensity();
            });
            $(document).on('click', '.sbs-month-next', function() {
                self._monthDate.setMonth(self._monthDate.getMonth() + 1);
                renderMonthView();
                self.loadAvailabilityDensity();
            });
            // ----------------------------------------------------------------

            function renderWeekView() {
                const currentWeekStart = self._weekStart;
                const weekStart = new Date(currentWeekStart);
                const weekEnd   = new Date(currentWeekStart);
                weekEnd.setDate(weekEnd.getDate() + 6);
                const rangeOpts = { month:'short', day:'numeric' };
                $('.sbs-week-range').text(
                    weekStart.toLocaleDateString(undefined, rangeOpts) + ' - ' +
                    weekEnd.toLocaleDateString(undefined, rangeOpts)
                );
                const today    = ysSiteNow(); today.setHours(0,0,0,0);
                const dayNames = [_x('Sun','weekday abbreviation'),_x('Mon','weekday abbreviation'),_x('Tue','weekday abbreviation'),_x('Wed','weekday abbreviation'),_x('Thu','weekday abbreviation'),_x('Fri','weekday abbreviation'),_x('Sat','weekday abbreviation')];
                let html = '';
                for (let i = 0; i < 7; i++) {
                    const date   = new Date(weekStart);
                    date.setDate(date.getDate() + i);
                    const isPast  = date < today;
                    const isToday = date.getTime() === today.getTime();
                    const y = date.getFullYear();
                    const m = String(date.getMonth() + 1).padStart(2,'0');
                    const d = String(date.getDate()).padStart(2,'0');
                    const dateStr = `${y}-${m}-${d}`;
                    const availability = self._dateAvailability[dateStr];
                    const availabilityClass = typeof availability !== 'undefined'
                        ? 'sbs-availability-' + self._availabilityLevel(availability)
                        : '';
                    // 4.6.2: see the month view above - same fullness classes and
                    // same accessible name. The three visible spans are marked
                    // aria-hidden so the button reads as one label ("September 5,
                    // 2026, 3 time slots available") instead of "Sep Mon 5".
                    const fullnessClass = self._dayFullnessClasses(availability).join(' ');
                    const ariaLabel = self._dateButtonAriaLabel(
                        dateStr,
                        typeof availability === 'undefined' ? null : availability
                    );
                    const classes = ['sbs-day-button', isPast ? 'disabled' : '', availabilityClass, fullnessClass, isToday ? 'today' : ''].filter(Boolean).join(' ');
                    html += `<button type="button" class="${classes}" data-date="${dateStr}" aria-label="${self._esc(ariaLabel)}" ${typeof availability === 'undefined' ? '' : `data-availability-count="${availability}"`} ${isPast ? 'disabled' : ''}>
                        <span class="sbs-day-month" aria-hidden="true">${date.toLocaleDateString(undefined, { month: 'short' })}</span>
                        <span class="sbs-day-name" aria-hidden="true">${dayNames[date.getDay()]}</span>
                        <span class="sbs-day-number" aria-hidden="true">${date.getDate()}</span>
                        <span class="sbs-date-availability-meter" aria-hidden="true"><span></span></span>
                    </button>`;
                }
                $('.sbs-week-days').html(html);
            }
            
            $(document).on('click', '.sbs-view-toggle', function() {
                const view = $(this).data('view');
                $('.sbs-view-toggle').removeClass('active');
                $(this).addClass('active');
                if (view === 'week') {
                    $('.sbs-week-view').addClass('active');
                    $('.sbs-month-view').removeClass('active');
                    renderWeekView();
                } else {
                    $('.sbs-week-view').removeClass('active');
                    $('.sbs-month-view').addClass('active');
                    renderMonthView();
                }
                // 4.6.2: week and month cover different ranges, so switching view
                // is a range change like any other.
                self.loadAvailabilityDensity();
            });
            $(document).on('click', '.sbs-week-prev', function() {
                self._weekStart.setDate(self._weekStart.getDate() - 7);
                renderWeekView();
                self.loadAvailabilityDensity();
            });
            $(document).on('click', '.sbs-week-next', function() {
                self._weekStart.setDate(self._weekStart.getDate() + 7);
                renderWeekView();
                self.loadAvailabilityDensity();
            });
            $(document).on('click', '.sbs-day-button', function() {
                if ($(this).hasClass('disabled')) return;
                $('.sbs-day-button').removeClass('selected');
                $(this).addClass('selected');
                const selectedDate = $(this).data('date');
                // 4.6.2: immediate, not debounced. Tapping a day is the single
                // most deliberate action on this step and the customer is looking
                // straight at the slot list; a 250ms hold would read as lag.
                // Rapid taps are already de-duplicated by the sequence number and
                // abort() inside _loadAvailableTimesNow().
                self.loadAvailableTimes(selectedDate, true);
            });
            // Expose render functions so external code can refresh views
            self._renderWeekView  = renderWeekView;
            self._renderMonthView = renderMonthView;
            renderWeekView();
        }

        reset() {
            this.currentStep    = 1;
            this.selectedService  = null;
            this.selectedCategory = null;
            this.selectedOptions  = [];
            this.selectedStaff    = null;
            this.selectedDateTime = null;
            this.bookingCart      = [];
            this._pendingAdd      = false;
            this.bookingConfirmed = false;
            this.designUploadAttachmentId = 0;
            this.designUploadToken = '';
            this.designUploadInProgress = false;
            this.designUploadImageUrl = '';
            this.designUploadFileName = '';
            
            $('#sbs-customer-form')[0].reset();
            $('#sbs-selected-service-id, #sbs-selected-staff-id, #sbs-selected-start-at, #sbs-selected-options').val('');
            this._closeDrawer();
            $('#sbs-mobile-cart-bar').removeClass('sbs-bar-visible').css('display', 'none');
            this._clearCartBarSpacing();
            $('#sbs-cart-name').text(__('Selected services'));
            $('#sbs-cart-meta').text(__('Tap to view cart'));
            $('.sbs-step-success').hide();
            this._clearSession(); // user is resetting - wipe saved state
            this.goToStep(1);
        }
        
        getNextDayName() {
            const tomorrow = ysSiteNow();
            tomorrow.setDate(tomorrow.getDate() + 1);
            return tomorrow.toLocaleDateString(undefined, { weekday:'long', month:'long', day:'numeric' });
        }
    }

    $(document).ready(function() {
        if ($('.sbs-booking-container').length) {
            window._sbsFormInstance = new SquareBookingForm();
            // Initial empty guide state - hide if cart/service already loaded from session
            setTimeout(function() {
                const inst = window._sbsFormInstance;
                if (inst && (inst.bookingCart.length > 0 || inst.selectedService || inst.pendingServices.length > 0)) {
                    $('#sbs-sidebar-empty-guide').hide();
                    $('#sbs-booking-summary-panel').show();
                }
            }, 100);

            // -- Load "next available opening" for each service card ----------
            const $openingPlaceholders = $('.ybs-next-opening');
            if ($openingPlaceholders.length) {
                const svcIds = [];
                $openingPlaceholders.each(function() {
                    const id = parseInt($(this).data('svc-id'), 10);
                    if (id && svcIds.indexOf(id) === -1) svcIds.push(id);
                });
                if (svcIds.length) {
                    $.post(yatoonData.ajaxUrl || yatoonData.ajax_url, {
                        action: 'yatoon_get_next_openings',
                        nonce:  yatoonData.nonce,
                        service_ids: svcIds
                    }).done(function(resp) {
                        if (!resp || !resp.success || !resp.data || !resp.data.openings) {
                            $openingPlaceholders.hide();
                            return;
                        }
                        const openings = resp.data.openings;
                        $openingPlaceholders.each(function() {
                            const $el = $(this);
                            const id  = parseInt($el.data('svc-id'), 10);
                            const op  = openings[id];
                            if (!op || !op.label) {
                                $el.hide();
                                return;
                            }
                            $el.find('.ybs-next-opening-text').text(op.label);
                            if (op.urgent) $el.addClass('is-urgent');
                            $el.addClass('is-loaded');
                        });
                    }).fail(function() {
                        $openingPlaceholders.hide();
                    });
                }
            }

            $(document).on('click', '.sbs-popular-pick[data-scroll-service]', function() {
                const serviceId = $(this).data('scroll-service');
                const $target = $('.sbs-service-item[data-service-id="' + serviceId + '"]').first();
                if (!$target.length) return;
                const $cat = $target.prevAll('.sbs-category-title').first();
                if ($cat.length) {
                    const catId = ($cat.attr('id') || '').replace(/^cat-/, '');
                    if (catId) {
                        $('.sbs-cat-tab, .sbs-cat-dropdown-item').removeClass('active');
                        $('.sbs-cat-tab[data-cat="' + catId + '"], .sbs-cat-dropdown-item[data-cat="' + catId + '"]').addClass('active');
                    }
                }
                $target[0].scrollIntoView({ behavior: 'smooth', block: 'center' });
                $target.addClass('sbs-attention-pulse');
                setTimeout(function() { $target.removeClass('sbs-attention-pulse'); }, 900);
            });

            $(document).on('click', '.sbs-date-shortcut', function() {
                const inst = window._sbsFormInstance;
                if (!inst) return;
                const offset = parseInt($(this).data('offset'), 10) || 0;
                const d = ysSiteNow();
                d.setDate(d.getDate() + offset);
                d.setHours(0, 0, 0, 0);
                const y = d.getFullYear();
                const m = String(d.getMonth() + 1).padStart(2, '0');
                const day = String(d.getDate()).padStart(2, '0');
                const dateStr = y + '-' + m + '-' + day;
                $('.sbs-view-toggle[data-view="week"]').trigger('click');
                if (inst._weekStart) {
                    const weekStart = new Date(d);
                    weekStart.setDate(d.getDate() - d.getDay());
                    inst._weekStart = weekStart;
                    if (inst._renderWeekView) inst._renderWeekView();
                }
                $('.sbs-day-button').removeClass('selected');
                const $day = $('.sbs-day-button[data-date="' + dateStr + '"]').first();
                if ($day.length && !$day.hasClass('disabled')) {
                    $day.addClass('selected');
                    inst.loadAvailableTimes(dateStr, true);
                } else {
                    inst.loadAvailableTimes(dateStr, true);
                }
                $('.sbs-date-shortcut').removeClass('active');
                $(this).addClass('active');
            });
        }

        // -- US Phone validation handlers (functions are defined at IIFE scope above) --

        // Validate on blur
        $(document).on('blur', '#sbs-customer-phone', function() {
            const val = $(this).val().trim();
            if (!val) { showPhoneError(''); return; }
            const phoneMode = (typeof yatoonData !== 'undefined' && yatoonData.phoneMode) ? yatoonData.phoneMode : 'us_only';
            if (phoneMode === 'us_only') {
                showPhoneError(validateUSPhone(val));
            }
        });

        // Clear error while typing
        $(document).on('input', '#sbs-customer-phone', function() {
            showPhoneError('');
            $(this).css('border-color', '');
        });

        // -- 4.6.2: live per-field validation for step 5 name + email ---------
        //
        // Until now the only checkout validation was form.checkValidity() /
        // reportValidity() at submit time, so a mistyped email was discovered
        // after the customer pressed Confirm -- a native bubble, on a form they
        // had already mentally finished. Validating on blur surfaces it while
        // they are still in that field.
        //
        // Deliberately mirrors the existing phone behaviour: an EMPTY field is
        // never flagged on blur. Flagging empties would fire "Please enter your
        // name" the instant the customer tabs past name toward email, which
        // reads as the form scolding them for something they were about to do.
        // Required-but-empty is still caught by the native check at submit.
        $(document).on('blur', '#sbs-customer-name', function() {
            const val = $(this).val().trim();
            if (!val) { showFieldError('sbs-customer-name', ''); return; }
            showFieldError('sbs-customer-name', validateCustomerName(val));
        });
        $(document).on('input', '#sbs-customer-name', function() {
            showFieldError('sbs-customer-name', '');
        });

        $(document).on('blur', '#sbs-customer-email', function() {
            const val = $(this).val().trim();
            if (!val) { showFieldError('sbs-customer-email', ''); return; }
            showFieldError('sbs-customer-email', validateCustomerEmail(val));
        });
        $(document).on('input', '#sbs-customer-email', function() {
            showFieldError('sbs-customer-email', '');
        });

        // -- Gift card apply button -------------------------------------------
        $(document).on('click', '#sbs-gift-card-apply', function() {
            const code = $('#sbs-gift-card-code').val().trim().toUpperCase();
            if (!code) return;
            const $btn = $(this).prop('disabled', true).text(__('Checking...'));
            const $result = $('#sbs-gift-card-result');
            $.post(ajaxurl, {
                action: 'yatoon_validate_gift_card',
                nonce:  yatoonData.nonce,
                code,
            }, function(resp) {
                $btn.prop('disabled', false).text('Apply');
                $result.show();
                if (resp.success) {
                    $result.removeClass('error').addClass('success')
                        .text(resp.data.message);
                    $('#sbs-gift-card-code-confirmed').val(code);
                    $('#sbs-gift-card-code').prop('readonly', true);
                    $btn.text(__('Applied')).prop('disabled', true);
                } else {
                    $result.removeClass('success').addClass('error')
                        .text(resp.data?.message || __('Invalid gift card.'));
                    $('#sbs-gift-card-code-confirmed').val('');
                }
            }).fail(function() {
                $btn.prop('disabled', false).text('Apply');
                $result.show().removeClass('success').addClass('error').text(__('Network error. Please try again.'));
            });
        });

        // Auto-uppercase gift card input
        $(document).on('input', '#sbs-gift-card-code', function() {
            this.value = this.value.toUpperCase();
        });

        // -- Coupon code Apply button ------------------------------------------
        $(document).on('click', '#sbs-coupon-apply', function() {
            const code = $('#sbs-coupon-code').val().trim().toUpperCase();
            if (!code) return;
            const $btn = $(this).prop('disabled', true).text(__('Checking...'));
            const $result = $('#sbs-coupon-result');
            const inst = window._sbsFormInstance;
            const total = inst ? (inst.cartTotal || 0) : 0;
            $.post(ajaxurl, {
                action: 'yatoon_validate_coupon',
                nonce:  yatoonData.nonce,
                coupon_code: code,
                total:  total,
            }, function(resp) {
                $btn.prop('disabled', false).text('Apply');
                $result.show();
                if (resp.success) {
                    $result.removeClass('error').addClass('success')
                        .text(resp.data.message);
                    $('#sbs-coupon-code').prop('readonly', true);
                    $('#sbs-coupon-discount').val(resp.data.discount || 0);
                    $btn.text(__('Applied')).prop('disabled', true);
                } else {
                    $result.removeClass('success').addClass('error')
                        .text(resp.data?.message || __('Invalid coupon code.'));
                    $('#sbs-coupon-discount').val(0);
                }
            }).fail(function() {
                $btn.prop('disabled', false).text('Apply');
                $result.show().removeClass('success').addClass('error').text(__('Network error. Please try again.'));
            });
        });

        $(document).on('input', '#sbs-coupon-code', function() {
            this.value = this.value.toUpperCase();
        });

        // -- Tip preset buttons -----------------------------------------------
        $(document).on('click', '.sbs-tip-btn', function() {
            $('.sbs-tip-btn').removeClass('active');
            $(this).addClass('active');
            const pct = $(this).data('pct');
            if (pct === 'custom') {
                $('#sbs-tip-custom-wrap').show();
                $('#sbs-tip-amount').val($('#sbs-tip-custom-amount').val() || 0);
                $('#sbs-tip-display').hide();
            } else if (pct === 0 || pct === '0') {
                $('#sbs-tip-custom-wrap').hide();
                $('#sbs-tip-amount').val(0);
                $('#sbs-tip-display').hide();
            } else {
                $('#sbs-tip-custom-wrap').hide();
                // Calculate tip from current cart total
                const inst = window._sbsFormInstance;
                let subtotal = 0;
                if (inst) {
                    (inst.bookingCart || []).forEach(function(item) { subtotal += inst.getItemPrice(item); });
                    if (!subtotal && inst.selectedService) subtotal = inst.selectedService.price || 0;
                }
                const tip = Math.round(subtotal * parseInt(pct) / 100 * 100) / 100;
                $('#sbs-tip-amount').val(tip);
                $('#sbs-tip-display').text(__('Tip:') + ' ' + ysFormatMoney(tip)).show();
            }
            const inst = window._sbsFormInstance;
            if (inst && inst.currentStep === 5) inst.updateSummaryForCheckout();
        });

        $(document).on('input', '#sbs-tip-custom-amount', function() {
            const val = parseFloat($(this).val()) || 0;
            $('#sbs-tip-amount').val(val);
            if (val > 0) {
                $('#sbs-tip-display').text(__('Tip:') + ' ' + ysFormatMoney(val)).show();
            } else {
                $('#sbs-tip-display').hide();
            }
            const inst = window._sbsFormInstance;
            if (inst && inst.currentStep === 5) inst.updateSummaryForCheckout();
        });
        
        // "Go to next available" button - auto-triggered or manual click
        $(document).on('click', '.sbs-try-next-date', function(e) {
            e.preventDefault();
            const self = window._sbsFormInstance;
            if (self) self._findAndGoNextAvailable($(this));
        });
        
        // "Try another staff member" button - go back to step 3, clear staff + datetime
        $(document).on('click', '.sbs-try-other-staff', function(e) {
            e.preventDefault();
            const self = window._sbsFormInstance;
            if (!self) return;
            self.selectedStaff    = null;
            self.selectedDateTime = null;
            self.updateSummary();
            self.goToStep(3);
            self.loadAvailableStaff();
        });


        $(document).on('click', '.sbs-availability-choose-staff', function(e) {
            e.preventDefault();
            const self = window._sbsFormInstance;
            if (!self) return;
            self.selectedStaff = null;
            self.selectedDateTime = null;
            self.updateSummary();
            self.goToStep(3);
            self.loadAvailableStaff();
        });

        $(document).on('click', '.sbs-text-us-inline', function(e) {
            e.preventDefault();
            if ($('#sbs-text-us-btn').length) {
                $('#sbs-text-us-btn').trigger('click');
            }
        });
        // Waitlist button
        $(document).on('click', '.sbs-join-waitlist', function() {
            $('#sbs-waitlist-form')[0].reset();
            const currentServiceId = $('#sbs-selected-service-id').val();
            if (currentServiceId) {
                $('#waitlist-service-checkboxes input[value="' + currentServiceId + '"]').prop('checked', true)
                    .closest('.waitlist-service-label').css({ 'border-color':'var(--primary-color,#ec4899)', 'background':'#fdf2f8' });
            }
            const currentDate = $('.sbs-day-button.selected').data('date') || '';
            if (currentDate) $('#waitlist-date').val(currentDate);
            $('#sbs-waitlist-form').show();
            $('#sbs-waitlist-success').hide();
            $('#sbs-waitlist-modal').fadeIn(200);
        });
        
        $(document).on('change', '.waitlist-svc-chk', function() {
            const $label = $(this).closest('.waitlist-service-label');
            if ($(this).is(':checked')) {
                $label.css({ 'border-color':'var(--primary-color,#ec4899)', 'background':'#fdf2f8' });
            } else {
                $label.css({ 'border-color':'#e5e7eb', 'background':'' });
            }
        });
        $(document).on('change', '.waitlist-time-label input[type="radio"]', function() {
            $('.waitlist-time-label').css({ 'border-color':'#e5e7eb', 'background':'' });
            $(this).closest('.waitlist-time-label').css({ 'border-color':'var(--primary-color,#ec4899)', 'background':'#fdf2f8' });
        });
        $(document).on('click', '#sbs-waitlist-modal .sbs-text-modal-close, #sbs-waitlist-modal', function(e) {
            if (e.target === this) $('#sbs-waitlist-modal').fadeOut(200);
        });
        $(document).on('submit', '#sbs-waitlist-form', function(e) {
            e.preventDefault();
            const checkedServices = $('.waitlist-svc-chk:checked');
            if (checkedServices.length === 0) {
                $('#waitlist-service-checkboxes').css('border','1.5px solid #ef4444');
                setTimeout(function(){ $('#waitlist-service-checkboxes').css('border',''); }, 2000);
                if (window._sbsFormInstance) window._sbsFormInstance._showToast(__('Please select at least one service.'), 'error');
                return;
            }
            const $btn = $(this).find('button[type="submit"]');
            const origText = $btn.text();
            $btn.prop('disabled', true).text(__('Submitting...'));
            $.ajax({
                url:  yatoonData.ajax_url,
                type: 'POST',
                data: $(this).serialize() + '&action=yatoon_join_waitlist&nonce=' + yatoonData.nonce,
                success: function(response) {
                    $btn.prop('disabled', false).text(origText);
                    if (response.success) {
                        $('#sbs-waitlist-form').hide();
                        $('#sbs-waitlist-success').fadeIn(200);
                        setTimeout(function() { $('#sbs-waitlist-modal').fadeOut(200); }, 3500);
                    } else {
                        if (window._sbsFormInstance) window._sbsFormInstance._showToast(response.data.message || __('Failed to join waitlist. Please try again.'), 'error');
                    }
                },
                error: function() {
                    $btn.prop('disabled', false).text(origText);
                    if (window._sbsFormInstance) window._sbsFormInstance._showToast(__('Network error. Please try again.'), 'error');
                }
            });
        });
    });


    /* -- 2.3.0 Appointment type selector --------------------------- */
    /* Hide Group card if max guests = 0 */
    $(document).ready(function () {
        // Use yatoonData.maxGuestsTogether (set via wp_localize_script) as the
        // authoritative value - avoids get_option cache issues on some hosts.
        var maxG = (typeof yatoonData !== 'undefined' && yatoonData.maxGuestsTogether)
            ? parseInt(yatoonData.maxGuestsTogether, 10)
            : parseInt($('#sbs-appt-type-wrap').data('max-guests') || 0, 10);

        if (maxG <= 0) {
            $('#sbs-appt-group').hide();
            $('#sbs-appt-type-wrap').hide();
        } else {
            // Overwrite the subtitle with the correct JS-sourced value
            var guestWord = maxG === 1
                ? '1 guest'
                : 'up to ' + maxG + ' guests';
            $('#sbs-appt-group-sub').text('Add ' + guestWord + '; everyone starts together');
            // Also update the data attribute so the rest of the JS stays consistent
            $('#sbs-appt-type-wrap').data('max-guests', maxG);
        }
    });

    $(document).on('click', '.sbs-appt-type-card', function () {
        var type = $(this).data('type');
        $('.sbs-appt-type-card').removeClass('active');
        $(this).addClass('active');

        var $hint = $('#sbs-appt-group-hint');
        if (type === 'group') {
            $hint.slideDown(180);
            // Store intent so the first ADD triggers _addGuestTogether after adding own service
            window._sbsGroupIntent = true;
        } else {
            $hint.slideUp(180);
            window._sbsGroupIntent = false;
        }
        if (window._sbsFormInstance) window._sbsFormInstance._saveSession();
    });

    /* Group intent is now handled inline inside the time-slot click handler (addToCart path).
       No separate listener needed here - the old .sbs-book-btn listener was firing too early
       (before the user had finished selecting staff/time for themselves), causing a reset. */

})(jQuery);

})();
