{"id":1020,"date":"2026-05-14T16:27:03","date_gmt":"2026-05-14T06:27:03","guid":{"rendered":"https:\/\/airliebeachpartybus.com.au\/?page_id=1020"},"modified":"2026-05-28T12:38:30","modified_gmt":"2026-05-28T02:38:30","slug":"need-a-ride","status":"publish","type":"page","link":"https:\/\/airliebeachpartybus.com.au\/?page_id=1020","title":{"rendered":"Need a Tour"},"content":{"rendered":"    <div class=\"tb-multistep-wrap\" id=\"tb-multistep-form\" data-tour-id=\"0\">\n\n        <!-- Progress bar -->\n        <div class=\"tb-progress\">\n            <div class=\"tb-progress-step tb-step-active\" data-step=\"1\"><span>1<\/span> Date<\/div>\n            <div class=\"tb-progress-step\" data-step=\"2\"><span>2<\/span> Seats<\/div>\n            <div class=\"tb-progress-step\" data-step=\"3\"><span>3<\/span> Stops<\/div>\n            <div class=\"tb-progress-step\" data-step=\"4\"><span>4<\/span> Details<\/div>\n            <div class=\"tb-progress-step\" data-step=\"5\"><span>5<\/span> Done<\/div>\n        <\/div>\n\n        <!-- STEP 1: Select tour (if no id pinned) + Select schedule -->\n        <div class=\"tb-step-panel\" id=\"tb-step-1\">\n            <h3 class=\"tb-step-heading\">\ud83d\udcc5 Choose Your Departure Date<\/h3>\n\n                        <div class=\"tb-form-group\">\n                <label class=\"tb-label\">Tour <span class=\"tb-required\">*<\/span><\/label>\n                <select id=\"tb-ms-tour-select\" class=\"tb-select\">\n                    <option value=\"\">\u2014 Select a tour \u2014<\/option>\n                                        <option value=\"3\">\n                        Choose Your Own - Test tour (30 Nov -0001)                    <\/option>\n                                        <option value=\"4\">\n                        Shute Harbour to Proserpine (30 Nov -0001)                    <\/option>\n                                        <option value=\"6\">\n                        Cape Gloucester (30 Nov -0001)                    <\/option>\n                                        <option value=\"7\">\n                        Townsville Weekender (30 Nov -0001)                    <\/option>\n                                        <option value=\"5\">\n                        The Pioneer Valley Pub Tour (01 Jun 2026)                    <\/option>\n                                        <option value=\"1\">\n                        Choose Your Own Adventure Pub Crawl (07 Jun 2026)                    <\/option>\n                                    <\/select>\n            <\/div>\n            \n            <div id=\"tb-schedules-loading\" style=\"display:none\" class=\"tb-loading\">Loading schedules\u2026<\/div>\n            <div id=\"tb-schedules-list\" class=\"tb-schedules-grid\"><\/div>\n            <div id=\"tb-flexible-picker\" style=\"display:none\" class=\"tb-flexible-picker-wrap\">\n                <div class=\"tb-schedule-card tb-flexible-card\" id=\"tb-flexible-card\">\n                    <div class=\"tb-sched-date\">\ud83d\udcc5 Choose your own date<\/div>\n                    <div class=\"tb-sched-time\">Pick any date &amp; time that suits you<\/div>\n                    <div class=\"tb-flexible-input-wrap\" id=\"tb-flexible-input-wrap\" style=\"display:none;margin-top:10px;\">\n                        <input type=\"datetime-local\" id=\"tb-flexible-date\" class=\"tb-input\"\n                               min=\"2026-09-22T13:17\"\n                               style=\"width:100%;max-width:280px;\">\n                        <p class=\"description\" style=\"margin-top:4px;font-size:12px;\">Please choose a future date &amp; time.<\/p>\n                    <\/div>\n                <\/div>\n            <\/div>\n            <div id=\"tb-no-schedules\" style=\"display:none\" class=\"tb-info-box\">No upcoming departures available for this tour. <a href=\"mailto:leon_storey@hotmail.com\">Contact us<\/a> to arrange a custom date.<\/div>\n\n            <div class=\"tb-step-actions\">\n                <button type=\"button\" id=\"tb-step1-next\" class=\"tb-btn-primary\" disabled>Next: Choose Seats \u2192<\/button>\n            <\/div>\n        <\/div>\n\n        <!-- STEP 2: Seat class + passenger counts -->\n        <div class=\"tb-step-panel\" id=\"tb-step-2\" style=\"display:none\">\n            <h3 class=\"tb-step-heading\">\ud83e\ude91 Choose Your Seats<\/h3>\n\n            <div id=\"tb-seat-classes-list\" class=\"tb-seat-classes-grid\"><\/div>\n\n            <div class=\"tb-passenger-counts\" id=\"tb-passenger-counts\">\n                <h4 class=\"tb-subheading\">Passengers<\/h4>\n                <div class=\"tb-passenger-row\">\n                    <div>\n                        <strong>Adults<\/strong>\n                        <small>Age 18+<\/small>\n                    <\/div>\n                    <div class=\"tb-counter\">\n                        <button type=\"button\" class=\"tb-counter-btn\" data-type=\"adults\" data-dir=\"-1\">\u2212<\/button>\n                        <span class=\"tb-counter-val\" id=\"tb-count-adults\">1<\/span>\n                        <button type=\"button\" class=\"tb-counter-btn\" data-type=\"adults\" data-dir=\"1\">+<\/button>\n                    <\/div>\n                <\/div>\n                <div class=\"tb-passenger-row\">\n                    <div>\n                        <strong>Children<\/strong>\n                        <small>Age 3\u201317<\/small>\n                    <\/div>\n                    <div class=\"tb-counter\">\n                        <button type=\"button\" class=\"tb-counter-btn\" data-type=\"children\" data-dir=\"-1\">\u2212<\/button>\n                        <span class=\"tb-counter-val\" id=\"tb-count-children\">0<\/span>\n                        <button type=\"button\" class=\"tb-counter-btn\" data-type=\"children\" data-dir=\"1\">+<\/button>\n                    <\/div>\n                <\/div>\n                <div class=\"tb-passenger-row\">\n                    <div>\n                        <strong>Seniors<\/strong>\n                        <small>Age 65+<\/small>\n                    <\/div>\n                    <div class=\"tb-counter\">\n                        <button type=\"button\" class=\"tb-counter-btn\" data-type=\"seniors\" data-dir=\"-1\">\u2212<\/button>\n                        <span class=\"tb-counter-val\" id=\"tb-count-seniors\">0<\/span>\n                        <button type=\"button\" class=\"tb-counter-btn\" data-type=\"seniors\" data-dir=\"1\">+<\/button>\n                    <\/div>\n                <\/div>\n            <\/div>\n\n            <!-- Child Seat add-on (physical booster\/car seat, 0-5yo) -->\n            <div class=\"tb-child-seats-addon\">\n                <div class=\"tb-child-seats-header\">\n                    <div class=\"tb-child-seats-info\">\n                        <strong>\ud83d\udc76 Child Seat Add-on<\/strong>\n                        <span class=\"tb-child-fee-badge\">+A$20 per seat<\/span>\n                        <small>Age 0\u20135 &nbsp;\u00b7&nbsp; Max 4 seats<\/small>\n                    <\/div>\n                    <div class=\"tb-counter\">\n                        <button type=\"button\" class=\"tb-counter-btn\" data-type=\"childSeats\" data-dir=\"-1\">\u2212<\/button>\n                        <span class=\"tb-counter-val\" id=\"tb-count-childSeats\">0<\/span>\n                        <button type=\"button\" class=\"tb-counter-btn\" data-type=\"childSeats\" data-dir=\"1\" data-max=\"4\">+<\/button>\n                    <\/div>\n                <\/div>\n                <p class=\"tb-child-seats-note\">Physical booster or car seat for infants and toddlers. Does not count as a passenger seat.<\/p>\n            <\/div>\n\n            <div class=\"tb-price-preview\" id=\"tb-price-preview\" style=\"display:none\">\n                <div class=\"tb-price-preview-row\"><span id=\"tb-rate-span\">Rate<\/span><strong id=\"tb-unit-price-label\">\u2014<\/strong><\/div>\n                <div class=\"tb-price-preview-row\"><span>Travel time<\/span><strong id=\"tb-passengers-total-label\">\u2014<\/strong><\/div>\n                <div class=\"tb-price-preview-row\" id=\"tb-child-fee-row\" style=\"display:none\"><span id=\"tb-child-fee-label\">Child seats<\/span><strong id=\"tb-child-fee-total\">\u2014<\/strong><\/div>\n                <div class=\"tb-price-preview-row tb-price-total-row\"><span>Total<\/span><strong id=\"tb-total-price-label\">\u2014<\/strong><\/div>\n            <\/div>\n\n            <div class=\"tb-step-actions\">\n                <button type=\"button\" id=\"tb-step2-back\" class=\"tb-btn-secondary\">\u2190 Back<\/button>\n                <button type=\"button\" id=\"tb-step2-next\" class=\"tb-btn-primary\" disabled>Next: Choose Stops \u2192<\/button>\n            <\/div>\n        <\/div>\n\n        <!-- STEP 3: Stop selection -->\n        <div class=\"tb-step-panel\" id=\"tb-step-3\" style=\"display:none\">\n            <h3 class=\"tb-step-heading\">\ud83d\udccd Choose Your Stops<\/h3>\n\n            <!-- Custom pickup \/ drop-off -->\n            <div class=\"tb-location-row\">\n                <div class=\"tb-location-group\">\n                    <label class=\"tb-label\">\ud83d\udfe2 Pickup \/ Start location<\/label>\n                    <div class=\"tb-location-search-wrap\" id=\"tb-start-wrap\">\n                        <input type=\"text\" id=\"tb-start-input\" class=\"tb-input tb-location-input\"\n                               placeholder=\"Use tour start point or search an address\u2026\" autocomplete=\"off\">\n                        <ul id=\"tb-start-dropdown\" class=\"tb-location-dropdown\" style=\"display:none\"><\/ul>\n                    <\/div>\n                    <p class=\"tb-field-hint\" id=\"tb-start-hint\"><\/p>\n                <\/div>\n                <div class=\"tb-location-group\">\n                    <label class=\"tb-label\">\ud83d\udd34 Drop-off \/ End location<\/label>\n                    <div class=\"tb-location-search-wrap\" id=\"tb-end-wrap\">\n                        <input type=\"text\" id=\"tb-end-input\" class=\"tb-input tb-location-input\"\n                               placeholder=\"Use tour end point or search an address\u2026\" autocomplete=\"off\">\n                        <ul id=\"tb-end-dropdown\" class=\"tb-location-dropdown\" style=\"display:none\"><\/ul>\n                    <\/div>\n                    <p class=\"tb-field-hint\" id=\"tb-end-hint\"><\/p>\n                <\/div>\n            <\/div>\n\n            <p class=\"tb-stops-intro\">Select the stops you'd like to visit and set how long you'd like to spend at each one.<\/p>\n\n            <!-- Stop selection block (shown above the map) -->\n            <div id=\"tb-stops-step-list\" class=\"tb-stops-step-grid\"><\/div>\n            <div id=\"tb-stops-step-loading\" style=\"display:none\" class=\"tb-loading\">Loading stops\u2026<\/div>\n            <div id=\"tb-stops-step-none\" style=\"display:none\" class=\"tb-info-box\">No stops defined for this tour \u2014 all included automatically.<\/div>\n\n            <!-- Waypoints list -->\n            <div id=\"tb-route-legs\" class=\"tb-route-legs\" style=\"display:none\"><\/div>\n\n            <!-- Route map -->\n            <div class=\"tb-route-map-wrap\">\n                <div id=\"tb-booking-route-map\" class=\"tb-route-map\"><\/div>\n            <\/div>\n            <div id=\"tb-route-stats\" class=\"tb-route-stats\" style=\"display:none\"><\/div>\n\n            <!-- Choose Your Own Adventure Route Builder (shown only when allow_custom_route) -->\n            <div id=\"tb-custom-route-section\" style=\"display:none\">\n                <div class=\"tb-custom-route-header\">\n                    <h4 class=\"tb-custom-route-title\">\ud83d\uddfa\ufe0f Build Your Own Adventure Route<\/h4>\n                    <p class=\"tb-custom-route-desc\">Add up to 6 custom waypoints to personalise your route. Set how long you'd like to stay at each stop.<\/p>\n                <\/div>\n                <div id=\"tb-custom-wp-list\"><\/div>\n                <button type=\"button\" id=\"tb-add-custom-wp\" class=\"tb-btn-add-wp\">\uff0b Add a Stop<\/button>\n                <p id=\"tb-custom-wp-max-msg\" style=\"display:none\" class=\"tb-field-hint\">Maximum of 6 custom stops reached.<\/p>\n            <\/div>\n\n            <div class=\"tb-step-actions\">\n                <button type=\"button\" id=\"tb-step3-back\" class=\"tb-btn-secondary\">\u2190 Back<\/button>\n                <button type=\"button\" id=\"tb-step3-next\" class=\"tb-btn-primary\">Next: Your Details \u2192<\/button>\n            <\/div>\n        <\/div>\n\n        <!-- STEP 4: Contact details + payment -->\n        <div class=\"tb-step-panel\" id=\"tb-step-4\" style=\"display:none\">\n            <h3 class=\"tb-step-heading\">\ud83d\udc64 Your Details<\/h3>\n\n            <div class=\"tb-booking-summary-bar\" id=\"tb-summary-bar\"><\/div>\n\n            <div class=\"tb-form-group\">\n                <label class=\"tb-label\">Full Name <span class=\"tb-required\">*<\/span><\/label>\n                <input type=\"text\" id=\"tb-ms-name\" class=\"tb-input\" autocomplete=\"name\" required>\n            <\/div>\n            <div class=\"tb-form-group\">\n                <label class=\"tb-label\">Email <span class=\"tb-required\">*<\/span><\/label>\n                <input type=\"email\" id=\"tb-ms-email\" class=\"tb-input\" autocomplete=\"email\" required>\n            <\/div>\n            <div class=\"tb-form-group\">\n                <label class=\"tb-label\">Phone <span class=\"tb-optional\">(optional)<\/span><\/label>\n                <input type=\"tel\" id=\"tb-ms-phone\" class=\"tb-input\" autocomplete=\"tel\">\n            <\/div>\n\n            <div class=\"tb-payment-block\">\n                <h4>\ud83d\udcb3 Payment<\/h4>\n                <p class=\"description\">Enter your Square payment nonce.<\/p>\n                <div class=\"tb-form-group\">\n                    <label class=\"tb-label\">Payment Nonce <span class=\"tb-required\">*<\/span><\/label>\n                    <input type=\"text\" id=\"tb-ms-nonce\" class=\"tb-input\" placeholder=\"Square payment source ID\" required>\n                <\/div>\n            <\/div>\n\n            <div class=\"tb-step-actions\">\n                <button type=\"button\" id=\"tb-step4-back\" class=\"tb-btn-secondary\">\u2190 Back<\/button>\n                <button type=\"button\" id=\"tb-step3-confirm\" class=\"tb-btn-primary tb-btn-full\">Confirm &amp; Pay \ud83d\udd12<\/button>\n            <\/div>\n            <div id=\"tb-ms-result\"><\/div>\n        <\/div>\n\n        <!-- STEP 5: Confirmation -->\n        <div class=\"tb-step-panel\" id=\"tb-step-5\" style=\"display:none\">\n            <div class=\"tb-confirmed-icon\">\ud83c\udf89<\/div>\n            <h3 class=\"tb-step-heading\">Booking Confirmed!<\/h3>\n            <div id=\"tb-confirmed-detail\" class=\"tb-confirmed-detail\"><\/div>\n            <p class=\"tb-confirmed-email-note\">A confirmation has been sent to your email address.<\/p>\n            <button type=\"button\" id=\"tb-book-another\" class=\"tb-btn-secondary\" style=\"margin-top:16px\">Book Another Tour<\/button>\n        <\/div>\n\n    <\/div><!-- .tb-multistep-wrap -->\n\n    <script>\n    (function () {\n        var STATE = {\n            tourId:      null,\n            scheduleId:  null,\n            scheduleLabel: '',\n            seatClassId: null,\n            seatClassName: '',\n            unitPriceCents:     0,\n            driveSecs:          0,\n            firstBookingPriceCents: 0, \/\/ if >0, schedule already has a booking \u2192 remaining seats sold at this\/20 each\n            childSeatFeeCents: 2000,   \/\/ $20 per physical child seat (0-5yo add-on)\n            adults:      1,\n            children:    0,   \/\/ child fares (age 3-17), count toward seat total\n            seniors:     0,\n            childSeats:  0,   \/\/ physical child seat add-on (age 0-5), $20 each\n            preferredDate: '',\n            currencySymbol: \"A$\",\n            restBase: {\"schedules\":\"https:\\\/\\\/airliebeachpartybus.com.au\\\/index.php?rest_route=\\\/tour-bookings\\\/v1\\\/tour\\\/schedules\",\"seatClasses\":\"https:\\\/\\\/airliebeachpartybus.com.au\\\/index.php?rest_route=\\\/tour-bookings\\\/v1\\\/tour\\\/seat-classes\",\"bookV2\":\"https:\\\/\\\/airliebeachpartybus.com.au\\\/index.php?rest_route=\\\/tour-bookings\\\/v1\\\/tour\\\/book-v2\",\"route\":\"https:\\\/\\\/airliebeachpartybus.com.au\\\/index.php?rest_route=\\\/tour-bookings\\\/v1\\\/tour\\\/route\",\"nonce\":\"05a7c50c5b\"},\n            selectedStops:  [],\n            tourWaypoints:  [],\n            customStart:    null,\n            customEnd:      null,\n            customRoute:    [],          \/\/ user-built waypoints [{label,lat,lng,dwell_min}]\n            allowCustomRoute: false,     \/\/ set from REST response\n        };\n\n        \/\/ \u2500\u2500 Expose init function for [tour_list] cross-page \"Book now\" \u2500\u2500\u2500\u2500\n        window.tbInitBookingForm = function (tourId) {\n            STATE.tourId = parseInt(tourId, 10);\n            showStep(1);\n            loadSchedules();\n        };\n\n        \/\/ \u2500\u2500 navigation Step \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        function showStep(n) {\n            document.querySelectorAll('.tb-step-panel').forEach(function (p) { p.style.display = 'none'; });\n            var panel = document.getElementById('tb-step-' + n);\n            if (panel) panel.style.display = '';\n            document.querySelectorAll('.tb-progress-step').forEach(function (s) {\n                var sn = parseInt(s.getAttribute('data-step'), 10);\n                s.classList.toggle('tb-step-active',    sn === n);\n                s.classList.toggle('tb-step-completed', sn < n);\n            });\n            \/\/ When step 3 becomes visible, init map (150ms to ensure container is painted)\n            if (n === 3) {\n                setTimeout(function () {\n                    _initBookingMap(function () {\n                        if (_bookingMapWps && _bookingMapWps.length) { _doDrawBookingMap(); }\n                    });\n                }, 150);\n            }\n        }\n\n        \/\/ \u2500\u2500 Step 1: Schedules \u2500\u2500\u2500\n        function loadSchedules() {\n            if (!STATE.tourId) return;\n            var list = document.getElementById('tb-schedules-list');\n            var noSched = document.getElementById('tb-no-schedules');\n            var loading = document.getElementById('tb-schedules-loading');\n            list.innerHTML = '';\n            noSched.style.display = 'none';\n            loading.style.display = '';\n            document.getElementById('tb-step1-next').disabled = true;\n            STATE.scheduleId = null;\n\n            var schedSep = STATE.restBase.schedules.indexOf('?') !== -1 ? '&' : '?';\n            fetch(STATE.restBase.schedules + schedSep + 'tour_id=' + STATE.tourId, { credentials: 'omit' })\n            .then(function (r) {\n                if (!r.ok) throw new Error('HTTP ' + r.status);\n                return r.json();\n            }).then(function (data) {\n                loading.style.display = 'none';\n                var scheds = data.schedules || [];\n                STATE.allowCustomRoute = !!data.allow_custom_route;\n                var flexPicker = document.getElementById('tb-flexible-picker');\n                flexPicker.style.display = 'none';\n\n                if (!scheds.length && !data.allow_flexible) { noSched.style.display = ''; return; }\n\n                scheds.forEach(function (s) {\n                    var d    = new Date(s.departs_at.replace(' ', 'T'));\n                    var dStr = d.toLocaleDateString('en-GB', {weekday:'short',day:'numeric',month:'short',year:'numeric'});\n                    var tStr = d.toLocaleTimeString('en-GB', {hour:'2-digit',minute:'2-digit'});\n                    var avail = s.available > 0 ? s.available + ' seats left' : '<span style=\"color:#c00\">Sold out<\/span>';\n                    if (s.available >= 999) avail = 'Available';\n                    var card = document.createElement('div');\n                    card.className = 'tb-schedule-card' + (s.available <= 0 ? ' tb-schedule-sold' : '');\n                    card.dataset.id = s.id;\n                    var virtualBadge = s.virtual ? '<div class=\"tb-sched-virtual\">\ud83d\udcc5 Flexible date \u2014 contact us to confirm<\/div>' : '';\n                    card.innerHTML = '<div class=\"tb-sched-date\">' + dStr + '<\/div>'\n                        + '<div class=\"tb-sched-time\">\ud83d\udd50 ' + tStr + '<\/div>'\n                        + '<div class=\"tb-sched-avail\">' + avail + '<\/div>'\n                        + virtualBadge;\n                    if (s.available > 0) {\n                        card.addEventListener('click', function () {\n                            deselectFlexible();\n                            document.querySelectorAll('.tb-schedule-card').forEach(function (c) { c.classList.remove('tb-selected'); });\n                            card.classList.add('tb-selected');\n                            \/\/ Use -1 as sentinel for \"virtual\" schedule (no dedicated schedule row)\n                            STATE.scheduleId    = s.virtual ? -1 : s.id;\n                            STATE.scheduleLabel = dStr + ' ' + tStr;\n                            STATE.preferredDate = '';\n                            STATE.firstBookingPriceCents = s.first_booking_price_cents || 0;\n                            document.getElementById('tb-step1-next').disabled = false;\n                        });\n                    }\n                    list.appendChild(card);\n                });\n\n                \/\/ Flexible date picker card\n                if (data.allow_flexible) {\n                    flexPicker.style.display = '';\n                    var flexCard    = document.getElementById('tb-flexible-card');\n                    var flexInputWrap = document.getElementById('tb-flexible-input-wrap');\n                    var flexInput   = document.getElementById('tb-flexible-date');\n                    flexCard.addEventListener('click', function (e) {\n                        if (e.target === flexInput) return; \/\/ let input handle its own events\n                        document.querySelectorAll('.tb-schedule-card').forEach(function (c) { c.classList.remove('tb-selected'); });\n                        flexCard.classList.add('tb-selected');\n                        flexInputWrap.style.display = '';\n                        flexInput.focus();\n                        STATE.scheduleId = -1;\n                        STATE.preferredDate = flexInput.value || '';\n                        updateFlexNext();\n                    });\n                    flexInput.addEventListener('change', function () {\n                        STATE.preferredDate = flexInput.value;\n                        if (flexInput.value) {\n                            var d   = new Date(flexInput.value);\n                            STATE.scheduleLabel = d.toLocaleDateString('en-GB', {weekday:'short',day:'numeric',month:'short',year:'numeric'})\n                                + ' ' + d.toLocaleTimeString('en-GB', {hour:'2-digit',minute:'2-digit'});\n                        }\n                        updateFlexNext();\n                    });\n                }\n            }).catch(function () {\n                loading.style.display = 'none';\n                list.innerHTML = '<p class=\"tb-error\">Could not load schedules. Please try again.<\/p>';\n            });\n        }\n\n        function deselectFlexible() {\n            var flexCard = document.getElementById('tb-flexible-card');\n            if (flexCard) flexCard.classList.remove('tb-selected');\n            var flexInputWrap = document.getElementById('tb-flexible-input-wrap');\n            if (flexInputWrap) flexInputWrap.style.display = 'none';\n        }\n\n        function updateFlexNext() {\n            var flexInput = document.getElementById('tb-flexible-date');\n            var hasDate   = flexInput && flexInput.value && new Date(flexInput.value) > new Date();\n            document.getElementById('tb-step1-next').disabled = !hasDate;\n        }\n\n        \/\/ \u2500\u2500 Step 3: Stop selection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        \/\/ Nominatim autocomplete helper used for start\/end address search\n        var _locTimers = {};\n        function attachLocationSearch(inputId, dropdownId, hintId, stateKey) {\n            var input    = document.getElementById(inputId);\n            var dropdown = document.getElementById(dropdownId);\n            var hint     = document.getElementById(hintId);\n            if (!input) return;\n\n            input.addEventListener('input', function () {\n                var q = input.value.trim();\n                STATE[stateKey] = null; \/\/ cleared until a result is picked\n                dropdown.innerHTML = '';\n                dropdown.style.display = 'none';\n                clearTimeout(_locTimers[stateKey]);\n                if (q.length < 3) return;\n                _locTimers[stateKey] = setTimeout(function () {\n                    fetch('https:\/\/nominatim.openstreetmap.org\/search?format=json&limit=5&q=' + encodeURIComponent(q), { credentials: 'omit' })\n                    .then(function(r){ return r.json(); })\n                    .then(function(results) {\n                        dropdown.innerHTML = '';\n                        if (!results.length) { dropdown.style.display = 'none'; return; }\n                        results.forEach(function(r) {\n                            var li = document.createElement('li');\n                            li.className = 'tb-location-option';\n                            li.textContent = r.display_name;\n                            li.addEventListener('mousedown', function(e) { e.preventDefault(); }); \/\/ keep focus on input\n                            li.addEventListener('click', function() {\n                                STATE[stateKey] = { label: r.display_name, lat: parseFloat(r.lat), lng: parseFloat(r.lon) };\n                                input.value = r.display_name;\n                                if (hint) hint.textContent = '\u2713 Custom location set';\n                                dropdown.style.display = 'none';\n                                dropdown.innerHTML = '';\n                                syncSelectedStops(STATE.tourWaypoints);\n                                \/\/ Redraw map with updated start\/end\n                                if (STATE.customRoute.length > 0 || STATE.customStart || STATE.customEnd) {\n                                    drawBookingRouteMap(buildMapWaypoints());\n                                }\n                            });\n                            dropdown.appendChild(li);\n                        });\n                        dropdown.style.display = '';\n                    })\n                    .catch(function() { dropdown.style.display = 'none'; });\n                }, 320);\n            });\n\n            \/\/ Clear custom location if user clears the input\n            input.addEventListener('change', function() {\n                if (!input.value.trim()) {\n                    STATE[stateKey] = null;\n                    if (hint) hint.textContent = '';\n                    syncSelectedStops(STATE.tourWaypoints);\n                    \/\/ Redraw map without the cleared point\n                    if (STATE.customRoute.length > 0 || STATE.customStart || STATE.customEnd) {\n                        drawBookingRouteMap(buildMapWaypoints());\n                    }\n                }\n            });\n\n            \/\/ Close dropdown on outside click\n            document.addEventListener('click', function(e) {\n                if (!input.contains(e.target) && !dropdown.contains(e.target)) {\n                    dropdown.style.display = 'none';\n                }\n            });\n        }\n\n        function loadStops() {\n            if (!STATE.tourId) return;\n            var list    = document.getElementById('tb-stops-step-list');\n            var loading = document.getElementById('tb-stops-step-loading');\n            var none    = document.getElementById('tb-stops-step-none');\n            list.innerHTML = '';\n            none.style.display = 'none';\n            loading.style.display = '';\n            STATE.selectedStops = [];\n\n            var sep = STATE.restBase.route.indexOf('?') !== -1 ? '&' : '?';\n            fetch(STATE.restBase.route + sep + 'tour_id=' + STATE.tourId, { credentials: 'omit' })\n            .then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })\n            .then(function (data) {\n                loading.style.display = 'none';\n                var waypoints = data.waypoints || [];\n                STATE.tourWaypoints = waypoints;\n\n                \/\/ Pre-fill start\/end inputs with tour defaults (only if user hasn't typed something)\n                var startInput = document.getElementById('tb-start-input');\n                var endInput   = document.getElementById('tb-end-input');\n                if (startInput && !STATE.customStart && waypoints.length > 0) {\n                    startInput.placeholder = waypoints[0].label || 'Tour start';\n                }\n                if (endInput && !STATE.customEnd && waypoints.length > 0) {\n                    var last = waypoints[waypoints.length - 1];\n                    endInput.placeholder = last.label || 'Tour end';\n                }\n\n                \/\/ Auto-select all and skip if no meaningful intermediate stops\n                var intermediate = waypoints.slice(1, waypoints.length - 1);\n                if (waypoints.length < 2 || !intermediate.length) {\n                    STATE.selectedStops = waypoints.slice();\n                    none.style.display = '';\n                    return;\n                }\n\n                waypoints.forEach(function (wp, idx) {\n                    var isFixed = (idx === 0 || idx === waypoints.length - 1);\n                    var defaultDwell = wp.dwell_min || 0;\n\n                    var card = document.createElement('div');\n                    card.className = 'tb-stop-card' + (isFixed ? ' tb-stop-fixed' : '');\n\n                    \/\/ Row 1: checkbox + icon + label + badge\n                    var row1 = document.createElement('div');\n                    row1.className = 'tb-stop-card-row';\n\n                    var cb = document.createElement('input');\n                    cb.type    = 'checkbox';\n                    cb.checked = true;\n                    cb.disabled = isFixed;\n                    cb.dataset.idx = idx;\n                    cb.addEventListener('change', function () {\n                        var dwellRow = card.querySelector('.tb-stop-dwell-row');\n                        if (dwellRow) dwellRow.style.display = cb.checked ? '' : 'none';\n                        syncSelectedStops(waypoints);\n                    });\n\n                    var pin = document.createElement('span');\n                    pin.className = 'tb-stop-pin';\n                    pin.textContent = isFixed ? (idx === 0 ? '\ud83d\udfe2' : '\ud83d\udea9') : '\ud83d\udccd';\n\n                    var lbl = document.createElement('span');\n                    lbl.className = 'tb-stop-label';\n                    lbl.textContent = wp.label || ('Stop ' + (idx + 1));\n\n                    row1.appendChild(cb);\n                    row1.appendChild(pin);\n                    row1.appendChild(lbl);\n\n                    if (isFixed) {\n                        var badge = document.createElement('span');\n                        badge.className = 'tb-stop-badge';\n                        badge.textContent = idx === 0 ? 'Start' : 'End';\n                        row1.appendChild(badge);\n                    }\n                    card.appendChild(row1);\n\n                    \/\/ Row 2: dwell time input\n                    var row2 = document.createElement('div');\n                    row2.className = 'tb-stop-dwell-row';\n\n                    var dwellLabel = document.createElement('label');\n                    dwellLabel.className = 'tb-stop-dwell-label';\n                    dwellLabel.innerHTML = '\u23f1 Time at this stop: ';\n\n                    var dwellInput = document.createElement('input');\n                    dwellInput.type    = 'number';\n                    dwellInput.min     = '0';\n                    dwellInput.max     = '600';\n                    dwellInput.step    = '5';\n                    dwellInput.value   = defaultDwell;\n                    dwellInput.className = 'tb-stop-dwell-input';\n                    dwellInput.dataset.idx = idx;\n                    dwellInput.addEventListener('change', function () { syncSelectedStops(waypoints); });\n\n                    var dwellUnit = document.createElement('span');\n                    dwellUnit.className = 'tb-stop-dwell-unit';\n                    dwellUnit.textContent = 'min';\n\n                    if (defaultDwell > 0) {\n                        var dwellSuggest = document.createElement('span');\n                        dwellSuggest.className = 'tb-stop-dwell-suggest';\n                        dwellSuggest.textContent = '(suggested: ' + defaultDwell + 'min)';\n                        dwellLabel.appendChild(dwellSuggest);\n                    }\n\n                    dwellLabel.appendChild(dwellInput);\n                    dwellLabel.appendChild(dwellUnit);\n                    row2.appendChild(dwellLabel);\n                    card.appendChild(row2);\n\n                    list.appendChild(card);\n                });\n\n                syncSelectedStops(waypoints);\n                drawBookingRouteMap(waypoints);\n            })\n            .catch(function () {\n                loading.style.display = 'none';\n                STATE.selectedStops = STATE.tourWaypoints.slice();\n                none.style.display = '';\n            });\n\n            \/\/ Always show the custom adventure builder so any customer can build their own route\n            var customSection = document.getElementById('tb-custom-route-section');\n            if (customSection) {\n                customSection.style.display = '';\n            }\n            initCustomRouteBuilder();\n        }\n\n        \/* \u2500\u2500 Booking route map (step 3) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n        var _bookingMap        = null;\n        var _bookingRouteLayer = null;\n        var _bookingMarkers    = [];\n        var _bookingMapWps     = [];\n\n        \/\/ Load Leaflet if not already available (handles wp_enqueue footer + CDN fallback)\n        function tbEnsureLeaflet(cb) {\n            if (window.L) { cb(); return; }\n            \/\/ Script tag already injected \u2014 just poll\n            if (document.querySelector('script[src*=\"leaflet\"]')) {\n                var t = setInterval(function () { if (window.L) { clearInterval(t); cb(); } }, 50);\n                return;\n            }\n            \/\/ Last resort: load from CDN ourselves\n            var lnk = document.createElement('link');\n            lnk.rel = 'stylesheet';\n            lnk.href = 'https:\/\/unpkg.com\/leaflet@1.9.4\/dist\/leaflet.css';\n            document.head.appendChild(lnk);\n            var scr = document.createElement('script');\n            scr.src = 'https:\/\/unpkg.com\/leaflet@1.9.4\/dist\/leaflet.js';\n            scr.onload = cb;\n            document.head.appendChild(scr);\n        }\n\n        \/\/ Ensure the Leaflet map is created; container height guaranteed before calling\n        function _initBookingMap(cb) {\n            var mapEl = document.getElementById('tb-booking-route-map');\n            if (!mapEl) return;\n            if (mapEl.offsetHeight < 10) { mapEl.style.height = '280px'; }\n            tbEnsureLeaflet(function () {\n                if (!_bookingMap) {\n                    try {\n                        _bookingMap = L.map('tb-booking-route-map', { scrollWheelZoom: false });\n                        L.tileLayer('https:\/\/{s}.tile.openstreetmap.org\/{z}\/{x}\/{y}.png', {\n                            attribution: '&copy; <a href=\"https:\/\/openstreetmap.org\">OpenStreetMap<\/a>',\n                            maxZoom: 19\n                        }).addTo(_bookingMap);\n                        _bookingMap.setView([-25.0, 133.0], 4); \/\/ Default: Australia overview\n                    } catch(e) { _bookingMap = null; return; }\n                }\n                _bookingMap.invalidateSize();\n                if (cb) cb();\n            });\n        }\n\n        function drawBookingRouteMap(waypoints) {\n            _bookingMapWps = (waypoints || []).filter(function(w){ return w.lat && w.lng; });\n            _clearRouteStats();\n            _doDrawBookingMap();\n        }\n\n        function _fmtDuration(totalSecs) {\n            var h = Math.floor(totalSecs \/ 3600);\n            var m = Math.round((totalSecs % 3600) \/ 60);\n            if (h > 0) return h + 'h ' + m + 'm';\n            return m + 'm';\n        }\n\n        function _showRouteStats(distMetres, driveSecs, wps, legs) {\n            STATE.driveSecs = Math.round(driveSecs);\n            updatePricePreview();\n            var el = document.getElementById('tb-route-stats');\n            if (!el) return;\n            var distKm    = (distMetres \/ 1000).toFixed(1);\n            var dwellSecs = (wps || []).reduce(function(acc, w) {\n                return acc + ((w.dwell_min || 0) * 60);\n            }, 0);\n            var totalSecs = driveSecs + dwellSecs;\n            var html = '<span class=\"tb-rs-item\">&#x1F4CD; <strong>' + distKm + ' km<\/strong><\/span>'\n                     + '<span class=\"tb-rs-sep\">|<\/span>'\n                     + '<span class=\"tb-rs-item\">&#x1F697; Drive <strong>' + _fmtDuration(driveSecs) + '<\/strong><\/span>'\n                     + (dwellSecs > 0\n                         ? '<span class=\"tb-rs-sep\">|<\/span><span class=\"tb-rs-item\">&#x23F1; Stops <strong>' + _fmtDuration(dwellSecs) + '<\/strong><\/span>'\n                             + '<span class=\"tb-rs-sep\">|<\/span><span class=\"tb-rs-item tb-rs-total\">&#x2714; Total <strong>' + _fmtDuration(totalSecs) + '<\/strong><\/span>'\n                         : '');\n            el.innerHTML = html;\n            el.style.display = '';\n            _showRouteLegs(wps, legs);\n        }\n\n        function _showRouteLegs(wps, legs) {\n            var el = document.getElementById('tb-route-legs');\n            if (!el) return;\n            if (!legs || !wps || wps.length < 2) { el.innerHTML = ''; el.style.display = 'none'; return; }\n\n            var html = '<ol class=\"tb-legs-list\">';\n            var cumSecs = 0;\n\n            wps.forEach(function(wp, i) {\n                var label  = escHtml(wp.label || ('Stop ' + (i + 1)));\n                var emoji  = i === 0 ? '&#x1F7E2;' : (i === wps.length - 1 ? '&#x1F6A9;' : '&#x1F4CD;');\n                var dwell  = wp.dwell_min || 0;\n                var etaStr = '';\n\n                if (i === 0) {\n                    etaStr = '<span class=\"tb-leg-badge tb-leg-depart\">Departure<\/span>';\n                } else {\n                    var legDrive  = legs[i - 1] ? Math.round(legs[i - 1].duration) : 0;\n                    var prevDwell = (wps[i - 1].dwell_min || 0) * 60;\n                    cumSecs += prevDwell + legDrive;\n                    etaStr = '<span class=\"tb-leg-badge\">+' + _fmtDuration(cumSecs) + '<\/span>';\n                }\n\n                var dwellStr = (dwell > 0 && i < wps.length - 1)\n                    ? '<span class=\"tb-leg-dwell\">&#x23F1; ' + dwell + 'min stop<\/span>' : '';\n\n                \/\/ Connector arrow (drive time for the leg INTO this stop)\n                if (i > 0) {\n                    var legMins = legs[i - 1] ? _fmtDuration(Math.round(legs[i - 1].duration)) : '';\n                    html += '<li class=\"tb-leg-connector\">&#x2193; ' + legMins + ' drive<\/li>';\n                }\n\n                html += '<li class=\"tb-leg-item\">';\n                html +=   '<span class=\"tb-leg-icon\">' + emoji + '<\/span>';\n                html +=   '<span class=\"tb-leg-label\">' + label + '<\/span>';\n                html +=   '<span class=\"tb-leg-right\">' + etaStr + dwellStr + '<\/span>';\n                html += '<\/li>';\n            });\n\n            html += '<\/ol>';\n            el.innerHTML = html;\n            el.style.display = '';\n\n            \/\/ Update marker tooltips with ETAs\n            cumSecs = 0;\n            wps.forEach(function(wp, i) {\n                if (!_bookingMarkers[i]) return;\n                var dwell    = wp.dwell_min || 0;\n                var tipLabel = wp.label || ('Stop ' + (i + 1));\n                var tipEta   = '';\n                if (i === 0) {\n                    tipEta = 'Departure' + (dwell > 0 ? ' \u00b7 \u23f1 ' + dwell + 'min stop' : '');\n                } else {\n                    var legDrive  = legs[i - 1] ? Math.round(legs[i - 1].duration) : 0;\n                    var prevDwell = (wps[i - 1].dwell_min || 0) * 60;\n                    cumSecs += prevDwell + legDrive;\n                    tipEta = '+' + _fmtDuration(cumSecs) + ' from start'\n                           + (dwell > 0 && i < wps.length - 1 ? ' \u00b7 \u23f1 ' + dwell + 'min stop' : '');\n                }\n                _bookingMarkers[i].unbindTooltip()\n                    .bindTooltip('<strong>' + tipLabel + '<\/strong><br><small>' + tipEta + '<\/small>',\n                        { direction: 'top', offset: [0, -18], permanent: false });\n            });\n        }\n\n        function _clearRouteStats() {\n            var el = document.getElementById('tb-route-stats');\n            if (el) { el.innerHTML = ''; el.style.display = 'none'; }\n            var el2 = document.getElementById('tb-route-legs');\n            if (el2) { el2.innerHTML = ''; el2.style.display = 'none'; }\n        }\n\n        function _doDrawBookingMap() {\n            var validWps = _bookingMapWps;\n            _initBookingMap(function () {\n                if (!_bookingMap) return;\n\n                \/\/ Clear previous markers + route\n                _bookingMarkers.forEach(function(m){ _bookingMap.removeLayer(m); });\n                _bookingMarkers = [];\n                if (_bookingRouteLayer) { _bookingMap.removeLayer(_bookingRouteLayer); _bookingRouteLayer = null; }\n\n                if (!validWps || validWps.length < 1) return;\n\n                validWps.forEach(function(wp, i) {\n                    var emoji = i === 0 ? '\ud83d\udfe2' : (i === validWps.length - 1 ? '\ud83d\udea9' : '\ud83d\udccd');\n                    var m = L.marker([wp.lat, wp.lng], {\n                        icon: L.divIcon({\n                            html: '<span style=\"font-size:22px;line-height:1;filter:drop-shadow(0 1px 2px rgba(0,0,0,.4))\">' + emoji + '<\/span>',\n                            className: 'tb-emoji-icon',\n                            iconAnchor: [11, 22]\n                        })\n                    }).bindTooltip(wp.label || ('Stop ' + (i + 1)), { direction: 'top', offset: [0, -18] });\n                    m.addTo(_bookingMap);\n                    _bookingMarkers.push(m);\n                });\n\n                var lls    = validWps.map(function(w){ return [w.lat, w.lng]; });\n                var bounds = L.latLngBounds(lls);\n                _bookingMap.fitBounds(bounds, { padding: [30, 30] });\n                setTimeout(function () {\n                    _bookingMap.invalidateSize();\n                    _bookingMap.fitBounds(bounds, { padding: [30, 30] });\n                }, 300);\n\n                if (validWps.length < 2) return;\n\n                var coordStr = validWps.map(function(w){ return w.lng + ',' + w.lat; }).join(';');\n                var xhr = new XMLHttpRequest();\n                xhr.open('GET', 'https:\/\/router.project-osrm.org\/route\/v1\/driving\/' + coordStr + '?overview=full&geometries=geojson', true);\n                xhr.timeout = 8000;\n                xhr.onload = function () {\n                    try {\n                        var d = JSON.parse(xhr.responseText);\n                        if (d.routes && d.routes[0]) {\n                            _bookingRouteLayer = L.geoJSON(d.routes[0].geometry, {\n                                style: { color: '#3b82f6', weight: 4, opacity: 0.85 }\n                            }).addTo(_bookingMap);\n                            _bookingMap.fitBounds(_bookingRouteLayer.getBounds(), { padding: [30, 30] });\n                            _showRouteStats(d.routes[0].distance, d.routes[0].duration, validWps, d.routes[0].legs);\n                            return;\n                        }\n                    } catch(e) {}\n                    _bookingRouteLayer = L.polyline(lls, { color: '#ef4444', weight: 3, dashArray: '7,6' }).addTo(_bookingMap);\n                    _clearRouteStats();\n                };\n                xhr.ontimeout = xhr.onerror = function () {\n                    _bookingRouteLayer = L.polyline(lls, { color: '#ef4444', weight: 3, dashArray: '7,6' }).addTo(_bookingMap);\n                    _clearRouteStats();\n                };\n                xhr.send();\n            });\n        }\n\n        \/* \u2500\u2500 Custom Adventure Route Builder \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n        var MAX_CUSTOM_WP = 6;\n\n        function initCustomRouteBuilder() {\n            STATE.customRoute = [];\n            var wpList    = document.getElementById('tb-custom-wp-list');\n            var maxMsg    = document.getElementById('tb-custom-wp-max-msg');\n            wpList.innerHTML = '';\n            if (maxMsg) maxMsg.style.display = 'none';\n\n            \/\/ Replace the Add button with a fresh clone to clear any stacked listeners\n            var oldBtn = document.getElementById('tb-add-custom-wp');\n            var addBtn = oldBtn.cloneNode(true);\n            oldBtn.parentNode.replaceChild(addBtn, oldBtn);\n            addBtn.disabled = false;\n\n            \/\/ Start with one empty row\n            addCustomWaypointRow();\n            addBtn.addEventListener('click', function () {\n                if (wpList.querySelectorAll('.tb-custom-wp-row').length >= MAX_CUSTOM_WP) return;\n                addCustomWaypointRow();\n                updateCustomRouteButtons();\n            });\n        }\n\n        function addCustomWaypointRow() {\n            var wpList = document.getElementById('tb-custom-wp-list');\n            var idx    = wpList.querySelectorAll('.tb-custom-wp-row').length;\n            if (idx >= MAX_CUSTOM_WP) return;\n\n            var row = document.createElement('div');\n            row.className = 'tb-custom-wp-row';\n            row.dataset.wpIdx = idx;\n\n            var rowNum = document.createElement('span');\n            rowNum.className = 'tb-custom-wp-num';\n            rowNum.textContent = (idx + 1) + '.';\n\n            \/\/ Address search\n            var searchWrap = document.createElement('div');\n            searchWrap.className = 'tb-custom-wp-search-wrap';\n\n            var addrInput = document.createElement('input');\n            addrInput.type = 'text';\n            addrInput.placeholder = 'Search for a location\u2026';\n            addrInput.className = 'tb-input tb-custom-wp-addr';\n            addrInput.autocomplete = 'off';\n            addrInput.dataset.lat = '';\n            addrInput.dataset.lng = '';\n\n            var dropdown = document.createElement('ul');\n            dropdown.className = 'tb-location-dropdown';\n            dropdown.style.display = 'none';\n\n            searchWrap.appendChild(addrInput);\n            searchWrap.appendChild(dropdown);\n\n            \/\/ Dwell input\n            var dwellWrap = document.createElement('div');\n            dwellWrap.className = 'tb-custom-wp-dwell-wrap';\n            var dwellLabel = document.createElement('label');\n            dwellLabel.className = 'tb-stop-dwell-label';\n            dwellLabel.textContent = '\u23f1 ';\n            var dwellInput = document.createElement('input');\n            dwellInput.type  = 'number';\n            dwellInput.min   = '0';\n            dwellInput.max   = '600';\n            dwellInput.step  = '5';\n            dwellInput.value = '30';\n            dwellInput.className = 'tb-stop-dwell-input';\n            dwellInput.addEventListener('change', updateCustomRoute);\n            var dwellUnit = document.createElement('span');\n            dwellUnit.className = 'tb-stop-dwell-unit';\n            dwellUnit.textContent = 'min';\n            dwellLabel.appendChild(dwellInput);\n            dwellLabel.appendChild(dwellUnit);\n            dwellWrap.appendChild(dwellLabel);\n\n            \/\/ Remove button\n            var removeBtn = document.createElement('button');\n            removeBtn.type = 'button';\n            removeBtn.className = 'tb-custom-wp-remove';\n            removeBtn.textContent = '\u2715';\n            removeBtn.title = 'Remove this stop';\n            removeBtn.addEventListener('click', function () {\n                row.parentNode.removeChild(row);\n                renumberCustomWpRows();\n                updateCustomRouteButtons();\n                updateCustomRoute();\n            });\n\n            row.appendChild(rowNum);\n            row.appendChild(searchWrap);\n            row.appendChild(dwellWrap);\n            row.appendChild(removeBtn);\n            wpList.appendChild(row);\n\n            \/\/ Attach Nominatim search to this address input\n            attachCustomWpLocationSearch(addrInput, dropdown, dwellInput);\n            updateCustomRouteButtons();\n        }\n\n        function renumberCustomWpRows() {\n            var rows = document.querySelectorAll('#tb-custom-wp-list .tb-custom-wp-row');\n            rows.forEach(function (r, i) {\n                r.dataset.wpIdx = i;\n                var num = r.querySelector('.tb-custom-wp-num');\n                if (num) num.textContent = (i + 1) + '.';\n            });\n        }\n\n        function updateCustomRouteButtons() {\n            var wpList = document.getElementById('tb-custom-wp-list');\n            var addBtn = document.getElementById('tb-add-custom-wp');\n            var maxMsg = document.getElementById('tb-custom-wp-max-msg');\n            var count  = wpList ? wpList.querySelectorAll('.tb-custom-wp-row').length : 0;\n            if (addBtn) addBtn.disabled = count >= MAX_CUSTOM_WP;\n            if (maxMsg) maxMsg.style.display = (count >= MAX_CUSTOM_WP) ? '' : 'none';\n        }\n\n        function updateCustomRoute() {\n            STATE.customRoute = [];\n            var rows = document.querySelectorAll('#tb-custom-wp-list .tb-custom-wp-row');\n            rows.forEach(function (r) {\n                var addr  = r.querySelector('.tb-custom-wp-addr');\n                var dwell = r.querySelector('.tb-stop-dwell-input');\n                if (!addr || !addr.value.trim()) return;\n                var lat = parseFloat(addr.dataset.lat);\n                var lng = parseFloat(addr.dataset.lng);\n                if (isNaN(lat) || isNaN(lng)) return; \/\/ skip rows with no geocode\n                STATE.customRoute.push({\n                    label:     addr.value.trim(),\n                    lat:       lat,\n                    lng:       lng,\n                    dwell_min: dwell ? Math.max(0, parseInt(dwell.value, 10) || 0) : 30,\n                    custom_location: true,\n                });\n            });\n            \/\/ Redraw the map whenever the custom route changes (include start\/end)\n            drawBookingRouteMap(buildMapWaypoints());\n        }\n\n        \/\/ Build the full ordered waypoint list for the map:\n        \/\/ customStart (if set) \u2192 custom stops \u2192 customEnd (if set)\n        function buildMapWaypoints() {\n            var wps = [];\n            if (STATE.customStart && STATE.customStart.lat && STATE.customStart.lng) {\n                wps.push({ label: STATE.customStart.label, lat: STATE.customStart.lat, lng: STATE.customStart.lng });\n            }\n            STATE.customRoute.forEach(function(w) { wps.push(w); });\n            if (STATE.customEnd && STATE.customEnd.lat && STATE.customEnd.lng) {\n                wps.push({ label: STATE.customEnd.label, lat: STATE.customEnd.lat, lng: STATE.customEnd.lng });\n            }\n            return wps;\n        }\n\n        function attachCustomWpLocationSearch(input, dropdown, dwellInput) {\n            var debounceTimer;\n            input.addEventListener('input', function () {\n                clearTimeout(debounceTimer);\n                input.dataset.lat = '';\n                input.dataset.lng = '';\n                var q = input.value.trim();\n                if (q.length < 3) { dropdown.style.display = 'none'; dropdown.innerHTML = ''; return; }\n                debounceTimer = setTimeout(function () {\n                    fetch('https:\/\/nominatim.openstreetmap.org\/search?format=json&limit=5&q=' + encodeURIComponent(q), { credentials: 'omit' })\n                    .then(function (r) { return r.json(); })\n                    .then(function (results) {\n                        dropdown.innerHTML = '';\n                        if (!results.length) { dropdown.style.display = 'none'; return; }\n                        results.forEach(function (place) {\n                            var li = document.createElement('li');\n                            li.className = 'tb-location-option';\n                            li.textContent = place.display_name;\n                            li.addEventListener('mousedown', function (e) { e.preventDefault(); }); \/\/ keep focus on input\n                            li.addEventListener('click', function () {\n                                input.value       = place.display_name;\n                                input.dataset.lat = place.lat;\n                                input.dataset.lng = place.lon;\n                                dropdown.style.display = 'none';\n                                dropdown.innerHTML = '';\n                                updateCustomRoute();\n                            });\n                            dropdown.appendChild(li);\n                        });\n                        dropdown.style.display = '';\n                    }).catch(function () { dropdown.style.display = 'none'; });\n                }, 350);\n            });\n\n            \/\/ Clear geocode if user edits the text manually\n            input.addEventListener('change', function () {\n                if (!input.value.trim()) {\n                    input.dataset.lat = '';\n                    input.dataset.lng = '';\n                    updateCustomRoute();\n                }\n            });\n\n            \/\/ Close dropdown when clicking outside\n            document.addEventListener('click', function (e) {\n                if (!input.contains(e.target) && !dropdown.contains(e.target)) {\n                    dropdown.style.display = 'none';\n                }\n            });\n        }\n\n        function syncSelectedStops(waypoints) {\n            if (!waypoints || !waypoints.length) return;\n            var checkboxes = document.querySelectorAll('#tb-stops-step-list input[type=checkbox]');\n            STATE.selectedStops = [];\n            checkboxes.forEach(function (cb) {\n                if (!cb.checked) return;\n                var wpIdx   = parseInt(cb.dataset.idx, 10);\n                var wp      = Object.assign({}, waypoints[wpIdx]);\n                var dwellEl = document.querySelector('#tb-stops-step-list input.tb-stop-dwell-input[data-idx=\"' + wpIdx + '\"]');\n                var dwell   = dwellEl ? (parseInt(dwellEl.value, 10) || 0) : (wp.dwell_min || 0);\n                wp.custom_dwell_min = dwell;\n\n                \/\/ Override start \/ end with custom addresses if set\n                var isFirst = (wpIdx === 0);\n                var isLast  = (wpIdx === waypoints.length - 1);\n                if (isFirst && STATE.customStart) {\n                    wp.label = STATE.customStart.label;\n                    wp.lat   = STATE.customStart.lat;\n                    wp.lng   = STATE.customStart.lng;\n                    wp.custom_location = true;\n                }\n                if (isLast && STATE.customEnd) {\n                    wp.label = STATE.customEnd.label;\n                    wp.lat   = STATE.customEnd.lat;\n                    wp.lng   = STATE.customEnd.lng;\n                    wp.custom_location = true;\n                }\n                STATE.selectedStops.push(wp);\n            });\n\n            \/\/ If no-stops panel is showing (no intermediate stops), still respect custom start\/end\n            if (!checkboxes.length && waypoints.length) {\n                STATE.selectedStops = waypoints.map(function(wp, idx) {\n                    var out = Object.assign({}, wp);\n                    if (idx === 0 && STATE.customStart) {\n                        out.label = STATE.customStart.label; out.lat = STATE.customStart.lat; out.lng = STATE.customStart.lng; out.custom_location = true;\n                    }\n                    if (idx === waypoints.length - 1 && STATE.customEnd) {\n                        out.label = STATE.customEnd.label; out.lat = STATE.customEnd.lat; out.lng = STATE.customEnd.lng; out.custom_location = true;\n                    }\n                    return out;\n                });\n            }\n        }\n\n        \/\/ \u2500\u2500 Step 2: Seat classes \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        function loadSeatClasses() {\n            if (!STATE.tourId) return;\n            var list = document.getElementById('tb-seat-classes-list');\n            list.innerHTML = '<p class=\"tb-loading\">Loading seat classes\u2026<\/p>';\n            document.getElementById('tb-step2-next').disabled = true;\n            STATE.seatClassId = null;\n\n            var scSep = STATE.restBase.seatClasses.indexOf('?') !== -1 ? '&' : '?';\n            fetch(STATE.restBase.seatClasses + scSep + 'tour_id=' + STATE.tourId, { credentials: 'omit' })\n            .then(function (r) {\n                if (!r.ok) throw new Error('HTTP ' + r.status);\n                return r.json();\n            }).then(function (data) {\n                list.innerHTML = '';\n                var classes = data.seat_classes || [];\n                if (!classes.length) {\n                    list.innerHTML = '<p class=\"tb-info-box\">Standard seating \u2014 no class selection needed.<\/p>';\n                    STATE.seatClassId   = 0;\n                    STATE.seatClassName = 'Standard';\n                    STATE.unitPriceCents = data.fallback_price_cents || 0;\n                    updatePricePreview();\n                    document.getElementById('tb-step2-next').disabled = false;\n                    return;\n                }\n                classes.forEach(function (sc) {\n                    var card = document.createElement('div');\n                    card.className = 'tb-seat-class-card';\n                    card.dataset.id = sc.id;\n                    var priceStr = sc.price_cents > 0 ? STATE.currencySymbol + (sc.price_cents \/ 100).toFixed(2) + ' \/ person' : 'Included';\n                    card.innerHTML = '<div class=\"tb-sc-name\">' + escHtml(sc.class_name) + '<\/div>'\n                        + '<div class=\"tb-sc-price\">' + priceStr + '<\/div>'\n                        + '<div class=\"tb-sc-avail\">' + (sc.available > 0 ? sc.available + ' seats' : '<span style=\"color:#c00\">Full<\/span>') + '<\/div>';\n                    if (sc.available > 0) {\n                        card.addEventListener('click', function () {\n                            document.querySelectorAll('.tb-seat-class-card').forEach(function (c) { c.classList.remove('tb-selected'); });\n                            card.classList.add('tb-selected');\n                            STATE.seatClassId    = sc.id;\n                            STATE.seatClassName  = sc.class_name;\n                            STATE.unitPriceCents = sc.price_cents;\n                            updatePricePreview();\n                            document.getElementById('tb-step2-next').disabled = false;\n                        });\n                    }\n                    list.appendChild(card);\n                });\n            }).catch(function () {\n                list.innerHTML = '<p class=\"tb-error\">Could not load seat classes.<\/p>';\n            });\n        }\n\n        \/\/ \u2500\u2500 Passenger counters \u2500\u2500\u2500\n        document.querySelectorAll('.tb-counter-btn').forEach(function (btn) {\n            btn.addEventListener('click', function () {\n                var type = btn.getAttribute('data-type');\n                var dir  = parseInt(btn.getAttribute('data-dir'), 10);\n                var max  = btn.dataset.max ? parseInt(btn.dataset.max, 10) : 99;\n                STATE[type] = Math.min(max, Math.max(0, (STATE[type] || 0) + dir));\n                if (type === 'adults' && STATE.adults < 1) STATE.adults = 1;\n                document.getElementById('tb-count-' + type).textContent = STATE[type];\n                updatePricePreview();\n            });\n        });\n\n        function totalPassengers() { return STATE.adults + STATE.children + STATE.seniors; }\n\n        function childSeatSurcharge() { return STATE.childSeats * STATE.childSeatFeeCents; }\n\n        \/\/ \u2500\u2500 Time-based group pricing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        \/\/ Up to 11 pax: $100\/hr | 12-21 pax: $150\/hr | 22-33 pax: $250\/hr\n        \/\/ Total time (drive + dwell) rounded UP to nearest 15 min\n        function calcTourRateCentsPerHour() {\n            var pax = totalPassengers();\n            if (pax <= 11) return 10000;\n            if (pax <= 21) return 15000;\n            return 25000;\n        }\n\n        \/\/ Sum dwell time (seconds) across the active waypoints\n        function calcDwellSecs() {\n            var wps = (STATE.customRoute && STATE.customRoute.length) ? STATE.customRoute : STATE.selectedStops;\n            return (wps || []).reduce(function(acc, w) {\n                return acc + ((w.dwell_min || w.custom_dwell_min || 0) * 60);\n            }, 0);\n        }\n\n        \/\/ Total billable seconds = drive + dwell\n        function calcTotalSecs() {\n            return STATE.driveSecs + calcDwellSecs();\n        }\n\n        \/\/ Per-seat price once a schedule already has a first booking\n        function perSeatPriceCents() {\n            return STATE.firstBookingPriceCents > 0 ? Math.round(STATE.firstBookingPriceCents \/ 20) : 0;\n        }\n\n        function calcTourPriceCents() {\n            \/\/ After the first booking on a schedule, remaining seats are sold at\n            \/\/ (first booking total) \/ 20 each \u2014 no longer time-based.\n            if (STATE.firstBookingPriceCents > 0) {\n                return perSeatPriceCents() * totalPassengers();\n            }\n            var total = calcTotalSecs();\n            if (!total) return 0;\n            var rounded = Math.ceil(total \/ 900) * 900; \/\/ round up to nearest 15 min\n            return Math.round(calcTourRateCentsPerHour() * rounded \/ 3600);\n        }\n\n        function updatePricePreview() {\n            var prev = document.getElementById('tb-price-preview');\n            if (STATE.seatClassId === null) return;\n\n            var pax        = totalPassengers();\n            var rateCents  = calcTourRateCentsPerHour();\n            var tourCents  = calcTourPriceCents();\n            var childExtra = childSeatSurcharge();\n            var grandTotal = tourCents + childExtra;\n\n            \/\/ \u2500\u2500 Per-seat mode: schedule already has a first booking \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n            if (STATE.firstBookingPriceCents > 0) {\n                var seatCents = perSeatPriceCents();\n                document.getElementById('tb-unit-price-label').textContent =\n                    STATE.currencySymbol + (seatCents \/ 100).toFixed(2) + '\/seat';\n                document.getElementById('tb-passengers-total-label').textContent =\n                    pax + ' seat' + (pax !== 1 ? 's' : '') + ' \u00d7 ' + STATE.currencySymbol + (seatCents \/ 100).toFixed(2);\n\n                var childRowPS = document.getElementById('tb-child-fee-row');\n                if (STATE.childSeats > 0) {\n                    document.getElementById('tb-child-fee-label').textContent = 'Child seat hire (' + STATE.childSeats + ' \u00d7 ' + STATE.currencySymbol + '20)';\n                    document.getElementById('tb-child-fee-total').textContent  = STATE.currencySymbol + (childExtra \/ 100).toFixed(2);\n                    childRowPS.style.display = '';\n                } else {\n                    childRowPS.style.display = 'none';\n                }\n                document.getElementById('tb-total-price-label').textContent = STATE.currencySymbol + (grandTotal \/ 100).toFixed(2);\n                prev.style.display = '';\n                return;\n            }\n\n            \/\/ Rate label: show tier description\n            var paxTier = pax <= 11 ? '(up to 11 pax)' : pax <= 21 ? '(12\u201321 pax)' : '(22\u201333 pax)';\n            document.getElementById('tb-unit-price-label').textContent = STATE.currencySymbol + (rateCents \/ 100).toFixed(0) + '\/hr ' + paxTier;\n\n            \/\/ Duration label: show breakdown + rounded total, or prompt to add route\n            var durationLabel, totalLabel;\n            if (STATE.driveSecs > 0) {\n                var dwellSecs  = calcDwellSecs();\n                var totalRaw   = STATE.driveSecs + dwellSecs;\n                var rounded    = Math.ceil(totalRaw \/ 900) * 900;\n                var breakdown  = _fmtDuration(STATE.driveSecs) + ' drive'\n                               + (dwellSecs > 0 ? ' + ' + _fmtDuration(dwellSecs) + ' stops' : '')\n                               + ' \u2192 ' + _fmtDuration(rounded) + ' (rounded up)';\n                durationLabel = breakdown;\n                totalLabel    = STATE.currencySymbol + (grandTotal \/ 100).toFixed(2);\n            } else {\n                durationLabel = 'Route needed \u2014 add stops on next step';\n                totalLabel    = 'Pending';\n            }\n            document.getElementById('tb-passengers-total-label').textContent = durationLabel;\n\n            var childRow = document.getElementById('tb-child-fee-row');\n            if (STATE.childSeats > 0) {\n                document.getElementById('tb-child-fee-label').textContent = 'Child seat hire (' + STATE.childSeats + ' \u00d7 ' + STATE.currencySymbol + '20)';\n                document.getElementById('tb-child-fee-total').textContent  = STATE.currencySymbol + (childExtra \/ 100).toFixed(2);\n                childRow.style.display = '';\n            } else {\n                childRow.style.display = 'none';\n            }\n            document.getElementById('tb-total-price-label').textContent = totalLabel;\n            prev.style.display = '';\n        }\n\n        \/\/ \u2500\u2500 Summary bar (step 4) \u2500\u2500\u2500\n        function renderSummaryBar() {\n            var total = totalPassengers();\n            var cents = calcTourPriceCents() + childSeatSurcharge();\n            var stopsLabel;\n            if (STATE.selectedStops.length) {\n                stopsLabel = STATE.selectedStops.map(function(s) {\n                    var d    = s.custom_dwell_min || s.dwell_min || 0;\n                    var name = escHtml(s.label);\n                    if (s.custom_location) name = '\ud83d\udccc ' + name;\n                    return name + (d > 0 ? ' <span class=\"tb-summary-dwell\">(' + d + 'min)<\/span>' : '');\n                }).join(' \u2192 ');\n            } else {\n                stopsLabel = 'All stops';\n            }\n            document.getElementById('tb-summary-bar').innerHTML =\n                '<strong>' + (STATE.tourTitle || 'Tour') + '<\/strong>'\n                + ' &nbsp;\u00b7&nbsp; ' + escHtml(STATE.scheduleLabel)\n                + ' &nbsp;\u00b7&nbsp; ' + escHtml(STATE.seatClassName)\n                + ' &nbsp;\u00b7&nbsp; ' + total + ' passenger' + (total!==1?'s':'')\n                + ' &nbsp;\u00b7&nbsp; ' + stopsLabel\n                + ' &nbsp;\u00b7&nbsp; <strong>' + STATE.currencySymbol + (cents\/100).toFixed(2) + '<\/strong>';\n        }\n\n        \/\/ \u2500\u2500 buttons Step \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        document.getElementById('tb-step1-next').addEventListener('click', function () {\n            if (STATE.scheduleId === null) return;\n            showStep(2);\n            loadSeatClasses();\n        });\n        document.getElementById('tb-step2-back').addEventListener('click', function () { showStep(1); });\n        document.getElementById('tb-step2-next').addEventListener('click', function () {\n            if (STATE.seatClassId === null) return;\n            showStep(3);\n            loadStops();\n            \/\/ Attach address-search autocompletes (safe to call multiple times)\n            attachLocationSearch('tb-start-input', 'tb-start-dropdown', 'tb-start-hint', 'customStart');\n            attachLocationSearch('tb-end-input',   'tb-end-dropdown',   'tb-end-hint',   'customEnd');\n        });\n        document.getElementById('tb-step3-back').addEventListener('click', function () { showStep(2); });\n        document.getElementById('tb-step3-next').addEventListener('click', function () {\n            renderSummaryBar();\n            showStep(4);\n        });\n        document.getElementById('tb-step4-back').addEventListener('click', function () { showStep(3); });\n\n        \/\/ \u2500\u2500 Pay & Confirm \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        document.getElementById('tb-step3-confirm').addEventListener('click', function () {\n            var name  = document.getElementById('tb-ms-name').value.trim();\n            var email = document.getElementById('tb-ms-email').value.trim();\n            var phone = document.getElementById('tb-ms-phone').value.trim();\n            var nonce = document.getElementById('tb-ms-nonce') ? document.getElementById('tb-ms-nonce').value.trim() : '';\n            var resultEl = document.getElementById('tb-ms-result');\n\n            if (!name || !email) { resultEl.innerHTML = '<p class=\"tb-error\">Please fill in your name and email.<\/p>'; return; }\n            if (!nonce) { resultEl.innerHTML = '<p class=\"tb-error\">Payment details are required.<\/p>'; return; }\n\n            var btn = this;\n            btn.disabled = true;\n            btn.textContent = 'Processing\u2026';\n            resultEl.innerHTML = '';\n\n            var payload = {\n                tour_id:              STATE.tourId,\n                schedule_id:          STATE.scheduleId,\n                seat_class_id:        STATE.seatClassId,\n                name:                 name,\n                email:                email,\n                phone:                phone,\n                adults:               STATE.adults,\n                children:             STATE.children,\n                seniors:              STATE.seniors,\n                child_seats:          STATE.childSeats,\n                child_seat_fee_cents: childSeatSurcharge(),\n                drive_secs:           STATE.driveSecs,\n                dwell_secs:           calcDwellSecs(),\n                sq_nonce:             nonce,\n                preferred_date:       STATE.preferredDate || '',\n                selected_stops: STATE.selectedStops.map(function(s){\n                    return {\n                        label:            s.label,\n                        lat:              s.lat,\n                        lng:              s.lng,\n                        custom_dwell_min: s.custom_dwell_min || s.dwell_min || 0,\n                        custom_location:  !!s.custom_location,\n                    };\n                }),\n                custom_route: buildMapWaypoints().map(function(w){\n                    return {\n                        label:           w.label,\n                        lat:             w.lat,\n                        lng:             w.lng,\n                        dwell_min:       w.dwell_min || 0,\n                        custom_location: true,\n                    };\n                }),\n            };\n\n            fetch(STATE.restBase.bookV2, {\n                method:  'POST',\n                headers: { 'Content-Type': 'application\/json', 'X-WP-Nonce': STATE.restBase.nonce },\n                body:    JSON.stringify(payload),\n            }).then(function (r) {\n                if (!r.ok) {\n                    return r.json().then(function (d) {\n                        throw new Error(d.message || d.error || 'HTTP ' + r.status);\n                    }).catch(function () {\n                        throw new Error('HTTP ' + r.status);\n                    });\n                }\n                return r.json();\n            }).then(function (data) {\n                if (!data.success) {\n                    resultEl.innerHTML = '<p class=\"tb-error\">\u2717 ' + escHtml(data.error || 'Booking failed. Please try again.') + '<\/p>';\n                    btn.disabled = false; btn.textContent = 'Confirm & Pay \ud83d\udd12';\n                    return;\n                }\n\n                \/\/ Redirect to the default payment page to collect payment details and take payment\n                if (data.payment_url) {\n                    resultEl.innerHTML = '<p class=\"tb-info\">Redirecting to secure payment\u2026<\/p>';\n                    btn.textContent = 'Redirecting to payment\u2026';\n                    window.location.href = data.payment_url;\n                    return;\n                }\n\n                var total = totalPassengers();\n                var cents = calcTourPriceCents() + childSeatSurcharge();\n                var childLine = STATE.childSeats > 0\n                    ? '<p><strong>Child Seat Hire:<\/strong> ' + STATE.childSeats + ' \u00d7 ' + STATE.currencySymbol + '20 = ' + STATE.currencySymbol + (childSeatSurcharge()\/100).toFixed(2) + '<\/p>'\n                    : '';\n                var depLabel = STATE.scheduleLabel || (STATE.preferredDate ? new Date(STATE.preferredDate).toLocaleString('en-GB') : '\u2014');\n\n                \/\/ Build waypoints section\n                function wpListHtml(wps, dwellKey) {\n                    if (!wps || !wps.length) return '';\n                    return '<ol class=\"tb-confirm-wp-list\">' + wps.map(function(w, i) {\n                        var label = escHtml(w.label || ('Stop ' + (i + 1)));\n                        var dwell = w[dwellKey] || w.dwell_min || 0;\n                        var pin   = w.custom_location ? '<span class=\"tb-confirm-wp-pin\">\\uD83D\\uDCCC<\/span>' : '';\n                        var dwellStr = dwell > 0 ? ' <span class=\"tb-confirm-wp-dwell\">\\u23F1 ' + dwell + ' min stop<\/span>' : '';\n                        return '<li>' + pin + label + dwellStr + '<\/li>';\n                    }).join('') + '<\/ol>';\n                }\n\n                var wpsSection = '';\n                var hasCustom = (STATE.customStart && STATE.customStart.lat)\n                             || (STATE.customRoute && STATE.customRoute.length)\n                             || (STATE.customEnd   && STATE.customEnd.lat);\n                if (hasCustom) {\n                    \/\/ Full custom itinerary: start + adventure builder stops + end\n                    var fullRoute = buildMapWaypoints();\n                    wpsSection += '<p><strong>Your Itinerary (' + fullRoute.length + ' stop' + (fullRoute.length !== 1 ? 's' : '') + '):<\/strong><\/p>'\n                                + wpListHtml(fullRoute, 'dwell_min');\n                } else if (STATE.selectedStops && STATE.selectedStops.length) {\n                    \/\/ Predefined tour stops\n                    wpsSection += '<p><strong>Stops:<\/strong><\/p>' + wpListHtml(STATE.selectedStops, 'custom_dwell_min');\n                } else {\n                    wpsSection = '<p><strong>Stops:<\/strong> All stops<\/p>';\n                }\n\n                var driveLine = STATE.driveSecs > 0\n                    ? (function() {\n                        var dwellSecs = calcDwellSecs();\n                        var totalRaw  = STATE.driveSecs + dwellSecs;\n                        var rounded   = Math.ceil(totalRaw \/ 900) * 900;\n                        var s = '<p><strong>Total time:<\/strong> '\n                              + _fmtDuration(STATE.driveSecs) + ' drive'\n                              + (dwellSecs > 0 ? ' + ' + _fmtDuration(dwellSecs) + ' stops' : '')\n                              + ' = ' + _fmtDuration(rounded) + ' (billed)<\/p>';\n                        return s;\n                      })()\n                    : '';\n\n                document.getElementById('tb-confirmed-detail').innerHTML =\n                    '<p><strong>Booking Ref:<\/strong> ' + escHtml(data.booking_ref || '') + '<\/p>'\n                    + '<p><strong>Tour:<\/strong> ' + escHtml(STATE.tourTitle || '') + '<\/p>'\n                    + '<p><strong>Departure:<\/strong> ' + escHtml(depLabel) + '<\/p>'\n                    + '<p><strong>Class:<\/strong> ' + escHtml(STATE.seatClassName) + '<\/p>'\n                    + '<p><strong>Passengers:<\/strong> ' + total + '<\/p>'\n                    + childLine\n                    + wpsSection\n                    + driveLine\n                    + '<p><strong>Total:<\/strong> ' + STATE.currencySymbol + (cents\/100).toFixed(2) + '<\/p>'\n                    + '<p><strong>Name:<\/strong> ' + escHtml(name) + '<\/p>'\n                    + '<p><strong>Email:<\/strong> ' + escHtml(email) + '<\/p>';\n                showStep(5);\n            }).catch(function (err) {\n                var msg = (err && err.message) ? err.message : 'An error occurred. Please refresh the page and try again.';\n                resultEl.innerHTML = '<p class=\"tb-error\">\u2717 ' + escHtml(msg) + '<\/p>';\n                btn.disabled = false; btn.textContent = 'Confirm & Pay \ud83d\udd12';\n            });\n        });\n\n        \/\/ \u2500\u2500 another Book \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        document.getElementById('tb-book-another').addEventListener('click', function () {\n            STATE.scheduleId = null; STATE.seatClassId = null;\n            STATE.adults = 1; STATE.children = 0; STATE.seniors = 0;\n            STATE.preferredDate = '';\n            STATE.selectedStops = []; STATE.tourWaypoints = [];\n            STATE.customStart = null; STATE.customEnd = null;\n            STATE.driveSecs = 0;\n            var startInput = document.getElementById('tb-start-input');\n            var endInput   = document.getElementById('tb-end-input');\n            if (startInput) { startInput.value = ''; startInput.placeholder = 'Use tour start point or search an address\u2026'; }\n            if (endInput)   { endInput.value   = ''; endInput.placeholder   = 'Use tour end point or search an address\u2026'; }\n            var flexDate = document.getElementById('tb-flexible-date');\n            if (flexDate) flexDate.value = '';\n            document.querySelectorAll('.tb-counter-val').forEach(function(el){\n                el.textContent = el.id === 'tb-count-adults' ? '1' : '0';\n            });\n            showStep(1);\n            if (STATE.tourId) loadSchedules();\n        });\n\n        \/\/ \u2500\u2500 Tour dropdown (when no pinned id) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        var tourSel = document.getElementById('tb-ms-tour-select');\n        if (tourSel) {\n            tourSel.addEventListener('change', function () {\n                STATE.tourId = parseInt(this.value, 10) || null;\n                STATE.tourTitle = this.options[this.selectedIndex].text;\n                if (STATE.tourId) loadSchedules();\n            });\n        }\n\n        function escHtml(str) {\n            return String(str).replace(\/&\/g,'&amp;').replace(\/<\/g,'&lt;').replace(\/>\/g,'&gt;').replace(\/\"\/g,'&quot;');\n        }\n\n        \/\/ \u2500\u2500 Boot \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        (function boot() {\n                        showStep(1);\n            if (STATE.tourId) loadSchedules();\n        })();\n    })();\n    <\/script>\n    \n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_monsterinsights_skip_tracking":false,"footnotes":""},"class_list":["post-1020","page","type-page","status-publish","hentry"],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO 5.0.1.1 - aioseo.com -->\n\t<meta name=\"description\" content=\"1 Date 2 Seats 3 Stops 4 Details 5 Done \ud83d\udcc5 Choose Your Departure Date Tour * \u2014 Select a tour \u2014 Choose Your Own - Test tour (30 Nov -0001) Shute Harbour to Proserpine (30 Nov -0001) Cape Gloucester (30 Nov -0001) Townsville Weekender (30 Nov -0001) The Pioneer Valley Pub Tour (01 Jun\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<link rel=\"canonical\" href=\"https:\/\/airliebeachpartybus.com.au\/?page_id=1020\" \/>\n\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.1.1\" \/>\n\t\t<meta property=\"og:locale\" content=\"en_US\" \/>\n\t\t<meta property=\"og:site_name\" content=\"Airlie Beach Party Bus | Servicing the Whitsundays and more!\" \/>\n\t\t<meta property=\"og:type\" content=\"article\" \/>\n\t\t<meta property=\"og:title\" content=\"Need a Tour | Airlie Beach Party Bus\" \/>\n\t\t<meta property=\"og:description\" content=\"1 Date 2 Seats 3 Stops 4 Details 5 Done \ud83d\udcc5 Choose Your Departure Date Tour * \u2014 Select a tour \u2014 Choose Your Own - Test tour (30 Nov -0001) Shute Harbour to Proserpine (30 Nov -0001) Cape Gloucester (30 Nov -0001) Townsville Weekender (30 Nov -0001) The Pioneer Valley Pub Tour (01 Jun\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/airliebeachpartybus.com.au\/?page_id=1020\" \/>\n\t\t<meta property=\"og:image\" content=\"https:\/\/172-105-183-176.ip.linodeusercontent.com\/wp-content\/uploads\/2019\/12\/bat-n-ball.png\" \/>\n\t\t<meta property=\"og:image:secure_url\" content=\"https:\/\/172-105-183-176.ip.linodeusercontent.com\/wp-content\/uploads\/2019\/12\/bat-n-ball.png\" \/>\n\t\t<meta property=\"article:published_time\" content=\"2026-05-14T06:27:03+00:00\" \/>\n\t\t<meta property=\"article:modified_time\" content=\"2026-05-28T02:38:30+00:00\" \/>\n\t\t<meta property=\"article:publisher\" content=\"https:\/\/facebook.com\/airliebeachpartybus\" \/>\n\t\t<meta name=\"twitter:card\" content=\"summary\" \/>\n\t\t<meta name=\"twitter:title\" content=\"Need a Tour | Airlie Beach Party Bus\" \/>\n\t\t<meta name=\"twitter:description\" content=\"1 Date 2 Seats 3 Stops 4 Details 5 Done \ud83d\udcc5 Choose Your Departure Date Tour * \u2014 Select a tour \u2014 Choose Your Own - Test tour (30 Nov -0001) Shute Harbour to Proserpine (30 Nov -0001) Cape Gloucester (30 Nov -0001) Townsville Weekender (30 Nov -0001) The Pioneer Valley Pub Tour (01 Jun\" \/>\n\t\t<meta name=\"twitter:image\" content=\"https:\/\/172-105-183-176.ip.linodeusercontent.com\/wp-content\/uploads\/2019\/12\/bat-n-ball.png\" \/>\n\t\t<script type=\"application\/ld+json\" class=\"aioseo-schema\">\n\t\t\t{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/airliebeachpartybus.com.au\\\/?page_id=1020#breadcrumblist\",\"itemListElement\":[{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/airliebeachpartybus.com.au#listItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/airliebeachpartybus.com.au\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/airliebeachpartybus.com.au\\\/?page_id=1020#listItem\",\"name\":\"Need a Tour\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/airliebeachpartybus.com.au\\\/?page_id=1020#listItem\",\"position\":2,\"name\":\"Need a Tour\",\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/airliebeachpartybus.com.au#listItem\",\"name\":\"Home\"}}]},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/airliebeachpartybus.com.au\\\/#organization\",\"name\":\"Airlie Beach Party Bus\",\"description\":\"Servicing the Whitsundays and more!\",\"url\":\"https:\\\/\\\/airliebeachpartybus.com.au\\\/\",\"telephone\":\"+61484653767\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https:\\\/\\\/172-105-183-176.ip.linodeusercontent.com\\\/wp-content\\\/uploads\\\/2018\\\/12\\\/Airlie-Party-Bus_Logo-Black-FINAL.jpg\",\"@id\":\"https:\\\/\\\/airliebeachpartybus.com.au\\\/?page_id=1020\\\/#organizationLogo\"},\"image\":{\"@id\":\"https:\\\/\\\/airliebeachpartybus.com.au\\\/?page_id=1020\\\/#organizationLogo\"},\"sameAs\":[\"https:\\\/\\\/facebook.com\\\/airliebeachpartybus\",\"https:\\\/\\\/instagram.com\\\/airliebeachpartybus\"]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/airliebeachpartybus.com.au\\\/?page_id=1020#webpage\",\"url\":\"https:\\\/\\\/airliebeachpartybus.com.au\\\/?page_id=1020\",\"name\":\"Need a Tour | Airlie Beach Party Bus\",\"description\":\"1 Date 2 Seats 3 Stops 4 Details 5 Done \\ud83d\\udcc5 Choose Your Departure Date Tour * \\u2014 Select a tour \\u2014 Choose Your Own - Test tour (30 Nov -0001) Shute Harbour to Proserpine (30 Nov -0001) Cape Gloucester (30 Nov -0001) Townsville Weekender (30 Nov -0001) The Pioneer Valley Pub Tour (01 Jun\",\"inLanguage\":\"en-AU\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/airliebeachpartybus.com.au\\\/#website\"},\"breadcrumb\":{\"@id\":\"https:\\\/\\\/airliebeachpartybus.com.au\\\/?page_id=1020#breadcrumblist\"},\"datePublished\":\"2026-05-14T16:27:03+10:00\",\"dateModified\":\"2026-05-28T12:38:30+10:00\"},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/airliebeachpartybus.com.au\\\/#website\",\"url\":\"https:\\\/\\\/airliebeachpartybus.com.au\\\/\",\"name\":\"Airlie Beach Party Bus\",\"description\":\"Servicing the Whitsundays and more!\",\"inLanguage\":\"en-AU\",\"publisher\":{\"@id\":\"https:\\\/\\\/airliebeachpartybus.com.au\\\/#organization\"}}]}\n\t\t<\/script>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"Need a Tour | Airlie Beach Party Bus","description":"1 Date 2 Seats 3 Stops 4 Details 5 Done \ud83d\udcc5 Choose Your Departure Date Tour * \u2014 Select a tour \u2014 Choose Your Own - Test tour (30 Nov -0001) Shute Harbour to Proserpine (30 Nov -0001) Cape Gloucester (30 Nov -0001) Townsville Weekender (30 Nov -0001) The Pioneer Valley Pub Tour (01 Jun","canonical_url":"https:\/\/airliebeachpartybus.com.au\/?page_id=1020","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"BreadcrumbList","@id":"https:\/\/airliebeachpartybus.com.au\/?page_id=1020#breadcrumblist","itemListElement":[{"@type":"ListItem","@id":"https:\/\/airliebeachpartybus.com.au#listItem","position":1,"name":"Home","item":"https:\/\/airliebeachpartybus.com.au","nextItem":{"@type":"ListItem","@id":"https:\/\/airliebeachpartybus.com.au\/?page_id=1020#listItem","name":"Need a Tour"}},{"@type":"ListItem","@id":"https:\/\/airliebeachpartybus.com.au\/?page_id=1020#listItem","position":2,"name":"Need a Tour","previousItem":{"@type":"ListItem","@id":"https:\/\/airliebeachpartybus.com.au#listItem","name":"Home"}}]},{"@type":"Organization","@id":"https:\/\/airliebeachpartybus.com.au\/#organization","name":"Airlie Beach Party Bus","description":"Servicing the Whitsundays and more!","url":"https:\/\/airliebeachpartybus.com.au\/","telephone":"+61484653767","logo":{"@type":"ImageObject","url":"https:\/\/172-105-183-176.ip.linodeusercontent.com\/wp-content\/uploads\/2018\/12\/Airlie-Party-Bus_Logo-Black-FINAL.jpg","@id":"https:\/\/airliebeachpartybus.com.au\/?page_id=1020\/#organizationLogo"},"image":{"@id":"https:\/\/airliebeachpartybus.com.au\/?page_id=1020\/#organizationLogo"},"sameAs":["https:\/\/facebook.com\/airliebeachpartybus","https:\/\/instagram.com\/airliebeachpartybus"]},{"@type":"WebPage","@id":"https:\/\/airliebeachpartybus.com.au\/?page_id=1020#webpage","url":"https:\/\/airliebeachpartybus.com.au\/?page_id=1020","name":"Need a Tour | Airlie Beach Party Bus","description":"1 Date 2 Seats 3 Stops 4 Details 5 Done \ud83d\udcc5 Choose Your Departure Date Tour * \u2014 Select a tour \u2014 Choose Your Own - Test tour (30 Nov -0001) Shute Harbour to Proserpine (30 Nov -0001) Cape Gloucester (30 Nov -0001) Townsville Weekender (30 Nov -0001) The Pioneer Valley Pub Tour (01 Jun","inLanguage":"en-AU","isPartOf":{"@id":"https:\/\/airliebeachpartybus.com.au\/#website"},"breadcrumb":{"@id":"https:\/\/airliebeachpartybus.com.au\/?page_id=1020#breadcrumblist"},"datePublished":"2026-05-14T16:27:03+10:00","dateModified":"2026-05-28T12:38:30+10:00"},{"@type":"WebSite","@id":"https:\/\/airliebeachpartybus.com.au\/#website","url":"https:\/\/airliebeachpartybus.com.au\/","name":"Airlie Beach Party Bus","description":"Servicing the Whitsundays and more!","inLanguage":"en-AU","publisher":{"@id":"https:\/\/airliebeachpartybus.com.au\/#organization"}}]},"og:locale":"en_US","og:site_name":"Airlie Beach Party Bus | Servicing the Whitsundays and more!","og:type":"article","og:title":"Need a Tour | Airlie Beach Party Bus","og:description":"1 Date 2 Seats 3 Stops 4 Details 5 Done \ud83d\udcc5 Choose Your Departure Date Tour * \u2014 Select a tour \u2014 Choose Your Own - Test tour (30 Nov -0001) Shute Harbour to Proserpine (30 Nov -0001) Cape Gloucester (30 Nov -0001) Townsville Weekender (30 Nov -0001) The Pioneer Valley Pub Tour (01 Jun","og:url":"https:\/\/airliebeachpartybus.com.au\/?page_id=1020","og:image":"https:\/\/172-105-183-176.ip.linodeusercontent.com\/wp-content\/uploads\/2019\/12\/bat-n-ball.png","og:image:secure_url":"https:\/\/172-105-183-176.ip.linodeusercontent.com\/wp-content\/uploads\/2019\/12\/bat-n-ball.png","article:published_time":"2026-05-14T06:27:03+00:00","article:modified_time":"2026-05-28T02:38:30+00:00","article:publisher":"https:\/\/facebook.com\/airliebeachpartybus","twitter:card":"summary","twitter:title":"Need a Tour | Airlie Beach Party Bus","twitter:description":"1 Date 2 Seats 3 Stops 4 Details 5 Done \ud83d\udcc5 Choose Your Departure Date Tour * \u2014 Select a tour \u2014 Choose Your Own - Test tour (30 Nov -0001) Shute Harbour to Proserpine (30 Nov -0001) Cape Gloucester (30 Nov -0001) Townsville Weekender (30 Nov -0001) The Pioneer Valley Pub Tour (01 Jun","twitter:image":"https:\/\/172-105-183-176.ip.linodeusercontent.com\/wp-content\/uploads\/2019\/12\/bat-n-ball.png"},"aioseo_meta_data":{"post_id":"1020","title":null,"description":null,"keywords":null,"keyphrases":{"focus":[],"additional":[]},"primary_term":null,"canonical_url":null,"og_title":null,"og_description":null,"og_object_type":"default","og_image_type":"default","og_image_url":null,"og_image_width":null,"og_image_height":null,"og_image_custom_url":null,"og_image_custom_fields":null,"og_video":"","og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"default","twitter_image_type":"default","twitter_image_url":null,"twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_title":null,"twitter_description":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"WebPage","isEnabled":true},"graphs":[]},"schema_type":"default","schema_type_options":null,"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":"-1","robots_max_videopreview":"-1","robots_max_imagepreview":"large","priority":null,"frequency":"default","local_seo":null,"breadcrumb_settings":null,"limit_modified_date":false,"ai":{"faqs":[],"keyPoints":[],"schemas":[],"titles":[],"descriptions":[],"socialPosts":{"email":[],"linkedin":[],"twitter":[],"facebook":[],"instagram":[]}},"created":"2026-05-14 06:26:31","updated":"2026-08-07 00:24:04","seo_analyzer_scan_date":null,"focus_keyword":null,"additional_keywords":null,"truseo_locale":null},"jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/Pawia8-gs","jetpack-related-posts":[],"_links":{"self":[{"href":"https:\/\/airliebeachpartybus.com.au\/index.php?rest_route=\/wp\/v2\/pages\/1020","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/airliebeachpartybus.com.au\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/airliebeachpartybus.com.au\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/airliebeachpartybus.com.au\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/airliebeachpartybus.com.au\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=1020"}],"version-history":[{"count":3,"href":"https:\/\/airliebeachpartybus.com.au\/index.php?rest_route=\/wp\/v2\/pages\/1020\/revisions"}],"predecessor-version":[{"id":1057,"href":"https:\/\/airliebeachpartybus.com.au\/index.php?rest_route=\/wp\/v2\/pages\/1020\/revisions\/1057"}],"wp:attachment":[{"href":"https:\/\/airliebeachpartybus.com.au\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=1020"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}