<?php
declare(strict_types=1);

if (!defined('ABSPATH')) { exit; }

/**
 * Booking Block element renderer (shared).
 *
 * Phase 1, step 30. The editor's version was the modern, polished
 * implementation — three-step booking flow with form validation, an
 * empty-state UI directing builders to add services, configurable
 * styling vars passed to the inline engine, and postMessage hooks so
 * clicking inside the booking widget in the builder keeps the parent
 * element selected.
 *
 * The frontend's version (94 lines) was a stripped-down sibling that
 * lacked the empty-state UI, the validation, the postMessage glue, and
 * shipped with a simpler calendar that didn't visualise the selected
 * day. This refactor adopts the editor's renderer as canonical; the
 * live frontend gets the upgrade.
 *
 * Pre-extraction differences resolved:
 *
 *   - DB driver: frontend uses `\Instinctor\Core\DB`, editor uses
 *     `$wpdb`. Unified — DB class when available, else $wpdb.
 *
 *   - Empty state: frontend silently `break`s when no services exist
 *     (block disappears). Editor renders a "No services found" card
 *     with an "Add Service ›" CTA pointing into the admin. Editor-only
 *     UI is preserved here; frontend returns '' so the block hides.
 *
 *   - AJAX URL: editor handled standalone + WP; frontend never did.
 *     Editor's branching adopted.
 *
 *   - Visibility badge: editor-only helper; wrapped in function_exists.
 *
 *   - Element id: previously `$id` (editor) vs `$el['id']` (frontend).
 *     Standardised to `$el['id']` since both contexts now pass the
 *     element array.
 *
 *   - Inline JS `_keepSelected` postMessage: editor-specific but inert
 *     on frontend (the `window.impulseIsPreview` flag is only set in
 *     the builder iframe), so it ships in both contexts unchanged.
 *
 * @sideeffect wp_add_inline_style('instinctor-page-inline', ...) for
 *             per-instance hover overrides on the calendar / slot grid.
 */
function impulse_element_render_booking($el, array $settings, string $context): string {
    if (is_object($el)) $el = (array) $el;
    $isEditor = ($context === 'editor');

    // Personalized-demo signals (set by DemoService on the per-token page). When
    // demo_services are present the widget seeds from settings and runs fully
    // client-side — slots/staff/submit never hit the host store's REST/AJAX.
    $bkDemoVertical = (string) ($settings['demo_vertical'] ?? '');
    $bkDemo         = (!empty($settings['demo_services']) && is_array($settings['demo_services']));
    $bkDemoStaff    = (!empty($settings['demo_staff']) && is_array($settings['demo_staff'])) ? $settings['demo_staff'] : [];

    $rawId = (string) ($el['id'] ?? uniqid());
    $rawId = preg_replace('/[^a-z0-9_-]/i', '', $rawId);
    $bkId  = 'ibk-' . $rawId;

    // Free-publish tenants can't take live bookings (Pro), so the widget becomes an
    // appointment REQUEST form: it captures the visitor as a lead and the owner
    // confirms by hand. Detected once here, reused for labels + the hint below.
    $bkOwnerCtx = class_exists('\\Instinctor\\Core\\Tenant') ? \Instinctor\Core\Tenant::currentOwnerId() : null;
    $bkIsRequestMode = ($bkOwnerCtx && class_exists('\\Instinctor\\Core\\TenantPlan') && !\Instinctor\Core\TenantPlan::bookingAllowed($bkOwnerCtx));

    // Store-type-aware defaults: salons read "Appointment", others "Service".
    $bkDefaultTitle = 'Book a Service';
    
    $bkDefaultBtn   = \Instinctor\Core\StoreFeatures::bookingCtaLabel();
    $bkTitle      = (string) ($settings['title']        ?? $bkDefaultTitle);
    $bkBtnText    = (string) ($settings['button_text']  ?? $bkDefaultBtn);
    if ($bkIsRequestMode) { $bkTitle = 'Request an Appointment'; $bkBtnText = 'Request appointment'; }
    $bkBtnBg      = esc_attr((string) ($settings['button_bg']    ?? '#1d2327'));
    $bkBtnColor   = esc_attr((string) ($settings['button_color'] ?? '#ffffff'));
    $bkFieldBg    = esc_attr((string) ($settings['field_bg']     ?? '#ffffff'));
    $bkFieldBd    = esc_attr((string) ($settings['field_border'] ?? '#e0e0e0'));
    $bkFieldR     = intval($settings['field_radius'] ?? 8);
    $bkBtnR       = intval($settings['button_radius'] ?? 8);
    $bkMaxW       = intval($settings['max_width']    ?? 560);
    $bkTitleSize  = intval($settings['title_size']   ?? 22);
    // Optional card surface — lets the widget read as a floating panel over a
    // dark/animated screen background (the block's text is dark by design, so
    // it needs a light surface to stay readable). Empty = transparent (legacy).
    $bkCardBg     = trim((string) ($settings['card_bg'] ?? ''));

    // ── Fetch services ──────────────────────────────────────────────────
    // Builder-demo hook: when $GLOBALS['impulse_bd_services_override'] is
    // set (marketing-page Booking demo on instinctor.com), use that array
    // verbatim and skip the DB query. The global is unset by the caller
    // after render so this branch never runs in production.
    if (!empty($GLOBALS['impulse_bd_services_override'])
        && is_array($GLOBALS['impulse_bd_services_override'])) {
        $bkServices = $GLOBALS['impulse_bd_services_override'];
    } elseif (!empty($GLOBALS['impulse_is_demo']) && $bkDemo) {
        // Personalized demo (/demo/:token): the per-token page carries the
        // generated salon services in-settings; no host-store query.
        $bkServices = $settings['demo_services'];
    } else {
        // Live/published: the OWNER's own active services, scoped to this tenant so
        // each salon shows only theirs (the old unscoped query showed one global list
        // across every tenant). Falls back to the element's demo_services when a
        // freshly-published owner has not added real services yet.
        $bkServices = [];
        if (class_exists('\\Instinctor\\Core\\DB')) {
            $tbl   = \Instinctor\Core\DB::prefix() . 'instinctor_services';
            $scope = class_exists('\\Instinctor\\Core\\Tenant') ? \Instinctor\Core\Tenant::scopeSql('owner_id') : '1=1';
            $bkServices = \Instinctor\Core\DB::get_results(
                "SELECT id,name,duration,price,color,capacity,deposit FROM {$tbl} WHERE status='active' AND {$scope} ORDER BY sort_order ASC, name ASC",
                ARRAY_A
            ) ?: [];
        } else {
            global $wpdb;
            if (isset($wpdb)) {
                $bkServices = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- plugin's own custom table; query is prepared; per-request render value, not object-cacheable
                    "SELECT id,name,duration,price,color,capacity,deposit FROM {$wpdb->prefix}instinctor_services WHERE status='active' ORDER BY sort_order ASC, name ASC",
                    ARRAY_A
                ) ?: [];
            }
        }
        if (empty($bkServices) && $bkDemo) {
            $bkServices = $settings['demo_services'];   // instant-publish fallback
        }
    }

    // Salon staff picker flag. Generic default is off; the real check is stripped
    // from the free build, where $bkIsSalon (and the JS _isSalon) is always false.
    $bkIsSalon = false;
    

    // ── Empty state ─────────────────────────────────────────────────────
    if (empty($bkServices)) {
        if (!$isEditor) return '';
        $servicesUrl = (defined('INSTINCTOR_ENV') && INSTINCTOR_ENV === 'standalone')
            ? '/instinctor/store?section=bookings'
            : (function_exists('admin_url') ? esc_url(admin_url('admin.php?page=instinctor#store/bookings')) : '#');
        ob_start();
        $visBadge = function_exists('impulse_visibility_badge') ? impulse_visibility_badge($settings) : '';
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- impulse_visibility_badge() returns the plugin's own curated badge markup (no user input).
        if ($visBadge) echo '<div style="position:relative">' . wp_kses($visBadge, impulse_svg_allowed());
        echo '<div style="max-width:' . esc_attr((string) $bkMaxW) . 'px;margin:0 auto;padding:32px 24px;box-sizing:border-box;border:1px dashed #3a3f44;border-radius:8px;background:#1d2327;text-align:center">';
        echo '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#555" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="margin-bottom:12px"><path d="M8 2v4"/><path d="M16 2v4"/><rect width="18" height="18" x="3" y="4" rx="2"/><path d="M3 10h18"/><path d="M8 14h.01"/><path d="M12 14h.01"/><path d="M16 14h.01"/><path d="M8 18h.01"/><path d="M12 18h.01"/><path d="M16 18h.01"/></svg>';
        echo '<div style="font-size:13px;font-weight:600;color:#e0e0e0;margin-bottom:6px">No services found</div>';
        echo '<div style="font-size:12px;color:#888;margin-bottom:16px">Add at least one active service in Store &rsaquo; Bookings &rsaquo; Services before this block will work.</div>';
        echo '<button type="button" class="impulse-goto-add-service" data-url="' . esc_attr($servicesUrl) . '" style="display:inline-block;padding:7px 16px;background:#2a2e33;border:1px solid #3a3f44;border-radius:4px;font-size:12px;color:#e0e0e0;cursor:pointer;pointer-events:auto;position:relative;z-index:10">Add Service &rsaquo;</button>';
        echo '</div>';
        if ($visBadge) echo '</div>';
        return (string) ob_get_clean();
    }

    // ── Resolve URLs + endpoints ────────────────────────────────────────
    $bkAjax = (defined('INSTINCTOR_ENV') && INSTINCTOR_ENV === 'standalone')
        ? esc_url(rtrim(defined('INSTINCTOR_STANDALONE_URL') ? INSTINCTOR_STANDALONE_URL : '', '/') . '/admin/admin-ajax.php')
        : (function_exists('admin_url') ? esc_url(admin_url('admin-ajax.php')) : '/admin/admin-ajax.php');
    $bkRestBase = class_exists('\\Instinctor\\Core\\Router')
        ? \Instinctor\Core\Router::restBase()
        : '/wp-json/instinctor/v1';
    $bkNonce = class_exists('\\Instinctor\\Core\\Session')
        ? \Instinctor\Core\Session::token('instinctor_frontend')
        : (function_exists('wp_create_nonce') ? wp_create_nonce('instinctor_frontend') : '');

    $bkSvcsJs = function_exists('wp_json_encode') ? wp_json_encode($bkServices) : wp_json_encode($bkServices);

    $bkFStyle   = 'width:100%;padding:10px 14px;background:' . $bkFieldBg . ';border:1px solid ' . $bkFieldBd
                . ';border-radius:' . $bkFieldR . 'px;font-size:14px;color:#1d2327;outline:none;box-sizing:border-box;font-family:inherit';
    $bkBtnStyle = 'width:100%;padding:12px;background:' . $bkBtnBg . ';color:' . $bkBtnColor
                . ';border:none;border-radius:' . $bkBtnR . 'px;font-size:15px;font-weight:600;cursor:pointer;font-family:inherit';

    $currencySymbol = function_exists('get_option') ? (string) get_option('instinctor_currency_symbol', '$') : '$';

    // ── Booking deposits (upfront, optional) ────────────────────────────
    // Deposits engage only when Stripe is connected, payment is required, and at
    // least one active service carries a deposit. Stripe.js is loaded only then.
    $bkStripePk   = class_exists('\\Instinctor\\Core\\Config') ? (string) \Instinctor\Core\Config::get('instinctor_stripe_publishable_key', '') : '';
    $bkRequirePay = class_exists('\\Instinctor\\Services\\BookingService')
        ? !empty(\Instinctor\Services\BookingService::instance()->getSettings()['require_payment'])
        : true;
    $bkAnyDeposit = false;
    foreach ($bkServices as $sv) { if ((float) ($sv['deposit'] ?? 0) > 0) { $bkAnyDeposit = true; break; } }
    $bkUseDeposits = (!$isEditor && $bkStripePk !== '' && $bkRequirePay && $bkAnyDeposit);
    if ($bkUseDeposits && function_exists('wp_enqueue_script')) {
        // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Stripe's versioned CDN URL; passing a version string would duplicate the version in the query param
        wp_enqueue_script('stripe-js', 'https://js.stripe.com/v3/', [], null, true);
    }

    ob_start();

    $visBadge = $isEditor && function_exists('impulse_visibility_badge') ? impulse_visibility_badge($settings) : '';
    if ($visBadge) echo '<div style="position:relative">' . wp_kses($visBadge, impulse_svg_allowed());

    // Calendar / slot hover styling — same rule for every booking block on
    // every page (CSS consolidation pass, post-step-33). Pre-consolidation
    // this emitted two `#bkId .ibk-cal-grid …` and `#bkId .ibk-slots-grid …`
    // rules per booking block; the body of each rule was identical, so a
    // single class-based pair covers all instances. Singleton-gated by a
    // GLOBALS flag so the rule emits at most once per page render.
    if (empty($GLOBALS['impulse_booking_block_css_emitted']) && function_exists('wp_add_inline_style')) {
        $GLOBALS['impulse_booking_block_css_emitted'] = true;
        wp_add_inline_style('instinctor-page-inline',
            '.impulse-booking-block .ibk-cal-grid button:not([disabled]):not(.ibk-selected-day):hover{background:#f0f0f0!important;border-color:#ddd!important}'
          . '.impulse-booking-block .ibk-slots-grid button:not([disabled]):not(.ibk-selected-slot):hover{background:#f0f0f0!important;border-color:#ddd!important}'
          . '.impulse-booking-block .ibk-staff-card:hover{opacity:0.8}'
          // Custom service/party dropdown (replaces the native <select>; native OS
          // popups can't be styled and break the greyscale rule).
          . '.ibk-dd{position:relative}'
          . '.ibk-dd-tg svg{transition:transform .18s ease-in-out}'
          . '.ibk-dd-tg[aria-expanded="true"] svg{transform:rotate(180deg)}'
          . '.ibk-dd-ls{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:60;max-height:260px;overflow-y:auto;'
          .   'background:#fff;border:1px solid ' . $bkFieldBd . ';border-radius:' . intval($bkFieldR) . 'px;'
          .   'box-shadow:0 10px 30px rgba(0,0,0,.16);padding:4px 0}'
          . '.ibk-dd-op{padding:7px 14px;cursor:pointer;font-size:14px;color:#1d2327;line-height:1.35;transition:background .12s ease-in-out}'
          . '.ibk-dd-op:hover{background:#1d2327;color:#fff}'
        );
    }

    // width:100% is required — margin:0 auto on a flex child cancels align-self:stretch
    // (CSS Flexbox spec), so without it the card shrinks to its calendar's intrinsic
    // width and never fills the column (tiny on mobile, cramped on desktop). With it the
    // card fills up to max-width, so mobile gets a full-width calendar.
    $bkCardStyle = 'width:100%;max-width:' . $bkMaxW . 'px;margin:0 auto;padding:24px;box-sizing:border-box';
    if ($bkCardBg !== '') {
        $bkCardStyle .= ';background:' . esc_attr($bkCardBg) . ';color:#1d2327;border-radius:14px;box-shadow:0 10px 40px rgba(0,0,0,0.45)';
    }
    echo '<div id="' . esc_attr($bkId) . '" class="impulse-booking-block" style="' . esc_attr($bkCardStyle) . '">';
    echo '<h2 style="margin:0 0 20px;font-size:' . esc_attr((string) $bkTitleSize) . 'px;color:#1d2327">' . esc_html($bkTitle) . '</h2>';
    // Free-tier request mode: a short hint that this captures a request the owner
    // confirms (so a submit that isn't an instant booking doesn't surprise anyone).
    if ($bkIsRequestMode) {
        echo '<div class="ibk-teaser" style="display:flex;align-items:center;gap:8px;background:#faf4ec;border:1px solid #efe1d0;color:#8a6a4a;font-size:12.5px;font-weight:600;padding:9px 12px;border-radius:8px;margin-bottom:16px">'
           . '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/></svg>'
           . 'Pick a time and we will confirm your appointment.</div>';
    }
    // ── Free tier: lighter REQUEST form (no live calendar/slots) ─────────
    // The full calendar implies real-time availability we don't have on free, so a
    // plain preferred date + time is more honest for a request the owner confirms.
    // Self-contained (its own submit) so it skips the whole calendar/slots engine.
    if ($bkIsRequestMode) {
        echo '<form class="ibk-req" style="display:flex;flex-direction:column;gap:12px">';
        echo '<select class="ibk-req-svc" style="' . esc_attr($bkFStyle) . '"><option value="">Choose a service&hellip;</option>';
        foreach ($bkServices as $sv) {
            echo '<option value="' . esc_attr((string) intval($sv['id'])) . '">' . esc_html((string) ($sv['name'] ?? '')) . '</option>';
        }
        echo '</select>';
        echo '<div style="display:flex;gap:8px">';
        echo '<input type="date" class="ibk-req-date" aria-label="Preferred date" style="' . esc_attr($bkFStyle) . ';flex:1;min-width:0">';
        echo '<select class="ibk-req-time" aria-label="Preferred time" style="' . esc_attr($bkFStyle) . ';flex:1;min-width:0"><option value="">Preferred time&hellip;</option>';
        for ($h = 8; $h < 20; $h++) {
            foreach ([0, 30] as $m) {
                $t    = sprintf('%02d:%02d', $h, $m);
                $h12  = ($h % 12) ?: 12;
                $disp = $h12 . ':' . sprintf('%02d', $m) . ' ' . ($h >= 12 ? 'PM' : 'AM');
                echo '<option value="' . esc_attr($t) . '">' . esc_html($disp) . '</option>';
            }
        }
        echo '</select></div>';
        echo '<input type="text" class="ibk-name" placeholder="Your name *" style="' . esc_attr($bkFStyle) . '">';
        // Phone only, no email (Sergei 2026-07-26). A salon client books by phone and
        // expects a text back; nobody watches their inbox for a haircut. Asking for an
        // address they will not check cost a field and gained nothing.
        echo '<input type="tel" class="ibk-phone" placeholder="Phone *" style="' . esc_attr($bkFStyle) . '">';
    // only when the value actually changes so the caret is left alone otherwise.
    $phoneFmtJs = '(function(){'
        . 'if(window.__ibkPhoneFmt)return;window.__ibkPhoneFmt=1;'
        . 'function f(v){var d=(v||"").replace(/[^0-9]/g,"").slice(0,10);'
        . 'if(d.length<4)return d;'
        . 'if(d.length<7)return "("+d.slice(0,3)+") "+d.slice(3);'
        . 'return "("+d.slice(0,3)+") "+d.slice(3,6)+"-"+d.slice(6);}'
        . 'document.addEventListener("input",function(e){'
        . 'var t=e.target;if(!t||!t.classList||!t.classList.contains("ibk-phone"))return;'
        . 'var atEnd=t.selectionStart===t.value.length,out=f(t.value);'
        . 'if(out===t.value)return;'
        . 't.value=out;if(atEnd){try{t.setSelectionRange(out.length,out.length);}catch(err){}}'
        . '},true);'
        . 'document.addEventListener("keydown",function(e){'
        . 'var t=e.target;if(!t||!t.classList||!t.classList.contains("ibk-phone"))return;'
        . 'if(e.key==="Backspace"&&/[^0-9]$/.test(t.value)){'
        . 't.value=t.value.replace(/[^0-9]*$/,"");}'   // eat the punctuation with one press
        . '},true);'
        . '})();';
    // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static inline engine, no interpolation.
    echo '<script>' . $phoneFmtJs . '</script>';
        echo '<textarea class="ibk-note" placeholder="Anything we should know? (optional)" rows="2" style="' . esc_attr($bkFStyle) . ';resize:vertical"></textarea>';
        echo '<button type="submit" class="ibk-req-btn" style="' . esc_attr($bkBtnStyle) . '">' . esc_html($bkBtnText) . '</button>';
        echo '<div class="ibk-req-msg" role="alert" style="font-size:13px;min-height:16px;color:#b13636;line-height:1.4"></div>';
        echo '</form>';
        echo '<div class="ibk-req-done" style="display:none;text-align:center;padding:16px 0">'
           . '<div style="font-size:40px;margin-bottom:10px;color:#1d2327">&#x2713;</div>'
           . '<div style="font-size:16px;font-weight:700;color:#1d2327">Request received</div>'
           . '<div class="ibk-req-done-detail" style="font-size:13px;color:#888;margin-top:6px;line-height:1.5"></div></div>';
        echo '</div>'; // .impulse-booking-block
        if ($visBadge) echo '</div>';

        // Type digits, get (215) 555-0148. Nobody should be punching brackets and
        // dashes on a phone keypad, and a consistently formatted number is also what
        // makes the customer lookup match on a repeat visit (Sergei 2026-07-26).
        // Delegated on the document so it covers both booking forms, and it rewrites
        // only when the value actually changes so the caret is left alone otherwise.
        $phoneFmtJs = '(function(){'
            . 'if(window.__ibkPhoneFmt)return;window.__ibkPhoneFmt=1;'
            . 'function f(v){var d=(v||"").replace(/[^0-9]/g,"").slice(0,10);'
            . 'if(d.length<4)return d;'
            . 'if(d.length<7)return "("+d.slice(0,3)+") "+d.slice(3);'
            . 'return "("+d.slice(0,3)+") "+d.slice(3,6)+"-"+d.slice(6);}'
            . 'document.addEventListener("input",function(e){'
            . 'var t=e.target;if(!t||!t.classList||!t.classList.contains("ibk-phone"))return;'
            . 'var atEnd=t.selectionStart===t.value.length,out=f(t.value);'
            . 'if(out===t.value)return;'
            . 't.value=out;if(atEnd){try{t.setSelectionRange(out.length,out.length);}catch(err){}}'
            . '},true);'
            . 'document.addEventListener("keydown",function(e){'
            . 'var t=e.target;if(!t||!t.classList||!t.classList.contains("ibk-phone"))return;'
            . 'if(e.key==="Backspace"&&/[^0-9]$/.test(t.value)){'
            . 't.value=t.value.replace(/[^0-9]*$/,"");}'   // eat the punctuation with one press
            . '},true);'
            . '})();';
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- static inline engine, no interpolation.
        echo '<script>' . $phoneFmtJs . '</script>';

        $reqJs = '(function(){'
            . 'var w=document.getElementById(' . wp_json_encode($bkId) . ');if(!w)return;'
            . 'var form=w.querySelector(".ibk-req");if(!form)return;'
            . 'var done=w.querySelector(".ibk-req-done"),msg=w.querySelector(".ibk-req-msg"),btn=w.querySelector(".ibk-req-btn");'
            . 'var AJAX=' . wp_json_encode($bkAjax) . ',NONCE=' . wp_json_encode((string) $bkNonce) . ';'
            . 'var di=w.querySelector(".ibk-req-date");if(di){var d=new Date();d.setDate(d.getDate()+1);di.min=d.getFullYear()+"-"+("0"+(d.getMonth()+1)).slice(-2)+"-"+("0"+d.getDate()).slice(-2);}'
            . 'var busy=false;'
            . 'form.addEventListener("submit",function(e){e.preventDefault();if(busy)return;'
            . 'var svc=w.querySelector(".ibk-req-svc").value,date=w.querySelector(".ibk-req-date").value,time=w.querySelector(".ibk-req-time").value;'
            . 'var nm=w.querySelector(".ibk-name").value.trim(),ph=w.querySelector(".ibk-phone").value.trim(),nt=w.querySelector(".ibk-note").value.trim();'
            . 'if(!svc){msg.textContent="Please choose a service.";return;}'
            . 'if(!date){msg.textContent="Please pick a preferred date.";return;}'
            . 'if(!time){msg.textContent="Please pick a preferred time.";return;}'
            . 'if(!nm){msg.textContent="Please enter your name.";return;}'
            . 'if(ph.replace(/[^0-9]/g,"").length<10){msg.textContent="Please enter your phone number so we can confirm.";return;}'
            . 'busy=true;btn.disabled=true;var bt=btn.textContent;btn.textContent="Sending…";msg.textContent="";'
            . 'var fd=new FormData();fd.append("action","instinctor_public_book");fd.append("nonce",NONCE);fd.append("service_id",svc);fd.append("booking_date",date);fd.append("booking_time",time);fd.append("customer_name",nm);fd.append("customer_phone",ph);fd.append("note",nt);try{var _ft=localStorage.getItem("instinctor_ft");if(_ft)fd.append("attribution",_ft);}catch(e){}'
            . 'var xhr=new XMLHttpRequest();xhr.open("POST",AJAX);'
            . 'xhr.onload=function(){var res;try{res=JSON.parse(xhr.responseText);}catch(e){busy=false;btn.disabled=false;btn.textContent=bt;msg.textContent="Connection error. Please try again.";return;}'
            // The teaser sits OUTSIDE the form, so hiding the form left "Pick a time and
            // we will confirm your appointment" standing above the confirmation, still
            // asking for something the visitor had just done (Sergei 2026-07-27).
            . 'if(res.success){w.querySelector(".ibk-req-done-detail").textContent=(res.data&&res.data.message)?res.data.message:"We will contact you to confirm.";form.style.display="none";var tz=w.querySelector(".ibk-teaser");if(tz)tz.style.display="none";done.style.display="block";}'
            . 'else{busy=false;btn.disabled=false;btn.textContent=bt;msg.textContent=(res.data&&res.data.message)?res.data.message:"Something went wrong. Please try again.";}};'
            . 'xhr.onerror=function(){busy=false;btn.disabled=false;btn.textContent=bt;msg.textContent="Network error. Please try again.";};'
            . 'xhr.send(fd);'
            . '});})();';
        // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $reqJs is a static inline engine; every interpolated value above is esc_js()/wp_json_encode()/intval()-composed.
        echo '<script>' . $reqJs . '</script>';
        return (string) ob_get_clean();
    }

    echo '<div class="ibk-toast" style="display:none;background:#1d2327;color:#fff;font-size:13px;padding:10px 14px;border-radius:6px;margin-bottom:12px"></div>';

    // ── Step 1: service + date/time ─────────────────────────────────────
    echo '<div class="ibk-step ibk-step1" data-step="1">';
    echo '<div style="display:flex;flex-direction:column;gap:12px">';

    
    // The picker earns its place only when there is a choice to make. A solo
    // instructor with one lesson type gets a form that just books it, and the
    // panel checkbox lets an owner collapse a longer list to its first entry
    // (absent = on; the panel writes '' for unchecked). The JS already handles a
    // non-select .ibk-service: it reads the value and takes _svcs[0] for the name.
    $bkShowPicker = !isset($settings['show_service_select']) || (string) $settings['show_service_select'] === '1';
    if (!empty($bkServices) && (count($bkServices) === 1 || !$bkShowPicker)) {
        $sv0 = $bkServices[0];
        echo '<input type="hidden" class="ibk-service" value="' . esc_attr((string) intval($sv0['id'] ?? 0)) . '"'
            . ' data-duration="' . esc_attr((string) intval($sv0['duration'] ?? 0)) . '"'
            . ' data-price="' . esc_attr((string) (float) ($sv0['price'] ?? 0)) . '">';
    } else {
    echo '<select class="ibk-service" style="' . esc_attr($bkFStyle) . '"><option value="">Select service…</option>';
    foreach ($bkServices as $sv) {
        $svId       = intval($sv['id']);
        $svName     = (string) ($sv['name'] ?? '');
        $svDuration = intval($sv['duration'] ?? 0);
        $svPrice    = (float) ($sv['price'] ?? 0);
        // Service name only in the picker — the price is noise here (and its "— $140.00"
        // suffix forced two-line rows / big gaps). Price stays in data-price for the
        // deposit/summary logic. (Sergei 2026-07-14)
        echo '<option value="' . esc_attr((string) $svId) . '" data-duration="' . esc_attr((string) $svDuration) . '" data-price="' . esc_attr((string) $svPrice) . '">' . esc_html($svName) . '</option>';
    }
    echo '</select>';
    }

    

    echo '<div class="ibk-calendar-wrap">';
    echo '<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px">';
    echo '<button type="button" class="ibk-prev-month" style="background:none;border:none;cursor:pointer;padding:4px 8px;font-size:18px;color:#888">&#8249;</button>';
    echo '<span class="ibk-month-label" style="font-size:14px;font-weight:600;color:#1d2327"></span>';
    echo '<button type="button" class="ibk-next-month" style="background:none;border:none;cursor:pointer;padding:4px 8px;font-size:18px;color:#888">&#8250;</button>';
    echo '</div>';
    echo '<div class="ibk-cal-grid" style="display:grid;grid-template-columns:repeat(7,1fr);gap:4px;text-align:center"></div>';
    echo '</div>';

    echo '<div class="ibk-slots-wrap" style="display:none">';
    echo '<div style="font-size:13px;color:#888;margin-bottom:6px">Available times</div>';
    echo '<div class="ibk-slots-grid" style="display:grid;grid-template-columns:repeat(4,1fr);gap:6px"></div>';
    echo '</div>';

    echo '<button type="button" class="ibk-next-btn" style="' . esc_attr($bkBtnStyle) . ';opacity:0.45">Next &rsaquo;</button>';
    echo '</div></div>'; // gap + step1

    // ── Step 2: customer details ────────────────────────────────────────
    echo '<div class="ibk-step ibk-step2" data-step="2" style="display:none">';
    echo '<div style="display:flex;flex-direction:column;gap:12px">';
    echo '<div style="font-size:13px;color:#888;margin-bottom:-4px" class="ibk-booking-summary"></div>';
    echo '<input type="text" class="ibk-name" placeholder="Your name *" style="' . esc_attr($bkFStyle) . '">';
    echo '<input type="tel" class="ibk-phone" placeholder="Phone *" style="' . esc_attr($bkFStyle) . '">';
    echo '<textarea class="ibk-note" placeholder="Note (optional)" rows="3" style="' . esc_attr($bkFStyle) . ';resize:vertical"></textarea>';
    // Standing appointment. Off by default: most trades book one visit at a time,
    // and offering to repeat where the owner cannot honour it is worse than not
    // asking. 'once' carries a real value because the styled dropdown drops
    // empty-valued options, so a blank default could be chosen but never undone.
    if (!empty($settings['allow_repeat']) && (string) $settings['allow_repeat'] === '1') {
        echo '<select class="ibk-repeat" aria-label="Repeat this appointment" style="' . esc_attr($bkFStyle) . '">';
        echo '<option value="once" selected>One time only</option>';
        echo '<option value="week:1">Every week</option>';
        echo '<option value="week:2">Every 2 weeks</option>';
        echo '<option value="month:1">Every month</option>';
        echo '</select>';
    }
    // Deposit payment mount point — revealed by the widget JS only when the
    // selected service requires a deposit. Stripe Payment Element mounts here.
    echo '<div class="ibk-pay-wrap" style="display:none"><div class="ibk-pay-label" style="font-size:13px;color:#1d2327;margin-bottom:8px;font-weight:600"></div><div class="ibk-pay-element"></div></div>';
    echo '<div style="display:flex;gap:10px">';
    echo '<button type="button" class="ibk-back-btn" style="flex:1;padding:12px;background:transparent;color:#888;border:1px solid #e0e0e0;border-radius:' . esc_attr((string) $bkBtnR) . 'px;font-size:14px;cursor:pointer;font-family:inherit">Back</button>';
    echo '<button type="button" class="ibk-submit-btn" style="flex:3;' . esc_attr($bkBtnStyle) . '">' . esc_html($bkBtnText) . '</button>';
    echo '</div>';
    echo '</div></div>'; // gap + step2

    // ── Step 3: confirmation ────────────────────────────────────────────
    echo '<div class="ibk-step ibk-step3" data-step="3" style="display:none">';
    echo '<div style="text-align:center;padding:24px 0">';
    echo '<div style="font-size:40px;margin-bottom:12px">&#x2713;</div>';
    echo '<div class="ibk-confirm-msg" style="font-size:16px;color:#1d2327;font-weight:600"></div>';
    echo '<div class="ibk-confirm-detail" style="font-size:13px;color:#888;margin-top:6px"></div>';
    echo '</div></div>';

    echo '</div>'; // .impulse-booking-block

    if ($visBadge) echo '</div>';

    // ── Inline engine ───────────────────────────────────────────────────
    $jsonEnc = function_exists('wp_json_encode') ? 'wp_json_encode' : 'json_encode';
    ob_start();
    echo '(function(){';
    echo 'var _wrap=document.getElementById(' . wp_json_encode($bkId) . ');if(!_wrap)return;';
    echo 'var _svcs=' . wp_json_encode($bkServices) . ';';
    echo 'var _ajax=' . wp_json_encode($bkAjax) . ';';
    echo 'var _nonce=' . wp_json_encode((string) $bkNonce) . ';';
    echo 'var _ajaxNonce=' . wp_json_encode((string) $bkNonce) . ';';
    echo 'var _restBase=' . wp_json_encode((string) $bkRestBase) . ';';
    echo 'var _fldBg=' . wp_json_encode($bkFieldBg) . ';';
    echo 'var _fldBd=' . wp_json_encode($bkFieldBd) . ';';
    echo 'var _fldR=' . wp_json_encode($bkFieldR) . ';';
    echo 'var _btnBg=' . wp_json_encode($bkBtnBg) . ';';
    echo 'var _btnR=' . wp_json_encode($bkBtnR) . ';';
    echo 'var _btnColor=' . wp_json_encode($bkBtnColor) . ';';
    echo 'var _useDeposits=' . ($bkUseDeposits ? 'true' : 'false') . ';';
    echo 'var _cur=' . wp_json_encode($currencySymbol) . ';';
    echo 'var _isSalon=' . ($bkIsSalon ? 'true' : 'false') . ';';
    echo 'var _bkDemo=' . ($bkDemo ? 'true' : 'false') . ';';
    echo 'var _bkDemoStaff=' . (function_exists('wp_json_encode') ? wp_json_encode($bkDemoStaff) : json_encode($bkDemoStaff)) . ';';
    echo "
var _state={svcId:0,svcName:'',staffId:0,staffName:'Any available',date:'',time:'',year:new Date().getFullYear(),month:new Date().getMonth()};
var _payReady=false,_stripe=null,_elements=null,_piId='',_payLabel='';
var \$=function(sel){return _wrap.querySelector(sel);};
function fmtDate(y,m,d){return y+'-'+(m+1<10?'0':'')+(m+1)+'-'+(d<10?'0':'')+d;}
function fmtDisplay(y,m,d){var ms=['January','February','March','April','May','June','July','August','September','October','November','December'];return ms[m]+' '+d+', '+y;}
function fmtTime(t){var p=t.split(':');var h=parseInt(p[0]);var suf=h>=12?'PM':'AM';h=h%12||12;return h+':'+p[1]+' '+suf;}
function renderCalendar(){
  var g=\$('.ibk-cal-grid'),lbl=\$('.ibk-month-label');
  var ms=['January','February','March','April','May','June','July','August','September','October','November','December'];
  var days=['Su','Mo','Tu','We','Th','Fr','Sa'];
  lbl.textContent=ms[_state.month]+' '+_state.year;
  g.innerHTML='';
  days.forEach(function(d){var h=document.createElement('div');h.textContent=d;h.style.cssText='font-size:11px;color:#888;padding:4px 0';g.appendChild(h);});
  var first=new Date(_state.year,_state.month,1).getDay();
  for(var i=0;i<first;i++){g.appendChild(document.createElement('div'));}
  var dim=new Date(_state.year,_state.month+1,0).getDate();
  var today=new Date();today.setHours(0,0,0,0);
  for(var d=1;d<=dim;d++){
    var dt=new Date(_state.year,_state.month,d);
    var btn=document.createElement('button');
    btn.type='button';btn.textContent=d;
    var past=dt<today;
    var dateStr=fmtDate(_state.year,_state.month,d);
    var isSelected=(!past&&dateStr===_state.date);
    if(isSelected)btn.classList.add('ibk-selected-day');
    btn.style.border='1px solid '+(past?'transparent':isSelected?_btnBg:_fldBd);
    btn.style.background=past?'transparent':isSelected?_btnBg:_fldBg;
    btn.style.borderRadius=_fldR+'px';
    btn.style.padding='6px 2px';
    btn.style.fontSize='13px';
    btn.style.lineHeight='1';
    btn.style.cursor=past?'default':'pointer';
    btn.style.color=past?'#ccc':isSelected?_btnColor:'#1d2327';
    btn.style.fontFamily='inherit';
    btn.style.boxSizing='border-box';
    btn.style.width='100%';
    if(!past){
      btn.addEventListener('click',function(ds){return function(e){_keepSelected(e);_state.date=ds;_state.time='';\$('.ibk-next-btn').style.opacity='0.45';renderCalendar();loadSlots();};}(dateStr));
    }
    g.appendChild(btn);
  }
}
function renderDemoSlots(){
  var sw=\$('.ibk-slots-wrap'),sg=\$('.ibk-slots-grid');
  sw.style.display='block';sg.innerHTML='';
  var pr=_state.date.split('-'),dd=parseInt(pr[2]),mm=parseInt(pr[1]),yy=parseInt(pr[0]);
  var dow=new Date(yy,mm-1,dd).getDay();
  var startH=(dow===0)?11:9,endH=(dow===0)?16:19;
  var seed=dd+mm*31+(_state.svcId||0)+(_state.staffId||0)*7;
  var made=0;
  for(var h=startH;h<endH;h++){
    [0,30].forEach(function(min){
      if(h===endH-1&&min===30)return;
      var t=(h<10?'0':'')+h+':'+(min<10?'0':'')+min;
      seed=(seed*9301+49297)%233280;
      var avail=(seed/233280)>0.38;
      var b=document.createElement('button');
      b.type='button';b.textContent=fmtTime(t);
      b.style.cssText='padding:8px 4px;font-size:13px;border-radius:'+_fldR+'px;border:1px solid '+_fldBd+';background:'+(avail?_fldBg:'#f3f3f3')+';color:'+(avail?'#1d2327':'#ccc')+';cursor:'+(avail?'pointer':'default')+';font-family:inherit';
      if(avail){made++;b.addEventListener('click',(function(tt,el){return function(e){_keepSelected(e);_state.time=tt;sg.querySelectorAll('button').forEach(function(x){if(x.disabled)return;x.style.background=_fldBg;x.style.color='#1d2327';x.style.borderColor=_fldBd;});el.style.background=_btnBg;el.style.color='#fff';el.style.borderColor=_btnBg;\$('.ibk-next-btn').style.opacity='1';};})(t,b));}else{b.disabled=true;}
      sg.appendChild(b);
    });
  }
  if(!made){sg.innerHTML='<div style=\"color:#888;font-size:13px;grid-column:1/-1\">Fully booked. Try another day.</div>';}
}
function loadSlots(){
  var _pe=\$('.ibk-party');
  if(!_state.svcId||!_state.date||(_pe&&!_state.party)){return;}
  if(_bkDemo){renderDemoSlots();return;}
  var sw=\$('.ibk-slots-wrap'),sg=\$('.ibk-slots-grid');
  sw.style.display='block';sg.innerHTML='<div style=\"color:#888;font-size:13px;grid-column:1/-1\">Loading…</div>';
  var xhr=new XMLHttpRequest();
  xhr.open('GET',_restBase+'/bookings/slots?product_id='+_state.svcId+'&date='+_state.date+((_pe&&_state.party)?'&party_size='+_state.party:'')+(_state.staffId?'&staff_id='+_state.staffId:''));
  xhr.setRequestHeader('X-Instinctor-Nonce',_nonce);
  xhr.onload=function(){
    if(xhr.status!==200){sg.innerHTML='<div style=\"color:#888;font-size:13px;grid-column:1/-1\">Could not load slots (HTTP '+xhr.status+').</div>';return;}
    var data;try{data=JSON.parse(xhr.responseText);}catch(e){sg.innerHTML='<div style=\"color:#888;font-size:13px;grid-column:1/-1\">Invalid slot response.</div>';return;}
    var slots=data.slots||[];
    sg.innerHTML='';
    if(!slots.length){sg.innerHTML='<div style=\"color:#888;font-size:13px;grid-column:1/-1\">No slots available.</div>';return;}
    slots.forEach(function(s){
      var b=document.createElement('button');
      b.type='button';b.textContent=s.display;
      var active=s.time===_state.time;
      b.style.cssText='padding:8px 4px;font-size:13px;border-radius:'+_fldR+'px;border:1px solid '+(active?_btnBg:_fldBd)+';background:'+(active?_btnBg:_fldBg)+';color:'+(active?'#fff':(s.available?'#1d2327':'#ccc'))+';cursor:'+(s.available?'pointer':'default');
      if(s.available){
        b.addEventListener('click',function(t,el){return function(e){_keepSelected(e);_state.time=t;sg.querySelectorAll('button').forEach(function(x){if(x.disabled)return;x.style.background=_fldBg;x.style.color='#1d2327';x.style.borderColor=_fldBd;});el.style.background=_btnBg;el.style.color='#fff';el.style.borderColor=_btnBg;\$('.ibk-next-btn').style.opacity='1';};}(s.time,b));
      }else{b.disabled=true;}
      sg.appendChild(b);
    });
  };
  xhr.send();
}

function ibkToast(msg){var t=\$('.ibk-toast');t.textContent=msg;t.style.display='block';clearTimeout(t._timer);t._timer=setTimeout(function(){t.style.display='none';},3000);}
function showStep(n){\$('.ibk-toast').style.display='none';_wrap.querySelectorAll('.ibk-step').forEach(function(s){s.style.display='none';});_wrap.querySelector('.ibk-step'+n).style.display='block';}
var _elemId=_wrap.id.replace('ibk-','');
function _keepSelected(e){if(e)e.stopPropagation();if(window.impulseIsPreview&&window.parent&&_elemId){window.parent.postMessage({type:'impulse-select',id:_elemId,elementType:'booking'},'*');}}
var svcEl=\$('.ibk-service');
function onSvcChange(){var opt=svcEl.options?svcEl.options[svcEl.selectedIndex]:null;_state.svcId=parseInt(svcEl.value)||0;_state.svcName=opt?opt.text:'';_state.staffId=0;_state.staffName='Any available';_state.date='';_state.time='';\$('.ibk-slots-wrap').style.display='none';\$('.ibk-next-btn').style.opacity='0.45';if(_isSalon&&_state.svcId)loadStaff();renderCalendar();}
if(svcEl.tagName==='SELECT')svcEl.addEventListener('change',onSvcChange);
else{_state.svcId=parseInt(svcEl.value)||0;_state.svcName=(_svcs[0]||{}).name||'';}
var _partyEl=\$('.ibk-party');
if(_partyEl){_partyEl.addEventListener('change',function(){_state.party=parseInt(_partyEl.value)||0;_state.time='';\$('.ibk-next-btn').style.opacity='0.45';if(_state.date)loadSlots();});}
// Upgrade native <select> into a styled custom dropdown (native OS popups can't be
// styled and break the greyscale rule). Keeps the real <select> hidden as the value +
// change source, so all booking logic above is untouched.
function _ibkDD(sel){
  if(!sel||sel._dd||sel.tagName!=='SELECT')return;
  var css=sel.style.cssText;sel.style.display='none';
  var dd=document.createElement('div');dd.className='ibk-dd';
  var tg=document.createElement('button');tg.type='button';tg.className='ibk-dd-tg';tg.setAttribute('aria-haspopup','listbox');tg.setAttribute('aria-expanded','false');
  tg.style.cssText=css+';display:flex;align-items:center;justify-content:space-between;gap:8px;cursor:pointer;text-align:left;font-family:inherit';
  var vl=document.createElement('span');vl.className='ibk-dd-vl';vl.style.cssText='overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0';
  tg.appendChild(vl);
  tg.insertAdjacentHTML('beforeend','<svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"flex-shrink:0;opacity:.55\"><path d=\"M6 9l6 6 6-6\"/></svg>');
  var ls=document.createElement('div');ls.className='ibk-dd-ls';ls.setAttribute('role','listbox');ls.hidden=true;
  function sync(){var o=sel.options[sel.selectedIndex];vl.textContent=o?o.text:'';vl.style.opacity=sel.value?'1':'.55';}
  Array.prototype.forEach.call(sel.options,function(o){
    if(!o.value)return;   // skip the empty placeholder — the trigger already shows it
    var it=document.createElement('div');it.className='ibk-dd-op';it.setAttribute('role','option');it.textContent=o.text;
    it.addEventListener('click',function(){sel.value=o.value;sel.dispatchEvent(new Event('change',{bubbles:true}));sync();cls();});
    ls.appendChild(it);
  });
  sel.parentNode.insertBefore(dd,sel);dd.appendChild(tg);dd.appendChild(ls);dd.appendChild(sel);
  function opn(){ls.hidden=false;tg.setAttribute('aria-expanded','true');}
  function cls(){ls.hidden=true;tg.setAttribute('aria-expanded','false');}
  tg.addEventListener('click',function(e){e.stopPropagation();ls.hidden?opn():cls();});
  document.addEventListener('click',function(e){if(!dd.contains(e.target))cls();});
  sel._dd=dd;sync();
}
_ibkDD(svcEl);_ibkDD(_partyEl);_ibkDD(\$('.ibk-repeat'));
\$('.ibk-prev-month').addEventListener('click',function(e){_keepSelected(e);_state.month--;if(_state.month<0){_state.month=11;_state.year--;}_state.date='';_state.time='';\$('.ibk-next-btn').style.opacity='0.45';renderCalendar();});
\$('.ibk-next-month').addEventListener('click',function(e){_keepSelected(e);_state.month++;if(_state.month>11){_state.month=0;_state.year++;}_state.date='';_state.time='';\$('.ibk-next-btn').style.opacity='0.45';renderCalendar();});
\$('.ibk-next-btn').addEventListener('click',function(){if(\$('.ibk-party')&&!_state.party){ibkToast('Please choose a party size.');return;}if(!_state.svcId){ibkToast('Please select a service.');return;}if(!_state.date){ibkToast('Please select a date.');return;}if(!_state.time){ibkToast('Please select a time slot.');return;}\$('.ibk-booking-summary').textContent=_state.svcName+(_isSalon&&_state.staffId?' with '+_state.staffName:'')+' · '+fmtDisplay(_state.year,_state.month,parseInt(_state.date.split('-')[2]))+' at '+fmtTime(_state.time);_payReady=false;var _pw=\$('.ibk-pay-wrap');if(_pw)_pw.style.display='none';var _pe2=\$('.ibk-pay-element');if(_pe2)_pe2.innerHTML='';showStep(2);});
\$('.ibk-back-btn').addEventListener('click',function(){showStep(1);});
function _doBook(piId){
  if(_bkDemo){
    _wrap.querySelectorAll('.ibk-step').forEach(function(s){s.style.display='none';});
    var c=\$('.ibk-demo-confirm');
    if(!c){c=document.createElement('div');c.className='ibk-demo-confirm';c.style.cssText='text-align:center;padding:14px 4px';_wrap.appendChild(c);}
    var nm=(\$('.ibk-name')&&\$('.ibk-name').value.trim())||'';nm=nm.replace(/[<>]/g,'');
    c.innerHTML='<div style=\"width:46px;height:46px;border-radius:50%;background:'+_btnBg+';display:flex;align-items:center;justify-content:center;margin:0 auto 14px\"><svg xmlns=\"http://www.w3.org/2000/svg\" width=\"22\" height=\"22\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"#fff\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M20 6 9 17l-5-5\"/></svg></div><div style=\"font-size:17px;font-weight:700;color:#1d2327;margin-bottom:6px\">Appointment reserved'+(nm?', '+nm:'')+'.</div><div style=\"font-size:13px;color:#666;line-height:1.5;max-width:330px;margin:0 auto\">'+_state.svcName+' on '+fmtDisplay(_state.year,_state.month,parseInt(_state.date.split('-')[2]))+' at '+fmtTime(_state.time)+'. On your live site this books instantly and texts your client a confirmation.</div>';
    c.style.display='block';return;
  }
  var btn=\$('.ibk-submit-btn');btn.disabled=true;btn.textContent='Booking…';
  var fd=new FormData();
  fd.append('action','instinctor_public_book');
  fd.append('nonce',_ajaxNonce);
  fd.append('service_id',_state.svcId);
  fd.append('guest_count',_state.party||1);
  fd.append('booking_date',_state.date);
  fd.append('booking_time',_state.time);
  fd.append('customer_name',\$('.ibk-name').value.trim());
  /* no email is collected at all */
  fd.append('customer_phone',\$('.ibk-phone').value);
  fd.append('note',\$('.ibk-note').value);
  var _rep=\$('.ibk-repeat');
  if(_rep&&_rep.value&&_rep.value!=='once'){var _rp=_rep.value.split(':');fd.append('repeat_period',_rp[0]);fd.append('repeat_interval',_rp[1]||'1');}
  try{var _ft=localStorage.getItem('instinctor_ft');if(_ft)fd.append('attribution',_ft);}catch(e){}
  if(piId)fd.append('payment_intent_id',piId);
  if(_state.staffId)fd.append('staff_id',_state.staffId);
  var xhr=new XMLHttpRequest();xhr.open('POST',_ajax);
  xhr.onload=function(){
    var res;try{res=JSON.parse(xhr.responseText);}catch(e){btn.disabled=false;btn.textContent=" . wp_json_encode(esc_html($bkBtnText)) . ";ibkToast('Connection error');return;}
    if(res.success){
      var paid=res.data&&res.data.deposit_paid;
      \$('.ibk-confirm-msg').textContent=paid?'Appointment confirmed':'Request received';
      \$('.ibk-confirm-detail').textContent=_state.svcName+(_isSalon&&_state.staffId?' with '+_state.staffName:'')+' · '+fmtDisplay(_state.year,_state.month,parseInt(_state.date.split('-')[2]))+' at '+fmtTime(_state.time)+(paid?', deposit paid.':', we will confirm shortly.')+(function(){var r=\$('.ibk-repeat');return(r&&r.value&&r.value!=='once'&&r.options[r.selectedIndex])?' Repeats '+r.options[r.selectedIndex].text.toLowerCase()+'.':'';})();
      showStep(3);
    }else{
      btn.disabled=false;btn.textContent=" . wp_json_encode(esc_html($bkBtnText)) . ";
      ibkToast(res.data&&res.data.message?res.data.message:'Booking failed. Please try again.');
    }
  };
  xhr.send(fd);
}
function _confirmAndBook(){
  var btn=\$('.ibk-submit-btn');btn.disabled=true;btn.textContent='Processing…';
  _stripe.confirmPayment({elements:_elements,redirect:'if_required'}).then(function(result){
    if(result.error){btn.disabled=false;btn.textContent='Pay '+_payLabel+' & Book';ibkToast(result.error.message||'Payment failed.');return;}
    _doBook(_piId);
  });
}
\$('.ibk-submit-btn').addEventListener('click',function(){
  var name=\$('.ibk-name').value.trim(),phone=(\$('.ibk-phone').value||'').replace(/[^0-9]/g,'');
  if(!name){ibkToast('Please enter your name.');return;}
  if(phone.length<10){ibkToast('Please enter your phone number so we can confirm.');return;}
  if(_payReady){_confirmAndBook();return;}
  var svc=_svcs.filter(function(s){return parseInt(s.id)===_state.svcId;})[0];
  var dep=svc?parseFloat(svc.deposit||0):0;
  if(!_useDeposits||!dep||dep<=0||typeof Stripe==='undefined'){_doBook('');return;}
  var btn=this;btn.disabled=true;btn.textContent='…';
  var fd=new FormData();fd.append('action','instinctor_booking_deposit_intent');fd.append('nonce',_ajaxNonce);fd.append('service_id',_state.svcId);
  var xhr=new XMLHttpRequest();xhr.open('POST',_ajax);
  xhr.onload=function(){
    var res;try{res=JSON.parse(xhr.responseText);}catch(e){btn.disabled=false;btn.textContent=" . wp_json_encode(esc_html($bkBtnText)) . ";ibkToast('Connection error');return;}
    if(!res.success){btn.disabled=false;btn.textContent=" . wp_json_encode(esc_html($bkBtnText)) . ";ibkToast(res.data&&res.data.message?res.data.message:'Payment error.');return;}
    if(!res.data.deposit||res.data.deposit<=0){_doBook('');return;}
    _piId=res.data.payment_intent_id;_payLabel=_cur+Number(res.data.deposit).toFixed(2);
    try{_stripe=Stripe(res.data.pk);_elements=_stripe.elements({clientSecret:res.data.client_secret});_elements.create('payment').mount(\$('.ibk-pay-element'));}catch(e){_doBook('');return;}
    \$('.ibk-pay-label').textContent='A '+_payLabel+' deposit holds your appointment.';
    \$('.ibk-pay-wrap').style.display='block';
    _payReady=true;btn.disabled=false;btn.textContent='Pay '+_payLabel+' & Book';
  };
  xhr.send(fd);
});
if(svcEl.tagName!=='SELECT'&&_state.svcId){if(_isSalon)loadStaff();renderCalendar();}
else if(svcEl.tagName==='SELECT'&&svcEl.value)onSvcChange();
else renderCalendar();
})();";
    $_inlineJs = ob_get_clean();
    wp_add_inline_script('instinctor-frontend-js', $_inlineJs, 'after');

    return (string) ob_get_clean();
}
