UNPKG

@helllo-ai/spark-chat-widget

Version:

Helllo AI Spark Chat Widget — guided navigation embed for website assistants

9,064 lines • 391 kB
;(function () {
  if (typeof window === 'undefined') return

  const VERSION = 'spark-core-0.1.66-prod'

  // Replace a non-spark-core widget (e.g. agent-chat.prod.js from other console pages).
  if (window.AgentChatWidget) {
    if (window.AgentChatWidget.version === VERSION) return
    try {
      window.AgentChatWidget.destroy && window.AgentChatWidget.destroy()
    } catch (_) {}
  }

  /** Browser IIFE bundle (dist/daily-iframe.js). Older packages used build/daily.js — that path 404s on unpkg. */
  const DEFAULT_DAILY_SCRIPT_URL =
    'https://cdn.jsdelivr.net/npm/@daily-co/daily-js@0.87.0/dist/daily-iframe.js'

  /**
   * Partner CSPs often allow connect-src https://*.helllo.ai but block c.daily.co.
   * Route Daily HTTPS + WSS signaling through dash so /daily/start can join on any site.
   */
  function dailyProxyUrl() {
    var base = ''
    if (VERSION.includes('-staging')) base = 'https://dash-staging.helllo.ai'
    else if (VERSION.includes('-prod')) base = 'https://dash.helllo.ai'
    else {
      try {
        var host = window.location.hostname
        if (host === 'localhost' || host === '127.0.0.1') base = window.location.origin
        else if (host === 'helllo.ai' || host.endsWith('.helllo.ai')) base = window.location.origin
        else base = 'https://dash.helllo.ai'
      } catch (_) {
        base = 'https://dash.helllo.ai'
      }
    }
    return String(base).replace(/\/$/, '') + '/api/daily-proxy'
  }

  function createDailyCallObject(Daily, extras) {
    var opts = Object.assign({ audioSource: true, videoSource: false }, extras || {})
    opts.dailyConfig = Object.assign({}, opts.dailyConfig || {}, { proxyUrl: dailyProxyUrl() })
    return Daily.createCallObject(opts)
  }

  const DEFAULT_SUPPORT_AGENT_IMAGE_URL =
    'https://images.pexels.com/photos/6535381/pexels-photo-6535381.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940'

  /** Same-origin static assets shipped alongside this bundle (spark widget files live at /spark on both hosts). */
  const SPARK_ASSETS_BASE = VERSION.includes('-staging')
    ? 'https://dash-staging.helllo.ai'
    : VERSION.includes('-prod')
      ? 'https://dash.helllo.ai'
      : (function () {
          try {
            return new URL(document.currentScript?.src || window.location.href).origin
          } catch (_) {
            return ''
          }
        })()
  const PRECALL_HERO_IMAGE_URL =
    'https://ik.imagekit.io/ise7sbyg9/tr:w-760,h-300,q-80,f-auto/precall-hero.jpeg'

  /** Pipecat Daily public agent id: staging vs production bundles (VERSION suffix -prod / -staging). */
  function defaultDailyAgentName() {
    if (VERSION.includes('-prod')) return 'voice-web-prod'
    return 'voice-web-001'
  }

  const ACW_SPARKLE_SVG = '<svg class="acw-launcher-sparkle" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M12 2l1.2 5.2L18 9l-4.8 1.8L12 16l-1.2-5.2L6 9l4.8-1.8L12 2z" fill="currentColor"/><path d="M18.5 4l.4 1.4L20 6l-1.1.6-.4 1.4-.4-1.4L17 6l1.1-.6.4-1.4z" fill="currentColor"/><circle cx="7" cy="17" r="1.2" fill="currentColor"/></svg>'
  const ACW_LAUNCHER_X_SVG = '<svg class="acw-launcher-x" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" aria-hidden="true"><path d="M18 6L6 18M6 6l12 12"/></svg>'
  const ACW_ICON_END_CHAT = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="18" height="18" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="m14.5 8.5-5 5"/><path d="m9.5 8.5 5 5"/></svg>'
  const ACW_ICON_END_CALL = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="18" height="18" aria-hidden="true"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/><line x1="1" y1="1" x2="23" y2="23"/></svg>'
  const ACW_ICON_CLOSE_WIDGET = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true" width="18" height="18"><path d="M18 6L6 18M6 6l12 12"/></svg>'
  const ACW_ICON_EXPAND_WINDOW = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="18" height="18" aria-hidden="true"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>'
  const ACW_ICON_COLLAPSE_WINDOW = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="18" height="18" aria-hidden="true"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" y1="10" x2="21" y2="3"/><line x1="3" y1="21" x2="10" y2="14"/></svg>'
  const ACW_ICON_PAPERCLIP = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="20" height="20" aria-hidden="true"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>'
  const ACW_ICON_SEND = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" width="18" height="18" aria-hidden="true"><path d="M12 19V5"/><path d="m5 12 7-7 7 7"/></svg>'
  const ACW_ICON_THUMBS_UP =
    '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="14" height="14" aria-hidden="true"><path d="M7 10v12"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></svg>'
  const ACW_ICON_THUMBS_DOWN =
    '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="14" height="14" aria-hidden="true"><path d="M17 14V2"/><path d="M9 18.12 10 14H4.17a2 2 0 0 1-1.92-2.56l2.33-8A2 2 0 0 1 6.5 2H20a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-2.76a2 2 0 0 0-1.79 1.11L12 22a3.13 3.13 0 0 1-3-3.88Z"/></svg>'
  const ACW_ICON_HANDOVER_ACK =
    '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="18" height="18" aria-hidden="true"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="m16 11 2 2 4-4"/></svg>'

  const ACW_UI_LANG_CODES = { en: 1, id: 1, tl: 1, ms: 1, hi: 1, es: 1, fr: 1 }
  function normalizeWidgetUiLanguage(raw) {
    var s = String(raw == null ? 'en' : raw).trim().toLowerCase()
    var base = s.split('-')[0]
    return ACW_UI_LANG_CODES[base] ? base : 'en'
  }
  function acwT(uiLang, key) {
    var pack = ACW_I18N[uiLang] || ACW_I18N.en
    if (pack[key] != null) return pack[key]
    return ACW_I18N.en[key] != null ? ACW_I18N.en[key] : key
  }
  const ACW_I18N = {
    en: {
      always_online: 'Always Online',
      back_to_home: 'Back to home',
      end_chat_close: 'End chat and close',
      end_chat: 'End chat',
      close_widget: 'Close widget',
      expand_window: 'Expand window',
      collapse_window: 'Collapse window',
      end_call: 'End call',
      status_disconnected: 'Disconnected',
      status_connected: 'Online',
      status_connecting: 'Connecting…',
      status_reconnecting: 'Reconnecting…',
      connection_lost: 'Connection lost. Your messages are saved.',
      conversation_ended: 'You are now disconnected from live chat',
      connection_reconnecting: 'Reconnecting to your conversation…',
      reconnect: 'Reconnect',
      start_again: 'Start again',
      connecting_wait: 'Connecting... Please wait.',
      how_can_we_help: 'How can we help?',
      choose_communication_style: 'Choose your communication style',
      text_chat_title: 'Text Chat',
      text_chat_desc: 'Start chatting',
      voice_chat_title: 'Voice Chat',
      voice_chat_desc: 'Speak naturally',
      whatsapp_title: 'WhatsApp',
      whatsapp_desc: 'Continue on WhatsApp',
      placeholder_type_message: 'Type a message...',
      placeholder_connect_first: 'Connect to start chatting',
      attach_image: 'Attach file',
      remove_attachment: 'Remove attachment',
      file_too_large: 'File is too large (max {size}).',
      file_type_not_allowed: 'That file type is not supported.',
      max_files_reached: 'Maximum number of attachments reached.',
      file_upload_failed: 'File upload failed. Please try again.',
      file_upload_disabled: 'File uploads are not enabled.',
      send_message: 'Send message',
      message_aria: 'Message',
      powered_by: 'Powered by ',
      form_title_start: 'Share details',
      form_subtitle_details: 'We just need a few details',
      label_name: 'Your Name',
      placeholder_name: 'Enter your name',
      label_phone: 'Mobile Number',
      placeholder_phone: 'Enter your mobile',
      conversation_language: 'Conversation language',
      start_conversation: 'Start conversation →',
      skip: 'Skip',
      back_to_options: '← Back to options',
      error_name_required: 'Please enter your name',
      error_phone_required: 'Please enter your mobile number',
      error_phone_invalid: 'Please enter a valid mobile number',
      label_country_code: 'Country code',
      search_country: 'Search country or code',
      no_country_match: 'No countries found',
      alert_name_phone_required: 'Please fill in both name and phone number',
      alert_session_establish: 'Session not established. Please wait for connection.',
      ws_not_connected: 'WebSocket not connected. Please wait for connection.',
      submit_error: 'Error submitting information. Please try again.',
      voice_title: 'Voice Conversation',
      voice_tap_mic: 'Tap the mic to speak',
      voice_connecting: 'Connecting…',
      voice_loading_sdk: 'Loading voice SDK…',
      voice_url_or_key_required: 'Voice service URL or Daily API key required',
      voice_error: 'Voice error',
      voice_connected_speak: 'Connected — speak when ready',
      voice_starting_session: 'Starting voice session…',
      voice_joining_room: 'Joining voice room…',
      voice_connection_failed: 'Connection failed',
      voice_url_not_set: 'Voice service URL not set',
      voice_establishing_link: 'Establishing voice link…',
      voice_connecting_agent: 'Connecting to agent…',
      voice_negotiating: 'Negotiating with agent…',
      voice_signing_in: 'Signing in with agent…',
      voice_coming_soon: 'Voice coming soon',
      failed_connect: 'Failed to connect. Please try again or check your connection.',
      choose_text_chat_home: 'Please go to the home screen and choose Text Chat to start.',
      image_too_large: 'Image is too large (max 2MB).',
      image_read_error: 'Could not read the image. Please try again.',
      voice_end_conversation: 'End Conversation',
      voice_mic_aria: 'Microphone',
      launcher_default: 'Chat with us',
      navigation_blocked: 'Sorry, I cannot open that page.',
      navigation_scrolled: 'Scrolled to that section.',
      navigation_navigated: 'Navigated to that page.',
      navigation_opened_new_tab: 'Opened that page in a new tab.',
      navigation_popup_blocked: 'Could not open a new tab. Please allow pop-ups for this site and try again.',
      voice_status_connected: 'Connected',
      voice_status_disconnected: 'Disconnected',
      voice_listening: 'Listening…',
      voice_thinking: 'Thinking…',
      voice_speaking: 'Speaking…',
      voice_close_transcript: 'Close',
      voice_back_to_options: 'Back to options',
      nav_offer_yes: 'Yes, take me there',
      nav_offer_no: 'No thanks',
      navigation_continued: 'Continued on {page}',
      reconnected_success: 'Reconnected',
      copy_message: 'Copy',
      copied_message: 'Copied',
      feedback_helpful: 'Helpful',
      feedback_not_helpful: 'Not helpful',
      handover_ack_default: 'Message forwarded to human agent.',
      product_carousel_aria: 'Product results',
      product_view: 'View product',
      product_variants: '{count} variants',
      product_no_image: 'No image',
    },
    id: {
      always_online: 'Selalu online',
      back_to_home: 'Kembali ke beranda',
      end_chat_close: 'Akhiri obrolan dan tutup',
      status_disconnected: 'Terputus',
      status_connected: 'Online',
      status_connecting: 'Menyambungkan…',
      status_reconnecting: 'Menyambungkan kembali…',
      connection_lost: 'Koneksi terputus. Pesan Anda tersimpan.',
      conversation_ended: 'Anda sekarang terputus dari live chat',
      connection_reconnecting: 'Menyambungkan kembali ke percakapan…',
      reconnect: 'Sambungkan lagi',
      start_again: 'Mulai lagi',
      connecting_wait: 'Menyambungkan... Harap tunggu.',
      how_can_we_help: 'Bagaimana kami bisa membantu?',
      choose_communication_style: 'Pilih gaya komunikasi Anda',
      text_chat_title: 'Obrolan teks',
      text_chat_desc: 'Ketik pesan dengan AI',
      voice_chat_title: 'Obrolan suara',
      voice_chat_desc: 'Bicara alami dengan AI',
      whatsapp_title: 'WhatsApp',
      whatsapp_desc: 'Lanjutkan di WhatsApp',
      placeholder_type_message: 'Ketik pesan...',
      placeholder_connect_first: 'Sambungkan untuk mulai mengobrol',
      attach_image: 'Lampirkan gambar',
      send_message: 'Kirim pesan',
      message_aria: 'Pesan',
      powered_by: 'Didayakan oleh ',
      form_title_start: 'Mari mulai',
      form_subtitle_details: 'Kami hanya perlu beberapa detail',
      label_name: 'Nama Anda',
      placeholder_name: 'Masukkan nama Anda',
      label_phone: 'Nomor ponsel',
      placeholder_phone: 'Masukkan nomor ponsel',
      conversation_language: 'Bahasa percakapan',
      start_conversation: 'Mulai percakapan →',
      skip: 'Lewati',
      back_to_options: '← Kembali ke pilihan',
      error_name_required: 'Harap masukkan nama Anda',
      error_phone_required: 'Harap masukkan nomor ponsel Anda',
      alert_name_phone_required: 'Harap isi nama dan nomor telepon',
      alert_session_establish: 'Sesi belum tersedia. Harap tunggu sambungan.',
      ws_not_connected: 'WebSocket tidak terhubung. Harap tunggu sambungan.',
      submit_error: 'Gagal mengirim informasi. Coba lagi.',
      voice_title: 'Percakapan suara',
      voice_tap_mic: 'Ketuk mikrofon untuk berbicara',
      voice_connecting: 'Menyambungkan…',
      voice_loading_sdk: 'Memuat SDK suara…',
      voice_url_or_key_required: 'URL layanan suara atau kunci Daily diperlukan',
      voice_error: 'Kesalahan suara',
      voice_connected_speak: 'Terhubung — silakan berbicara',
      voice_starting_session: 'Memulai sesi suara…',
      voice_joining_room: 'Bergabung ke ruang suara…',
      voice_connection_failed: 'Sambungan gagal',
      voice_url_not_set: 'URL layanan suara tidak diatur',
      voice_establishing_link: 'Menjalin tautan suara…',
      voice_connecting_agent: 'Menyambung ke agen…',
      voice_negotiating: 'Bernegosiasi dengan agen…',
      voice_signing_in: 'Masuk ke agen…',
      voice_coming_soon: 'Suara segera hadir',
      failed_connect: 'Gagal menyambung. Coba lagi atau periksa koneksi Anda.',
      choose_text_chat_home: 'Buka layar utama dan pilih Obrolan teks untuk memulai.',
      image_too_large: 'Gambar terlalu besar (maks. 2MB).',
      image_read_error: 'Tidak dapat membaca gambar. Coba lagi.',
      voice_end_conversation: 'Akhiri percakapan',
      voice_mic_aria: 'Mikrofon',
      launcher_default: 'Ngobrol dengan kami',
    },
    tl: {
      always_online: 'Palaging online',
      back_to_home: 'Bumalik sa home',
      end_chat_close: 'Tapusin ang chat at isara',
      status_disconnected: 'Hindi konektado',
      status_connected: 'Online',
      status_connecting: 'Kumokonekta…',
      status_reconnecting: 'Muling kumokonekta…',
      connection_lost: 'Nawala ang koneksyon. Naka-save ang mga mensahe.',
      conversation_ended: 'Nadiskonekta ka na mula sa live chat',
      connection_reconnecting: 'Muling kumokonekta sa usapan…',
      reconnect: 'Kumonekta muli',
      start_again: 'Magsimula ulit',
      connecting_wait: 'Kumokonekta... Maghintay po.',
      how_can_we_help: 'Paano kami makakatulong?',
      choose_communication_style: 'Piliin ang istilo ng komunikasyon',
      text_chat_title: 'Text chat',
      text_chat_desc: 'Mag-type ng mensahe gamit ang AI',
      voice_chat_title: 'Voice chat',
      voice_chat_desc: 'Magsalita nang natural sa AI',
      whatsapp_title: 'WhatsApp',
      whatsapp_desc: 'Magpatuloy sa WhatsApp',
      placeholder_type_message: 'Mag-type ng mensahe...',
      placeholder_connect_first: 'Kumonekta para magsimula ng chat',
      attach_image: 'Maglakip ng larawan',
      send_message: 'Ipadala ang mensahe',
      message_aria: 'Mensahe',
      powered_by: 'Pinapagana ng ',
      form_title_start: 'Magsimula na tayo',
      form_subtitle_details: 'Kailangan lang namin ng ilang detalye',
      label_name: 'Iyong pangalan',
      placeholder_name: 'Ilagay ang iyong pangalan',
      label_phone: 'Mobile number',
      placeholder_phone: 'Ilagay ang iyong mobile',
      conversation_language: 'Wika ng pag-uusap',
      start_conversation: 'Simulan ang pag-uusap →',
      skip: 'Laktawan',
      back_to_options: '← Bumalik sa mga opsyon',
      error_name_required: 'Ilagay ang iyong pangalan',
      error_phone_required: 'Ilagay ang iyong mobile number',
      alert_name_phone_required: 'Punan ang pangalan at numero ng telepono',
      alert_session_establish: 'Hindi pa handa ang session. Maghintay sa koneksyon.',
      ws_not_connected: 'Hindi nakakonekta ang WebSocket. Maghintay.',
      submit_error: 'Error sa pagsusumite. Subukan muli.',
      voice_title: 'Usapang boses',
      voice_tap_mic: 'I-tap ang mic para magsalita',
      voice_connecting: 'Kumokonekta…',
      voice_loading_sdk: 'Naglo-load ng voice SDK…',
      voice_url_or_key_required: 'Kailangan ang voice service URL o Daily API key',
      voice_error: 'Error sa boses',
      voice_connected_speak: 'Nakakonekta — magsalita kapag handa na',
      voice_starting_session: 'Nagsisimula ng voice session…',
      voice_joining_room: 'Sumasali sa voice room…',
      voice_connection_failed: 'Nabigo ang koneksyon',
      voice_url_not_set: 'Hindi naka-set ang voice service URL',
      voice_establishing_link: 'Itinatatag ang voice link…',
      voice_connecting_agent: 'Kumokonekta sa agent…',
      voice_negotiating: 'Nakikipag-negotiate sa agent…',
      voice_signing_in: 'Nag-sign in sa agent…',
      voice_coming_soon: 'Darating na ang boses',
      failed_connect: 'Nabigo ang koneksyon. Subukan muli o tingnan ang koneksyon.',
      choose_text_chat_home: 'Pumunta sa home at piliin ang Text chat para magsimula.',
      image_too_large: 'Masyadong malaki ang larawan (max 2MB).',
      image_read_error: 'Hindi mabasa ang larawan. Subukan muli.',
      voice_end_conversation: 'Tapusin ang usapan',
      voice_mic_aria: 'Mikropono',
      launcher_default: 'Makipag-chat sa amin',
    },
    ms: {
      always_online: 'Sentiasa dalam talian',
      back_to_home: 'Kembali ke laman utama',
      end_chat_close: 'Tamatkan sembang dan tutup',
      status_disconnected: 'Terputus',
      status_connected: 'Dalam talian',
      status_connecting: 'Menyambung…',
      status_reconnecting: 'Menyambung semula…',
      connection_lost: 'Sambungan terputus. Mesej anda disimpan.',
      conversation_ended: 'Anda kini terputus dari sembang langsung',
      connection_reconnecting: 'Menyambung semula ke perbualan…',
      reconnect: 'Sambung semula',
      start_again: 'Mula semula',
      connecting_wait: 'Menyambung... Sila tunggu.',
      how_can_we_help: 'Bagaimana kami boleh membantu?',
      choose_communication_style: 'Pilih gaya komunikasi anda',
      text_chat_title: 'Sembang teks',
      text_chat_desc: 'Taip mesej dengan AI',
      voice_chat_title: 'Sembang suara',
      voice_chat_desc: 'Bercakap secara semula jadi dengan AI',
      whatsapp_title: 'WhatsApp',
      whatsapp_desc: 'Teruskan di WhatsApp',
      placeholder_type_message: 'Taip mesej...',
      placeholder_connect_first: 'Sambung untuk mula sembang',
      attach_image: 'Lampirkan imej',
      send_message: 'Hantar mesej',
      message_aria: 'Mesej',
      powered_by: 'Dikuasakan oleh ',
      form_title_start: 'Mari mulakan',
      form_subtitle_details: 'Kami hanya perlukan beberapa butiran',
      label_name: 'Nama anda',
      placeholder_name: 'Masukkan nama anda',
      label_phone: 'Nombor telefon bimbit',
      placeholder_phone: 'Masukkan nombor bimbit anda',
      conversation_language: 'Bahasa perbualan',
      start_conversation: 'Mula perbualan →',
      skip: 'Langkau',
      back_to_options: '← Kembali ke pilihan',
      error_name_required: 'Sila masukkan nama anda',
      error_phone_required: 'Sila masukkan nombor telefon bimbit anda',
      alert_name_phone_required: 'Sila isi nama dan nombor telefon',
      alert_session_establish: 'Sesi belum sedia. Sila tunggu sambungan.',
      ws_not_connected: 'WebSocket tidak bersambung. Sila tunggu.',
      submit_error: 'Ralat menyerahkan maklumat. Cuba lagi.',
      voice_title: 'Perbualan suara',
      voice_tap_mic: 'Ketik mikrofon untuk bercakap',
      voice_connecting: 'Menyambung…',
      voice_loading_sdk: 'Memuatkan SDK suara…',
      voice_url_or_key_required: 'URL perkhidmatan suara atau kunci Daily diperlukan',
      voice_error: 'Ralat suara',
      voice_connected_speak: 'Bersambung — bercakap apabila bersedia',
      voice_starting_session: 'Memulakan sesi suara…',
      voice_joining_room: 'Menyertai bilik suara…',
      voice_connection_failed: 'Sambungan gagal',
      voice_url_not_set: 'URL perkhidmatan suara tidak ditetapkan',
      voice_establishing_link: 'Menubuhkan pautan suara…',
      voice_connecting_agent: 'Menyambung ke ejen…',
      voice_negotiating: 'Berunding dengan ejen…',
      voice_signing_in: 'Log masuk dengan ejen…',
      voice_coming_soon: 'Suara akan datang tidak lama lagi',
      failed_connect: 'Gagal menyambung. Cuba lagi atau semak sambungan anda.',
      choose_text_chat_home: 'Pergi ke skrin utama dan pilih Sembang teks untuk mula.',
      image_too_large: 'Imej terlalu besar (maks. 2MB).',
      image_read_error: 'Tidak boleh membaca imej. Cuba lagi.',
      voice_end_conversation: 'Tamatkan perbualan',
      voice_mic_aria: 'Mikrofon',
      launcher_default: 'Sembang dengan kami',
    },
    hi: {
      always_online: 'हमेशा ऑनलाइन',
      back_to_home: 'होम पर वापस जाएँ',
      end_chat_close: 'चैट समाप्त करें और बंद करें',
      status_disconnected: 'डिस्कनेक्ट',
      status_connected: 'ऑनलाइन',
      status_connecting: 'कनेक्ट हो रहा है…',
      status_reconnecting: 'पुनः कनेक्ट हो रहा है…',
      connection_lost: 'कनेक्शन टूट गया। आपके संदेश सुरक्षित हैं।',
      conversation_ended: 'आप अब लाइव चैट से डिस्कनेक्ट हो गए हैं',
      connection_reconnecting: 'बातचीत से पुनः कनेक्ट हो रहा है…',
      reconnect: 'पुनः कनेक्ट करें',
      start_again: 'फिर से शुरू करें',
      connecting_wait: 'कनेक्ट हो रहा है... कृपया प्रतीक्षा करें।',
      how_can_we_help: 'हम कैसे मदद कर सकते हैं?',
      choose_communication_style: 'अपनी संवाद शैली चुनें',
      text_chat_title: 'टेक्स्ट चैट',
      text_chat_desc: 'AI के साथ संदेश टाइप करें',
      voice_chat_title: 'वॉइस चैट',
      voice_chat_desc: 'AI से स्वाभाविक रूप से बात करें',
      whatsapp_title: 'WhatsApp',
      whatsapp_desc: 'WhatsApp पर जारी रखें',
      placeholder_type_message: 'संदेश लिखें...',
      placeholder_connect_first: 'चैट शुरू करने के लिए कनेक्ट करें',
      attach_image: 'छवि संलग्न करें',
      send_message: 'संदेश भेजें',
      message_aria: 'संदेश',
      powered_by: 'संचालित ',
      form_title_start: 'चलिए शुरू करते हैं',
      form_subtitle_details: 'हमें बस कुछ विवरण चाहिए',
      label_name: 'आपका नाम',
      placeholder_name: 'अपना नाम दर्ज करें',
      label_phone: 'मोबाइल नंबर',
      placeholder_phone: 'अपना मोबाइल दर्ज करें',
      conversation_language: 'बातचीत की भाषा',
      start_conversation: 'बातचीत शुरू करें →',
      skip: 'छोड़ें',
      back_to_options: '← विकल्पों पर वापस जाएँ',
      error_name_required: 'कृपया अपना नाम दर्ज करें',
      error_phone_required: 'कृपया अपना मोबाइल नंबर दर्ज करें',
      alert_name_phone_required: 'कृपया नाम और फ़ोन नंबर भरें',
      alert_session_establish: 'सत्र तैयार नहीं। कृपया कनेक्शन की प्रतीक्षा करें।',
      ws_not_connected: 'WebSocket कनेक्ट नहीं। कृपया प्रतीक्षा करें।',
      submit_error: 'जानकारी भेजने में त्रुटि। पुनः प्रयास करें।',
      voice_title: 'वॉइस बातचीत',
      voice_tap_mic: 'बोलने के लिए माइक टैप करें',
      voice_connecting: 'कनेक्ट हो रहा है…',
      voice_loading_sdk: 'वॉइस SDK लोड हो रहा है…',
      voice_url_or_key_required: 'वॉइस सेवा URL या Daily कुंजी आवश्यक',
      voice_error: 'वॉइस त्रुटि',
      voice_connected_speak: 'कनेक्ट — तैयार होने पर बोलें',
      voice_starting_session: 'वॉइस सेशन शुरू हो रहा है…',
      voice_joining_room: 'वॉइस रूम में शामिल हो रहे…',
      voice_connection_failed: 'कनेक्शन विफल',
      voice_url_not_set: 'वॉइस सेवा URL सेट नहीं',
      voice_establishing_link: 'वॉइस लिंक स्थापित हो रहा है…',
      voice_connecting_agent: 'एजेंट से कनेक्ट…',
      voice_negotiating: 'एजेंट से बातचीत…',
      voice_signing_in: 'एजेंट में साइन इन…',
      voice_coming_soon: 'वॉइस जल्द आ रहा है',
      failed_connect: 'कनेक्ट नहीं हो सका। पुनः प्रयास करें या अपना कनेक्शन जाँचें।',
      choose_text_chat_home: 'होम पर जाएँ और टेक्स्ट चैट चुनकर शुरू करें।',
      image_too_large: 'छवि बहुत बड़ी (अधिकतम 2MB)।',
      image_read_error: 'छवि नहीं पढ़ पा रहे। पुनः प्रयास करें।',
      voice_end_conversation: 'बातचीत समाप्त करें',
      voice_mic_aria: 'माइक्रोफ़ोन',
      launcher_default: 'हमसे चैट करें',
    },
    es: {
      always_online: 'Siempre en línea',
      back_to_home: 'Volver al inicio',
      end_chat_close: 'Finalizar chat y cerrar',
      status_disconnected: 'Desconectado',
      status_connected: 'En línea',
      status_connecting: 'Conectando…',
      status_reconnecting: 'Reconectando…',
      connection_lost: 'Conexión perdida. Sus mensajes están guardados.',
      conversation_ended: 'Ya no está conectado al chat en vivo',
      connection_reconnecting: 'Reconectando a su conversación…',
      reconnect: 'Reconectar',
      start_again: 'Empezar de nuevo',
      connecting_wait: 'Conectando... Espere.',
      how_can_we_help: '¿Cómo podemos ayudarle?',
      choose_communication_style: 'Elija su estilo de comunicación',
      text_chat_title: 'Chat de texto',
      text_chat_desc: 'Escriba mensajes con la IA',
      voice_chat_title: 'Chat de voz',
      voice_chat_desc: 'Hable con naturalidad con la IA',
      whatsapp_title: 'WhatsApp',
      whatsapp_desc: 'Continuar en WhatsApp',
      placeholder_type_message: 'Escriba un mensaje...',
      placeholder_connect_first: 'Conéctese para empezar a chatear',
      attach_image: 'Adjuntar imagen',
      send_message: 'Enviar mensaje',
      message_aria: 'Mensaje',
      powered_by: 'Con tecnología de ',
      form_title_start: 'Empecemos',
      form_subtitle_details: 'Solo necesitamos algunos datos',
      label_name: 'Su nombre',
      placeholder_name: 'Introduzca su nombre',
      label_phone: 'Número móvil',
      placeholder_phone: 'Introduzca su móvil',
      conversation_language: 'Idioma de la conversación',
      start_conversation: 'Iniciar conversación →',
      skip: 'Omitir',
      back_to_options: '← Volver a opciones',
      error_name_required: 'Introduzca su nombre',
      error_phone_required: 'Introduzca su número móvil',
      alert_name_phone_required: 'Complete nombre y teléfono',
      alert_session_establish: 'Sesión no lista. Espere la conexión.',
      ws_not_connected: 'WebSocket no conectado. Espere.',
      submit_error: 'Error al enviar. Intente de nuevo.',
      voice_title: 'Conversación por voz',
      voice_tap_mic: 'Pulse el micrófono para hablar',
      voice_connecting: 'Conectando…',
      voice_loading_sdk: 'Cargando SDK de voz…',
      voice_url_or_key_required: 'Se requiere URL del servicio de voz o clave de Daily',
      voice_error: 'Error de voz',
      voice_connected_speak: 'Conectado — hable cuando esté listo',
      voice_starting_session: 'Iniciando sesión de voz…',
      voice_joining_room: 'Uniéndose a la sala de voz…',
      voice_connection_failed: 'Conexión fallida',
      voice_url_not_set: 'URL del servicio de voz no configurada',
      voice_establishing_link: 'Estableciendo enlace de voz…',
      voice_connecting_agent: 'Conectando con el agente…',
      voice_negotiating: 'Negociando con el agente…',
      voice_signing_in: 'Iniciando sesión con el agente…',
      voice_coming_soon: 'Voz próximamente',
      failed_connect: 'No se pudo conectar. Intente de nuevo o compruebe su conexión.',
      choose_text_chat_home: 'Vaya al inicio y elija chat de texto para empezar.',
      image_too_large: 'Imagen demasiado grande (máx. 2 MB).',
      image_read_error: 'No se pudo leer la imagen. Intente de nuevo.',
      voice_end_conversation: 'Finalizar conversación',
      voice_mic_aria: 'Micrófono',
      launcher_default: 'Chatea con nosotros',
    },
    fr: {
      always_online: 'Toujours en ligne',
      back_to_home: "Retour à l'accueil",
      end_chat_close: 'Terminer le chat et fermer',
      status_disconnected: 'Déconnecté',
      status_connected: 'En ligne',
      status_connecting: 'Connexion…',
      status_reconnecting: 'Reconnexion…',
      connection_lost: 'Connexion perdue. Vos messages sont enregistrés.',
      conversation_ended: 'Vous êtes maintenant déconnecté du chat en direct',
      connection_reconnecting: 'Reconnexion à votre conversation…',
      reconnect: 'Reconnecter',
      start_again: 'Recommencer',
      connecting_wait: 'Connexion en cours... Veuillez patienter.',
      how_can_we_help: 'Comment pouvons-nous vous aider ?',
      choose_communication_style: 'Choisissez votre mode de communication',
      text_chat_title: 'Chat texte',
      text_chat_desc: 'Écrivez des messages avec l’IA',
      voice_chat_title: 'Chat vocal',
      voice_chat_desc: 'Parlez naturellement avec l’IA',
      whatsapp_title: 'WhatsApp',
      whatsapp_desc: 'Continuer sur WhatsApp',
      placeholder_type_message: 'Saisissez un message...',
      placeholder_connect_first: 'Connectez-vous pour démarrer le chat',
      attach_image: 'Joindre une image',
      send_message: 'Envoyer le message',
      message_aria: 'Message',
      powered_by: 'Propulsé par ',
      form_title_start: 'Commençons',
      form_subtitle_details: 'Il nous faut quelques informations',
      label_name: 'Votre nom',
      placeholder_name: 'Entrez votre nom',
      label_phone: 'Numéro de mobile',
      placeholder_phone: 'Entrez votre mobile',
      conversation_language: 'Langue de la conversation',
      start_conversation: 'Démarrer la conversation →',
      skip: 'Ignorer',
      back_to_options: '← Retour aux options',
      error_name_required: 'Veuillez saisir votre nom',
      error_phone_required: 'Veuillez saisir votre numéro de mobile',
      alert_name_phone_required: 'Veuillez remplir le nom et le téléphone',
      alert_session_establish: 'Session non établie. Veuillez attendre.',
      ws_not_connected: 'WebSocket non connecté. Veuillez attendre.',
      submit_error: "Erreur d'envoi. Réessayez.",
      voice_title: 'Conversation vocale',
      voice_tap_mic: 'Appuyez sur le micro pour parler',
      voice_connecting: 'Connexion…',
      voice_loading_sdk: 'Chargement du SDK vocal…',
      voice_url_or_key_required: 'URL du service vocal ou clé Daily requise',
      voice_error: 'Erreur vocale',
      voice_connected_speak: 'Connecté — parlez quand vous êtes prêt',
      voice_starting_session: 'Démarrage de la session vocale…',
      voice_joining_room: 'Connexion à la salle vocale…',
      voice_connection_failed: 'Échec de la connexion',
      voice_url_not_set: 'URL du service vocal non définie',
      voice_establishing_link: 'Établissement du lien vocal…',
      voice_connecting_agent: "Connexion à l'agent…",
      voice_negotiating: "Négociation avec l'agent…",
      voice_signing_in: "Connexion à l'agent…",
      voice_coming_soon: 'La voix arrive bientôt',
      failed_connect: 'Échec de la connexion. Réessayez ou vérifiez votre connexion.',
      choose_text_chat_home: "Allez à l'accueil et choisissez le chat texte pour commencer.",
      image_too_large: 'Image trop volumineuse (max 2 Mo).',
      image_read_error: 'Impossible de lire l’image. Réessayez.',
      voice_end_conversation: 'Terminer la conversation',
      voice_mic_aria: 'Microphone',
      launcher_default: 'Discutez avec nous',
    },
  }

  function normalizeChatRole(role) {
    var r = String(role == null ? 'assistant' : role).toLowerCase().trim()
    if (r === 'user' || r === 'human' || r === 'customer' || r === 'visitor' || r === 'end_user') return 'user'
    return 'bot'
  }

  function normalizeWhatsAppDigits(phone) {
    return String(phone || '').replace(/\D/g, '')
  }

  var PHONE_COUNTRY_CODES = [
    { iso: 'AU', dial: '61', name: 'Australia' },
    { iso: 'AT', dial: '43', name: 'Austria' },
    { iso: 'BH', dial: '973', name: 'Bahrain' },
    { iso: 'BD', dial: '880', name: 'Bangladesh' },
    { iso: 'BE', dial: '32', name: 'Belgium' },
    { iso: 'BR', dial: '55', name: 'Brazil' },
    { iso: 'CA', dial: '1', name: 'Canada' },
    { iso: 'CN', dial: '86', name: 'China' },
    { iso: 'CO', dial: '57', name: 'Colombia' },
    { iso: 'CZ', dial: '420', name: 'Czech Republic' },
    { iso: 'DK', dial: '45', name: 'Denmark' },
    { iso: 'EG', dial: '20', name: 'Egypt' },
    { iso: 'FI', dial: '358', name: 'Finland' },
    { iso: 'FR', dial: '33', name: 'France' },
    { iso: 'DE', dial: '49', name: 'Germany' },
    { iso: 'GR', dial: '30', name: 'Greece' },
    { iso: 'HK', dial: '852', name: 'Hong Kong' },
    { iso: 'HU', dial: '36', name: 'Hungary' },
    { iso: 'IN', dial: '91', name: 'India' },
    { iso: 'ID', dial: '62', name: 'Indonesia' },
    { iso: 'IE', dial: '353', name: 'Ireland' },
    { iso: 'IL', dial: '972', name: 'Israel' },
    { iso: 'IT', dial: '39', name: 'Italy' },
    { iso: 'JP', dial: '81', name: 'Japan' },
    { iso: 'KE', dial: '254', name: 'Kenya' },
    { iso: 'KW', dial: '965', name: 'Kuwait' },
    { iso: 'MY', dial: '60', name: 'Malaysia' },
    { iso: 'MX', dial: '52', name: 'Mexico' },
    { iso: 'NL', dial: '31', name: 'Netherlands' },
    { iso: 'NZ', dial: '64', name: 'New Zealand' },
    { iso: 'NG', dial: '234', name: 'Nigeria' },
    { iso: 'NO', dial: '47', name: 'Norway' },
    { iso: 'PK', dial: '92', name: 'Pakistan' },
    { iso: 'PH', dial: '63', name: 'Philippines' },
    { iso: 'PL', dial: '48', name: 'Poland' },
    { iso: 'PT', dial: '351', name: 'Portugal' },
    { iso: 'QA', dial: '974', name: 'Qatar' },
    { iso: 'RO', dial: '40', name: 'Romania' },
    { iso: 'SA', dial: '966', name: 'Saudi Arabia' },
    { iso: 'SG', dial: '65', name: 'Singapore' },
    { iso: 'ZA', dial: '27', name: 'South Africa' },
    { iso: 'KR', dial: '82', name: 'South Korea' },
    { iso: 'ES', dial: '34', name: 'Spain' },
    { iso: 'LK', dial: '94', name: 'Sri Lanka' },
    { iso: 'SE', dial: '46', name: 'Sweden' },
    { iso: 'CH', dial: '41', name: 'Switzerland' },
    { iso: 'TW', dial: '886', name: 'Taiwan' },
    { iso: 'TH', dial: '66', name: 'Thailand' },
    { iso: 'TR', dial: '90', name: 'Turkey' },
    { iso: 'AE', dial: '971', name: 'United Arab Emirates' },
    { iso: 'GB', dial: '44', name: 'United Kingdom' },
    { iso: 'US', dial: '1', name: 'United States' },
    { iso: 'VN', dial: '84', name: 'Vietnam' },
  ]
  PHONE_COUNTRY_CODES.sort(function (a, b) { return a.name.localeCompare(b.name) })

  var ACW_TZ_TO_ISO = {
    'Asia/Kolkata': 'IN',
    'Asia/Calcutta': 'IN',
    'Asia/Jakarta': 'ID',
    'Asia/Manila': 'PH',
    'Asia/Singapore': 'SG',
    'Asia/Kuala_Lumpur': 'MY',
    'Asia/Dubai': 'AE',
    'Asia/Riyadh': 'SA',
    'Asia/Bangkok': 'TH',
    'Asia/Tokyo': 'JP',
    'Asia/Seoul': 'KR',
    'Asia/Shanghai': 'CN',
    'Asia/Hong_Kong': 'HK',
    'Asia/Karachi': 'PK',
    'Asia/Dhaka': 'BD',
    'Asia/Colombo': 'LK',
    'Europe/London': 'GB',
    'Europe/Paris': 'FR',
    'Europe/Madrid': 'ES',
    'Europe/Berlin': 'DE',
    'Europe/Rome': 'IT',
    'Europe/Amsterdam': 'NL',
    'America/New_York': 'US',
    'America/Chicago': 'US',
    'America/Denver': 'US',
    'America/Los_Angeles': 'US',
    'America/Toronto': 'CA',
    'America/Vancouver': 'CA',
    'America/Sao_Paulo': 'BR',
    'America/Mexico_City': 'MX',
    'Australia/Sydney': 'AU',
    'Pacific/Auckland': 'NZ',
  }

  function acwCountryFlag(iso) {
    if (!iso || iso.length !== 2) return ''
    var u = iso.toUpperCase()
    try {
      return String.fromCodePoint(u.charCodeAt(0) + 127397, u.charCodeAt(1) + 127397)
    } catch (_) {
      return ''
    }
  }

  function acwDetectBrowserCountryIso() {
    // Phone country follows where the browser is, not the UI language.
    // en-GB is a common language tag in India and was pinning the field to +44.
    try {
      var tz = Intl.DateTimeFormat().resolvedOptions().timeZone
      if (tz && ACW_TZ_TO_ISO[tz]) return ACW_TZ_TO_ISO[tz]
    } catch (_) {}
    try {
      var langs = (navigator.languages && navigator.languages.length)
        ? navigator.languages
        : [navigator.language || '']
      for (var i = 0; i < langs.length; i++) {
        var tag = String(langs[i] || '').trim()
        if (!tag) continue
        var parts = tag.split('-')
        if (parts.length >= 2) {
          var region = parts[parts.length - 1].toUpperCase()
          if (/^[A-Z]{2}$/.test(region) && region !== 'XX') return region
        }
        if (typeof Intl !== 'undefined' && Intl.Locale) {
          try {
            var loc = new Intl.Locale(tag)
            if (loc.region) return loc.region
          } catch (_) {}
        }
      }
    } catch (_) {}
    return 'US'
  }

  function acwFindPhoneCountry(iso) {
    for (var i = 0; i < PHONE_COUNTRY_CODES.length; i++) {
      if (PHONE_COUNTRY_CODES[i].iso === iso) return PHONE_COUNTRY_CODES[i]
    }
    return null
  }

  function acwDefaultPhoneCountryIso() {
    var iso = acwDetectBrowserCountryIso()
    return acwFindPhoneCountry(iso) ? iso : 'US'
  }

  function acwFormatPhoneE164(dial, national) {
    var dialDigits = String(dial || '').replace(/\D/g, '')
    var nationalDigits = String(national || '').replace(/\D/g, '')
    if (nationalDigits.charAt(0) === '0') nationalDigits = nationalDigits.replace(/^0+/, '')
    if (!dialDigits || !nationalDigits) return ''
    return '+' + dialDigits + nationalDigits
  }

  function acwValidateNationalPhone(national) {
    var digits = String(national || '').replace(/\D/g, '')
    if (digits.charAt(0) === '0') digits = digits.replace(/^0+/, '')
    return digits.length >= 6 && digits.length <= 14
  }

  function domainAllowed(hostname, allowedList) {
    if (!Array.isArray(allowedList) || allowedList.length === 0) return true
    const host = (hostname || '').toLowerCase()
    return allowedList.some((d) => {
      const domain = (d || '').toLowerCase().trim()
      if (!domain) return false
      return host === domain || host.endsWith('.' + domain)
    })
  }

  function stripWwwHost(hostname) {
    var host = String(hostname || '').toLowerCase()
    return host.indexOf('www.') === 0 ? host.slice(4) : host
  }

  /** True when target should navigate in the same tab (apex vs www counts as same site). */
  function isSameNavigationDomain(targetHostname, currentHostname) {
    var target = String(targetHostname || '').toLowerCase()
    var current = String(currentHostname || '').toLowerCase()
    if (!target || !current) return false
    if (target === current) return true
    return stripWwwHost(target) === stripWwwHost(current)
  }

  /**
   * Dynamic z-index: scan page for the highest z-index in use, set widget 1 above.
   * Runs twice: (1) on load before showing the launcher, (2) on click before opening
   * the panel. Falls back to 999 when no competing elements exist.
   */
  var ACW_ZINDEX_MAX = 2147483647
  var ACW_ZINDEX_FALLBACK = 999

  function getHighestPageZIndex(excludeEl) {
    if (typeof document === 'undefined') return 0
    var highest = 0
    var vw = window.innerWidth || document.documentElement.clientWidth || 0
    var vh = window.innerHeight || document.documentElement.clientHeight || 0
    var all = document.querySelectorAll('*')
    var limit = Math.min(all.length, 5000)
    for (var i = 0; i < limit; i++) {
      var el = all[i]
      if (el === excludeEl || (excludeEl && excludeEl.contains(el))) continue
      var cs
      try { cs = window.getComputedStyle(el) } catch (_) { continue }
      // Any positioned element competes on z-index in the root stacking
      // context — not just fixed/sticky. Shopify headers are frequently
      // `position: relative`/`absolute` wrappers carrying a large z-index
      // (or only turn fixed via a scroll class), so a fixed/sticky-only
      // filter under-counts and the widget ends up beneath the nav.
      if (cs.position === 'static') continue
      var z = parseInt(cs.zIndex, 10)
      if (isNaN(z) || z <= highest) continue
      // Only count layers the user can currently see. Cart drawers, modals,
      // and search panels sit in the DOM with huge z-indexes while hidden or
      // slid off-canvas; outbidding those would pin the widget above the
      // site's own overlays when they open later (e.g. add-to-cart drawer).
      if (cs.display === 'none' || cs.visibility === 'hidden' || parseFloat(cs.opacity) === 0) continue
      var rect
      try { rect = el.getBoundingClientRect() } catch (_) { continue }
      if (!rect.width || !rect.height) continue
      if (rect.bottom <= 0 || rect.right <= 0 || rect.top >= vh || rect.left >= vw) continue
      highest = z
    }
    return highest
  }

  function elevateWidget(containerEl) {
    if (!containerEl) return
    var highest = getHighestPageZIndex(containerEl)
    var target = highest > 0 ? highest + 1 : ACW_ZINDEX_FALLBACK
    if (target > ACW_ZINDEX_MAX) target = ACW_ZINDEX_MAX
    // Use the 'important' priority: merchant/theme/app stylesheets (seen live
    // on distacart) override a plain inline z-index with `!important` rules.
    // An inline declaration marked important beats any stylesheet rule.
    containerEl.style.setProperty('z-index', String(target), 'important')
    containerEl.style.setProperty('position', 'fixed', 'important')
    // If the page already uses the int32 max (common for consent banners) we
    // can only tie it; ties resolve by DOM order, so repaint last by moving
    // the container to the end of <body>. Moving preserves the shadow root
    // and listeners.
    if (
      highest >= ACW_ZINDEX_MAX &&
      containerEl.parentElement === document.body &&
      document.body.lastElementChild !== containerEl
    ) {
      try { document.body.appendChild(containerEl) } catch (_) {}
    }
  }

  /**
   * Height of fixed/sticky site chrome pinned to the top of the viewport (Shopify
   * announcement bars, sticky navs). Used to keep the panel clear of merchant headers.
   */
  function measureViewportTopObstruction() {
    if (typeof window === 'undefined' || typeof document === 'undefined') return 16
    var vw = window.innerWidth || document.documentElement.clientWidth || 0
    var vh = window.innerHeight || document.documentElement.clientHeight || 0
    if (!vw || !vh) return 16

    var maxBottom = 0
    var selectors = [
      'header',
      '[role="banner"]',
      '.site-header',
      '.shopify-section-header',
      '#shopify-section-header',
      '[class*="sticky-header"]',
      '[class*="announcement-bar"]',
    ]

    for (var si = 0; si < selectors.length; si++) {
      var matches
      try {
        matches = document.querySelectorAll(selectors[si])
      } catch (_) {
        continue
      }
      for (var mi = 0; mi < matches.length && mi < 12; mi++) {
        var el = matches[mi]
        if (!el || el.nodeType !== 1) continue
        var style
        try {
          style = window.getComputedStyle(el)
        } catch (_) {
          continue
        }
        if (style.position !== 'fixed' && style.position !== 'sticky') continue
        if (style.visibility === 'hidden' || style.display === 'none') continue
        var rect = el.getBoundingClientRect()
        if (rect.height < 28 || rect.width < vw * 0.35) continue
        if (rect.top > vh * 0.35 || rect.bottom <= 0) continue
        if (rect.bottom > maxBottom) maxBottom = rect.bottom
      }
    }

    return maxBottom > 0 ? Math.ceil(maxBottom + 8) : 16
  }

  /** Open URL in a new tab. Must not pass noopener in windowFeatures — browsers return null on success. */
  function openNavigationInNewTab(targetUrl) {
    try {
      var opened = window.open(targetUrl, '_blank')
      if (opened) {
        try {
          opened.opener = null
        } catch (_) {}
        return true
      }
      return false
    } catch (_) {
      return false
    }
  }

  function acwSessionKey(agentId) {
    return 'acw:spark:session:' + agentId
  }

  function loadPersistedSession(agentId, ttlMs) {
    try {
      var raw = sessionStorage.getItem(acwSessionKey(agentId))
      if (!raw) return null
      var data = JSON.parse(raw)
      if (!data || !data.savedAt) return null
      if (Date.now() - data.savedAt > ttlMs) {
        sessionStorage.removeItem(acwSessionKey(agentId))
        return null
      }
      return data
    } catch (_) { return null }
  }

  function persistSessionSnapshot(agentId, snapshot) {
    try {
      sessionStorage.setItem(acwSessionKey(agentId), JSON.stringify(snapshot))
    } catch (_) {}
  }

  function clearPersistedSession(agentId) {
    try { sessionStorage.removeItem(acwSessionKey(agentId)) } catch (_) {}
  }

  function navigationUrlAllowed(url, allowedDomains) {
    try {
      var u = new URL(url, window.location.href)
      if (u.protocol !== 'https:' && u.protocol !== 'http:') return false
      return domainAllowed(u.hostname, allowedDomains)
    } catch (_) { return false }
  }

  function normalizeActionNavUrl(url) {
    try {
      return String(new URL(url, window.location.href).href).replace(/\/$/, '').split('#')[0]
    } catch (_) {
      return String(url || '').replace(/\/$/, '')
    }
  }

  var PRODUCT_CAROUSEL_MAX_ITEMS = 10
  var PRODUCT_CAROUSEL_TITLE_MAX = 60
  var PRODUCT_CAROUSEL_PLACEHOLDER_IMAGE =
    "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240' viewBox='0 0 240 240'%3E%3Crect fill='%23f1f5f9' width='240' height='240'/%3E%3Ctext x='50%25' y='50%25' dominant-baseline='middle' text-anchor='middle' fill='%2394a3b8' font-size='13' font-family='system-ui,sans-serif'%3ENo image%3C/text%3E%3C/svg%3E"

  function truncateProductTitle(text, maxLen) {
    var s = String(text || '').trim()
    var limit = typeof maxLen === 'number' ? maxLen : PRODUCT_CAROUSEL_TITLE_MAX
    if (s.length <= limit) return s
    return s.slice(0, Math.max(0, limit - 1)).trim() + '…'
  }

  function formatProductPrice(price) {
    if (!price || price.amount == null || price.amount === '') return ''
    if (price.label) return String(price.label).trim()
    var amount = parseFloat(String(price.amount).replace(/[^0-9.-]/g, ''))
    if (isNaN(amount)) return String(price.amount)
    var currency = String(price.currency || 'USD').toUpperCase()
    try {
      return new Intl.NumberFormat(undefined, { style: 'currency', currency: currency }).format(amount)
    } catch (_) {
      return currency + ' ' + amount.toFixed(2)
    }
  }

  function acwDispatchProductCarouselEvent(eventName, detail) {
    try {
      window.dispatchEvent(
        new CustomEvent('acw:product_carousel', {
          detail: Object.assign({ event: eventName }, detail || {}),
        }),
      )
    } catch (_) {}
  }

  function acwGenerateMessageId() {
    try {
      if (typeof crypto !== 'undefined' && crypto.randomUUID) {
        return 'msg_' + crypto.randomUUID()
      }
    } catch (_) {}
    return 'msg_' + Date.now().toString(36) + '_' + Math.random().toString(36).slice(2, 10)
  }

  function acwDispatchMessageFeedbackEvent(detail) {
    try {
      window.dispatchEvent(
        new CustomEvent('acw:message_feedback', {
          detail: detail || {},
        }),
      )
    } catch (_) {}
  }

  var spaClickHandlerRef = null
  var spaPopstateHandlerRef = null

  function parseHtmlDocument(htmlText) {
    return new DOMParser().parseFromString(htmlText, 'text/html')
  }

  function swapDocumentRegions(fromDoc, selectors) {
    var swapped = false
    selectors.forEach(function (sel) {
      if (!sel) return
      var cur = document.querySelector(sel)
      var next = fromDoc.querySelector(sel)
      if (cur && next) {
        cur.innerHTML = next.innerHTML
        if (next.className) cur.className = next.className
        swapped = true
      }
    })
    return swapped
  }

  function createStyles(primary, background, headerColor) {
    const safePrimary = primary || '#2563eb'
    const safeBg = background || '#ffffff'
    const safeHeader = (headerColor && String(headerColor).trim()) || safePrimary
    const acwFont = "'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif"
    function hexToRgb(hex) {
      var h = String(hex || '#2563eb').replace('#', '')
      if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2]
      if (h.length !== 6) return { r: 37, g: 99, b: 235 }
      return {
        r: parseInt(h.slice(0, 2), 16),
        g: parseInt(h.slice(2, 4), 16),
        b: parseInt(h.slice(4, 6), 16),
      }
    }
    function clampChannel(n) {
      return Math.max(0, Math.min(255, Math.round(n)))
    }
    function darkenHex(hex, amount) {
      var rgb = hexToRgb(hex)
      var f = 1 - amount
      return '#' + [rgb.r * f, rgb.g * f, rgb.b * f].map(function (c) {
        return clampChannel(c).toString(16).padStart(2, '0')
      }).join('')
    }
    function mixHex(hex, hex2, weight) {
      var a = hexToRgb(hex)
      var b = hexToRgb(hex2)
      var w = Math.max(0, Math.min(1, weight))
      return '#' + [a.r + (b.r - a.r) * w, a.g + (b.g - a.g) * w, a.b + (b.b - a.b) * w].map(function (c) {
        return clampChannel(c).toString(16).padStart(2, '0')
      }).join('')
    }
    function rgbaFromHex(hex, alpha) {
      var rgb = hexToRgb(hex)
      return 'rgba(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ',' + alpha + ')'
    }
    function relativeLuminance(hex) {
      var rgb = hexToRgb(hex)
      function lin(c) {
        c = c / 255
        return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
      }
      return 0.2126 * lin(rgb.r) + 0.7152 * lin(rgb.g) + 0.0722 * lin(rgb.b)
    }
    const brandRgb = hexToRgb(safePrimary)
    const brandWashStrong = rgbaFromHex(safePrimary, 0.12)
    const brandWashMid = rgbaFromHex(safePrimary, 0.06)
    const brandWashSoft = rgbaFromHex(safePrimary, 0.04)
    const headerTop = mixHex(safeHeader, '#ffffff', 0.1)
    const headerBottom = darkenHex(safeHeader, 0.08)
    const headerOnLight = relativeLuminance(safeHeader) > 0.52
    const headerFg = headerOnLight ? '#0f172a' : '#ffffff'
    const headerFgMuted = headerOnLight ? 'rgba(15, 23, 42, 0.68)' : 'rgba(255, 255, 255, 0.82)'
    const headerFgSubtle = headerOnLight ? 'rgba(15, 23, 42, 0.48)' : 'rgba(255, 255, 255, 0.58)'
    const headerAccent = headerOnLight ? darkenHex(safeHeader, 0.18) : mixHex(safeHeader, '#ffffff', 0.35)
    const headerDivider = headerOnLight ? rgbaFromHex(darkenHex(safeHeader, 0.12), 0.22) : 'rgba(255, 255, 255, 0.16)'
    const headerHoverBg = headerOnLight ? 'rgba(15, 23, 42, 0.06)' : 'rgba(255, 255, 255, 0.12)'
    const brandBorder = rgbaFromHex(safePrimary, 0.22)
    const brandBorderMid = rgbaFromHex(safePrimary, 0.34)
    const brandShadow = rgbaFromHex(safePrimary, 0.2)
    const brandShadowStrong = rgbaFromHex(safePrimary, 0.32)
    const headerBorder = headerDivider
    const primaryLight = mixHex(safePrimary, '#ffffff', 0.1)
    const surfaceCard = '#ffffff'
    const brandGradientBtn = 'linear-gradient(135deg, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0) 42%), ' + safePrimary
    const brandGradientSoft = 'linear-gradient(145deg, ' + primaryLight + ' 0%, ' + safePrimary + ' 100%)'
    const headerGradient = 'linear-gradient(180deg, ' + headerTop + ' 0%, ' + safeHeader + ' 42%, ' + headerBottom + ' 100%)'
    return `
      @import url('https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&display=swap');
      :host, .acw * { box-sizing: border-box; }
      .spark-acw-container { position: fixed; z-index: 999; font-family: ${acwFont}; -webkit-font-smoothing: antialiased; }
      .spark-acw-container.bottom-right { right: 24px; bottom: 24px; }
      .spark-acw-container.middle-right { right: 24px; top: 50%; transform: translateY(-50%); }
      .acw-launcher-wrap { position: relative; display: inline-flex; border-radius: 999px; padding: 0; }
      .acw-launcher-wrap::before { content: ""; display: none; }
      @keyframes acw-launcher-glow { to { transform: rotate(360deg); } }
      .acw-launcher {
        position: relative; z-index: 1;
        background: linear-gradient(135deg, rgba(255,255,255,0.18) 0%, rgba(255,255,255,0) 55%), ${safePrimary};
        color: #fff; border: none; border-radius: 999px; padding: 10px 14px; cursor: pointer;
        display: inline-flex; align-items: center; justify-content: center; gap: 8px;
        box-shadow: 0 12px 40px ${brandShadowStrong}, 0 4px 12px rgba(0,0,0,0.12);
        font-size: 14px; transition: transform 0.2s, box-shadow 0.2s;
      }
      .acw-launcher:hover { transform: scale(1.03); box-shadow: 0 16px 44px ${brandShadowStrong}, 0 6px 14px rgba(0,0,0,0.14); }
      .acw-launcher:active { transform: scale(0.98); }
      .acw-launcher-fab { position: relative; width: 64px; height: 64px; padding: 0; border-radius: 50%; }
      .acw-launcher-fab .acw-launcher-sparkle, .acw-launcher-fab .acw-launcher-x { width: 28px; height: 28px; transition: transform 0.35s ease; }
      .acw-launcher-fab.is-open .acw-launcher-sparkle { transform: rotate(90deg) scale(0); opacity: 0; position: absolute; }
      .acw-launcher-fab.is-open .acw-launcher-x { transform: rotate(0deg) scale(1); opacity: 1; }
      .acw-launcher-fab:not(.is-open) .acw-launcher-x { transform: rotate(-90deg) scale(0); opacity: 0; position: absolute; }
      .acw-launcher-fab:not(.is-open) .acw-launcher-sparkle { position: relative; opacity: 1; }
      .acw-launcher img { width: 24px; height: 24px; object-fit: contain; border-radius: 50%; }
      .acw-launcher.vertical { flex-direction: column; padding: 14px 10px; min-width: 48px; height: auto; }
      .acw-launcher.vertical span { writing-mode: sideways-lr; text-orientation: mixed; letter-spacing: 0.5px; }
      .acw-header-left { display: flex; align-items: center; gap: 10px; flex: 1; min-width: 0; }
      .acw-header-icon { width: 40px; height: 40px; object-fit: cover; object-position: center 78%; flex-shrink: 0; border-radius: 50%;
        border: 2px solid ${headerBorder}; box-shadow: 0 2px 10px rgba(0,0,0,0.08); position: relative; }
      .acw-header-avatar-fallback {
        display: flex; align-items: center; justify-content: center;
        background: ${brandGradientSoft}; color: #fff;
      }
      .acw-header-icon-wrap { position: relative; display: inline-flex; }
      .acw-online-dot { position: absolute; right: 2px; bottom: 2px; width: 10px; height: 10px; border-radius: 50%;
        background: #22c55e; border: 2px solid #fff; box-shadow: 0 0 0 2px rgba(34,197,94,0.25); transition: background 0.2s ease, box-shadow 0.2s ease; }
      .acw-online-dot.is-connecting { background: #f59e0b; box-shadow: 0 0 0 2px rgba(245,158,11,0.3); animation: acw-dot-pulse 1.4s ease-in-out infinite; }
      .acw-online-dot.is-disconnected { background: #94a3b8; box-shadow: 0 0 0 2px rgba(148,163,184,0.25); }
      .acw-online-dot.is-just-connected { animation: acw-online-pop 0.55s ease-out; }
      @keyframes acw-online-pop {
        0% { transform: scale(0.7); box-shadow: 0 0 0 0 rgba(34,197,94,0.5); }
        60% { transform: scale(1.2); box-shadow: 0 0 0 6px rgba(34,197,94,0.15); }
        100% { transform: scale(1); box-shadow: 0 0 0 2px rgba(34,197,94,0.25); }
      }
      @keyframes acw-dot-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.55; } }
      .acw-header-title-block { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
      .acw-header-title-wrap { display: flex; align-items: center; gap: 8px; min-width: 0; }
      .acw-header-subtitle { font-size: 12px; font-weight: 600; color: ${headerFgMuted}; transition: color 0.2s ease; }
      .acw-header-subtitle.is-connected { color: ${headerAccent}; }
      .acw-header-subtitle.is-connecting { color: ${headerOnLight ? '#b45309' : '#fde68a'}; }
      .acw-header-subtitle.is-disconnected { color: ${headerFgSubtle}; }
      .acw-unread-badge { background: rgba(255,255,255,0.95); color: ${safePrimary}; font-size: 11px; font-weight: 700; min-width: 18px; height: 18px; border-radius: 9px; display: inline-flex; align-items: center; justify-content: center; padding: 0 5px; }
      .acw-unread-badge.hidden { display: none; }
      :host {
        /* Lock to light unless .spark-acw-theme-dark is set — prevents OS/browser
           dark mode (and Chrome Auto Dark) from restyling the embed. */
        color-scheme: only light;
        --acw-panel-height: min(max(650px, 60vh), calc(100vh - 24px));
        --acw-font: ${acwFont};
        --acw-text-xs: 11px;
        --acw-text-sm: 12px;
        --acw-text-base: 14px;
        --acw-text-md: 15px;
        --acw-text-lg: 16px;
        --acw-text-xl: 20px;
        --acw-text-2xl: 24px;
        --acw-text-display: 24px;
        --acw-leading: 1.45;
      }
      :host(.spark-acw-theme-light) { color-scheme: only light; }
      :host(.spark-acw-theme-dark) { color-scheme: only dark; }
      :host, .acw-panel {
        font-family: var(--acw-font);
        font-size: var(--acw-text-base);
        line-height: var(--acw-leading);
        -webkit-font-smoothing: antialiased;
      }
      .acw-panel {
        position: absolute; width: 380px; max-width: 92vw;
        height: var(--acw-panel-height);
        min-height: var(--acw-panel-height);
        max-height: var(--acw-panel-height);
        background: ${safeBg}; backdrop-filter: blur(24px); -webkit-backdrop-filter: blur(24px);
        border: 1px solid ${brandBorder}; border-radius: 20px;
        box-shadow: 0 20px 44px rgba(15, 23, 42, 0.14), 0 8px 20px ${brandShadow};
        display: none; flex-direction: column; overflow: hidden;
        z-index: 2; isolation: isolate;
        transition: width 0.28s cubic-bezier(0.22, 1, 0.36, 1),
          height 0.28s cubic-bezier(0.22, 1, 0.36, 1),
          min-height 0.28s cubic-bezier(0.22, 1, 0.36, 1),
          max-height 0.28s cubic-bezier(0.22, 1, 0.36, 1);
      }
      .acw-panel.is-expanded {
        width: min(680px, calc(100vw - 32px));
        max-width: calc(100vw - 32px);
      }
      .acw-panel.bottom-right { right: 0; bottom: 76px; }
      .acw-panel.middle-right { right: 80px; top: 50%; transform: translateY(-50%); }
      .acw-panel.top-right { right: 0; top: max(88px, var(--acw-viewport-top-offset, 88px)); }
      .acw-panel.bottom-left { left: 0; bottom: 76px; }
      .acw-panel.middle-left { left: 80px; top: 50%; transform: translateY(-50%); }
      .acw-panel.top-left { left: 0; top: max(88px, var(--acw-viewport-top-offset, 88px)); }
      .acw-header {
        position: relative; z-index: 2; flex-shrink: 0;
        background: ${headerGradient};
        color: ${headerFg}; padding: 14px 16px 12px; display: flex; align-items: center; justify-content: space-between;
        border-bottom: 1px solid ${headerDivider};
      }
      .acw-title { font-weight: 700; font-size: var(--acw-text-md); letter-spacing: -0.02em; line-height: 1.25; color: ${headerFg}; }
      .acw-close { background: transparent; border: none; color: ${headerFgSubtle}; cursor: pointer; font-size: 18px; line-height: 1;
        padding: 6px; width: 32px; height: 32px; border-radius: 999px; display: inline-flex; align-items: center; justify-content: center;
        transition: color 0.15s ease, background 0.15s ease; }
      .acw-close:hover { color: ${headerFg}; background: ${headerHoverBg}; }
      .acw-close svg { display: block; flex-shrink: 0; }
      .acw-header-actions { display: flex; align-items: center; gap: 2px; flex-shrink: 0; }
      .acw-header-expand:hover { color: ${headerFg}; background: ${headerHoverBg}; }
      .acw-header-disconnect { color: ${headerFgSubtle}; }
      .acw-header-disconnect:hover { color: ${headerOnLight ? '#dc2626' : '#fecaca'}; background: ${headerOnLight ? 'rgba(254, 226, 226, 0.65)' : 'rgba(239, 68, 68, 0.22)'}; }
      .acw-header-close:hover { color: ${headerFg}; background: ${headerHoverBg}; }
      .acw-messages {
        flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden;
        padding: 12px 16px 8px; display: flex; flex-direction: column; justify-content: flex-start; gap: 8px;
        background: ${safeBg};
        -webkit-overflow-scrolling: touch;
        overscroll-behavior: contain;
        scrollbar-gutter: stable;
      }
      .acw-messages-spacer {
        flex: 1 1 0; min-height: 0; width: 100%;
      }
      .acw-suggested-questions {
        flex-shrink: 0; display: none; flex-wrap: nowrap; gap: 6px;
        padding: 4px 12px 10px; overflow-x: auto; overflow-y: hidden;
        -webkit-overflow-scrolling: touch; scrollbar-width: thin;
        scrollbar-color: rgba(148, 163, 184, 0.45) transparent;
        background: ${safeBg};
      }
      .acw-panel[data-acw-view="chat"] .acw-suggested-questions.is-visible { display: flex !important; }
      .acw-composer {
        flex-shrink: 0; margin-top: auto; width: 100%;
        display: none; flex-direction: column; background: ${safeBg};
      }
      .acw-panel[data-acw-view="chat"] .acw-composer { display: flex !important; }
      .acw-suggested-questions::-webkit-scrollbar { height: 4px; }
      .acw-suggested-questions::-webkit-scrollbar-thumb {
        background: rgba(148, 163, 184, 0.45); border-radius: 999px;
      }
      .acw-suggested-question {
        border: 1px solid ${brandBorder}; background: ${surfaceCard}; color: #64748b;
        border-radius: 999px; padding: 6px 14px; font-size: 12px; font-family: inherit; font-weight: 500; cursor: pointer;
        transition: border-color 0.18s ease, color 0.18s ease, transform 0.18s ease, box-shadow 0.18s ease;
        flex-shrink: 0; white-space: nowrap; text-align: left;
      }
      .acw-suggested-question:hover {
        border-color: ${safePrimary}; color: ${safePrimary};
        transform: translateY(-2px); box-shadow: 0 4px 12px ${brandShadow};
      }
      .acw-suggested-question:active { transform: translateY(0) scale(0.98); }
      .acw-suggested-question.is-sending {
        border-color: ${safePrimary}; color: ${safePrimary}; background: ${brandWashSoft};
        transform: scale(0.96); opacity: 0.85;
      }
      .acw-suggested-question.acw-action-navigation {
        border-color: rgba(37, 99, 235, 0.35);
      }
      .acw-suggested-question.acw-action-open-url {
        border-color: rgba(16, 185, 129, 0.45);
      }
      .acw-msg {
        padding: 10px 14px; border-radius: 18px; max-width: 88%; font-size: var(--acw-text-base); line-height: var(--acw-leading);
        white-space: pre-wrap; word-break: break-word;
        animation: acw-msg-in 0.22s cubic-bezier(0.22, 1, 0.36, 1) both;
      }
      @keyframes acw-msg-in {
        from { opacity: 0; transform: translateY(8px); }
        to { opacity: 1; transform: translateY(0); }
      }
      .acw-msg-user {
        margin-left: auto; background: ${brandGradientBtn}; color: #fff;
        border-radius: 18px 18px 6px 18px;
        box-shadow: 0 4px 14px ${brandShadow};
      }
      .acw-msg-bot {
        margin-right: auto; background: ${surfaceCard}; color: #0f172a; border: 1px solid ${brandBorder};
        border-radius: 18px 18px 18px 6px; box-shadow: 0 2px 8px rgba(15,23,42,0.06);
      }
      .acw-msg-typing {
        margin-right: auto; display: inline-flex; align-items: center; gap: 3px; padding: 8px 12px; min-height: 0;
        background: #fff; border: 1px solid rgba(191, 219, 254, 0.9); border-radius: 18px 18px 18px 6px;
        box-shadow: 0 2px 8px rgba(15,23,42,0.06);
      }
      .acw-msg-typing span {
        width: 5px; height: 5px; border-radius: 50%; background: ${safePrimary}; opacity: 0.45;
        animation: acw-typing-dot 1.15s ease-in-out infinite;
      }
      .acw-msg-typing span:nth-child(2) { animation-delay: 0.2s; }
      .acw-msg-typing span:nth-child(3) { animation-delay: 0.4s; }
      @keyframes acw-typing-dot {
        0%, 70%, 100% { opacity: 0.35; transform: translateY(0); }
        35% { opacity: 1; transform: translateY(-2px); }
      }
      .acw-msg-img { max-width: 100%; border-radius: 16px; display: block; margin-top: 8px; max-height: 200px; object-fit: cover; }
      .acw-product-carousel-wrap {
        margin-right: auto; max-width: 100%; width: 100%;
        animation: acw-msg-in 0.22s cubic-bezier(0.22, 1, 0.36, 1) both;
      }
      .acw-product-carousel {
        display: flex; flex-wrap: nowrap; gap: 10px; overflow-x: auto; overflow-y: hidden;
        padding: 2px 2px 6px; margin-top: 4px;
        scroll-snap-type: x mandatory; -webkit-overflow-scrolling: touch;
        scrollbar-width: thin; scrollbar-color: rgba(148, 163, 184, 0.45) transparent;
      }
      .acw-product-carousel::-webkit-scrollbar { height: 4px; }
      .acw-product-carousel::-webkit-scrollbar-thumb {
        background: rgba(148, 163, 184, 0.45); border-radius: 999px;
      }
      .acw-product-card {
        flex: 0 0 72%; max-width: 220px; min-width: 168px;
        scroll-snap-align: start; scroll-snap-stop: always;
        border: 1px solid rgba(191, 219, 254, 0.9); border-radius: 14px;
        background: #fff; overflow: hidden;
        box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
        display: flex; flex-direction: column;
      }
      .acw-product-card-img-wrap {
        position: relative; width: 100%; aspect-ratio: 1 / 1; background: #f8fafc; overflow: hidden;
      }
      .acw-product-card-img {
        width: 100%; height: 100%; object-fit: cover; display: block;
      }
      .acw-product-card-body {
        padding: 10px 10px 12px; display: flex; flex-direction: column; gap: 6px; flex: 1 1 auto;
      }
      .acw-product-card-title {
        margin: 0; font-size: 13px; font-weight: 600; line-height: 1.35; color: #0f172a;
        display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
      }
      .acw-product-card-price-row {
        display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px; min-height: 18px;
      }
      .acw-product-card-price {
        font-size: 14px; font-weight: 700; color: ${safePrimary};
      }
      .acw-product-variants-btn {
        width: 100%; border: 1px solid rgba(191, 219, 254, 0.9); border-radius: 999px;
        padding: 6px 12px; font-size: 11px; font-weight: 600; font-family: inherit;
        cursor: pointer; background: #fff; color: #64748b;
        transition: border-color 0.15s ease, color 0.15s ease, background 0.15s ease;
      }
      .acw-product-variants-btn:hover {
        border-color: ${safePrimary}; color: ${safePrimary};
      }
      .acw-product-variants-btn.is-active {
        border-color: ${safePrimary}; color: ${safePrimary}; background: rgba(239, 246, 255, 0.9);
      }
      .acw-product-variants-panel {
        margin-top: 10px; padding-top: 10px; border-top: 1px solid rgba(226, 232, 240, 0.95);
        display: flex; flex-direction: column; gap: 8px;
      }
      .acw-product-variants-panel[hidden] { display: none !important; }
      .acw-product-variants-heading {
        margin: 0; font-size: 12px; font-weight: 600; color: #475569;
      }
      .acw-product-variant-row {
        display: flex; align-items: center; gap: 10px;
        border: 1px solid rgba(226, 232, 240, 0.95); border-radius: 12px;
        padding: 8px; background: #fff;
      }
      .acw-product-variant-row-img {
        width: 52px; height: 52px; border-radius: 8px; object-fit: cover; flex-shrink: 0;
        background: #f8fafc;
      }
      .acw-product-variant-row-body {
        flex: 1 1 auto; min-width: 0; display: flex; flex-direction: column; gap: 2px;
      }
      .acw-product-variant-row-title {
        margin: 0; font-size: 12px; font-weight: 600; color: #0f172a;
        white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
      }
      .acw-product-variant-row-price {
        margin: 0; font-size: 13px; font-weight: 700; color: ${safePrimary};
      }
      .acw-product-variant-row-cta {
        flex-shrink: 0; border: none; border-radius: 999px;
        padding: 6px 12px; font-size: 11px; font-weight: 600; font-family: inherit;
        cursor: pointer; background: ${safePrimary}; color: #fff;
      }
      .acw-product-card-cta {
        margin-top: auto; width: 100%; border: none; border-radius: 999px;
        padding: 7px 12px; font-size: 12px; font-weight: 600; font-family: inherit;
        cursor: pointer; background: ${safePrimary}; color: #fff;
        transition: opacity 0.15s ease, transform 0.15s ease;
      }
      .acw-product-card-cta:hover { opacity: 0.92; transform: translateY(-1px); }
      .acw-product-card-cta:active { transform: translateY(0); }
      .acw-product-show-more {
        display: inline-flex; align-items: center; gap: 4px; margin-top: 8px; padding: 0;
        border: none; background: transparent; color: ${safePrimary};
        font-size: 12px; font-weight: 600; font-family: inherit; cursor: pointer;
      }
      .acw-product-show-more:hover { text-decoration: underline; }
      .acw-input {
        flex-shrink: 0; width: 100%; box-sizing: border-box;
        padding: 10px 12px 12px; display: block;
        background: ${safeBg}; border-top: 1px solid rgba(226, 232, 240, 0.9);
      }
      .acw-composer-box {
        display: flex; flex-direction: column; gap: 6px; width: 100%; min-width: 0; box-sizing: border-box;
        border: 1.5px solid rgba(15, 23, 42, 0.88); border-radius: 16px;
        padding: 10px 12px 8px; background: ${surfaceCard}; overflow: hidden;
        transition: border-color 0.2s ease, box-shadow 0.2s ease;
      }
      .acw-composer-box:focus-within {
        border-color: ${safePrimary};
        box-shadow: 0 0 0 3px ${brandWashMid};
      }
      .acw-composer-field {
        width: 100%; min-width: 0; min-height: 24px; max-height: 96px;
        padding: 0; resize: none; border: none; background: transparent;
        font-size: var(--acw-text-base); font-family: inherit; line-height: var(--acw-leading);
        box-sizing: border-box; overflow: hidden; color: #0f172a;
      }
      .acw-composer-field::placeholder { color: #94a3b8; }
      .acw-composer-field:focus { outline: none; }
      .acw-composer-field:disabled { opacity: 0.6; cursor: not-allowed; }
      .acw-composer-actions {
        display: flex; align-items: center; justify-content: flex-end; gap: 2px;
        min-height: 32px;
      }
      .acw-input-attach {
        display: none; flex-shrink: 0; width: 32px; height: 32px; border: none; border-radius: 8px;
        background: transparent; cursor: pointer; align-items: center; justify-content: center;
        color: #64748b; transition: color 0.15s ease, background 0.15s ease;
      }
      .acw-file-upload-on .acw-input-attach { display: inline-flex; }
      .acw-input-attach:hover:not(:disabled) { color: ${safePrimary}; background: ${brandWashSoft}; }
      .acw-input-attach:disabled { opacity: 0.45; cursor: not-allowed; }
      .acw-input-attach svg { width: 20px; height: 20px; display: block; }
      .acw-attachments {
        display: none; flex-direction: column; flex-wrap: nowrap; gap: 6px;
        width: 100%; min-width: 0; max-width: 100%; box-sizing: border-box;
        padding: 0 0 4px; background: transparent; overflow: hidden;
      }
      .acw-attachment-chip {
        position: relative; display: flex; align-items: center; gap: 8px;
        width: 100%; min-width: 0; max-width: 100%; box-sizing: border-box;
        padding: 6px 8px 6px 6px; border-radius: 12px; border: 1px solid rgba(148, 163, 184, 0.45);
        background: #fff; font-size: 12px; color: #334155; overflow: hidden;
      }
      .acw-attachment-chip.is-uploading { opacity: 0.72; }
      .acw-attachment-chip.is-error { border-color: rgba(239, 68, 68, 0.55); color: #b91c1c; }
      .acw-attachment-thumb {
        width: 36px; height: 36px; border-radius: 8px; object-fit: cover; flex-shrink: 0; background: #e2e8f0;
      }
      .acw-attachment-name {
        flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
      }
      .acw-attachment-remove {
        flex-shrink: 0; width: 22px; height: 22px; border: none; border-radius: 999px;
        background: rgba(148, 163, 184, 0.2); color: #475569; cursor: pointer; line-height: 1; font-size: 14px;
      }
      .acw-attachment-remove:hover { background: rgba(239, 68, 68, 0.15); color: #b91c1c; }
      .acw-input-send {
        flex-shrink: 0; width: 32px; height: 32px; border-radius: 999px; border: none; cursor: pointer;
        background: ${brandGradientBtn}; color: #fff;
        display: inline-flex; align-items: center; justify-content: center;
        box-shadow: 0 2px 8px ${brandShadow};
        transition: transform 0.18s ease, filter 0.18s ease, box-shadow 0.18s ease, opacity 0.18s ease;
      }
      .acw-input-send:hover:not(:disabled) { filter: brightness(1.05); transform: scale(1.04); }
      .acw-input-send:active:not(:disabled) { transform: scale(0.96); }
      .acw-input-send:disabled, .acw-input-send.is-muted {
        opacity: 0.38; cursor: not-allowed; filter: grayscale(0.2); transform: none;
        box-shadow: none;
      }
      .acw-input-send svg { width: 18px; height: 18px; display: block; }
      .acw-connection-banner {
        flex-shrink: 0; display: none; align-items: center; justify-content: space-between; gap: 10px;
        padding: 8px 12px; font-size: 12px; font-weight: 600; color: #92400e;
        background: linear-gradient(90deg, rgba(254,243,199,0.95), rgba(255,251,235,0.98));
        border-bottom: 1px solid rgba(251, 191, 36, 0.35);
      }
      .acw-connection-banner.is-reconnecting { color: #1d4ed8; background: linear-gradient(90deg, rgba(219,234,254,0.95), rgba(239,246,255,0.98)); border-bottom-color: rgba(147, 197, 253, 0.45); }
      .acw-connection-banner-text { flex: 1; min-width: 0; line-height: 1.35; }
      .acw-connection-banner-btn {
        flex-shrink: 0; border: 1px solid rgba(37, 99, 235, 0.35); background: #fff; color: ${safePrimary};
        border-radius: 999px; padding: 5px 12px; font-size: 11px; font-weight: 700; font-family: inherit;
        cursor: pointer; transition: background 0.15s ease, transform 0.15s ease;
      }
      .acw-connection-banner-btn:hover:not(:disabled) { background: rgba(239, 246, 255, 0.9); transform: translateY(-1px); }
      .acw-connection-banner-btn:disabled { opacity: 0.5; cursor: not-allowed; }
      .acw-connection-banner.is-success {
        color: #166534; background: linear-gradient(90deg, rgba(220,252,231,0.98), rgba(240,253,244,0.98));
        border-bottom-color: rgba(134, 239, 172, 0.45);
      }
      .acw-continuity-toast {
        position: absolute; top: 56px; left: 12px; right: 12px; z-index: 12;
        display: flex; align-items: center; gap: 8px; padding: 8px 12px;
        font-size: 12px; font-weight: 600; color: #1e40af;
        background: rgba(255,255,255,0.96); border: 1px solid rgba(147, 197, 253, 0.65);
        border-radius: 12px; box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
        opacity: 0; visibility: hidden; transform: translateY(-6px);
        transition: opacity 0.22s ease, transform 0.22s ease, visibility 0.22s ease;
        pointer-events: none;
      }
      .acw-continuity-toast.visible {
        opacity: 1; visibility: visible; transform: translateY(0);
      }
      .acw-continuity-toast svg { width: 14px; height: 14px; flex-shrink: 0; color: ${safePrimary}; }
      .acw-msg-wrap {
        display: flex; flex-direction: column; align-items: flex-start; align-self: flex-start;
        width: fit-content; max-width: 88%; gap: 4px;
      }
      .acw-msg-wrap:has(.acw-product-carousel-wrap) {
        width: 100%; align-self: flex-start;
      }
      .acw-msg-wrap.is-user { align-items: flex-end; align-self: flex-end; max-width: 88%; }
      .acw-msg-wrap:has(.acw-msg-bot) .acw-msg-actions { display: flex; }
      .acw-msg-wrap:has(.acw-msg-user) .acw-msg-actions,
      .acw-msg-user + .acw-msg-actions { display: none !important; }
      .acw-msg-actions {
        display: none; align-items: center; flex-wrap: wrap; gap: 6px;
        width: auto; max-width: 100%; align-self: flex-start;
        opacity: 0; transition: opacity 0.18s ease;
      }
      .acw-msg-wrap:hover .acw-msg-actions,
      .acw-msg-wrap:focus-within .acw-msg-actions { opacity: 1; }
      .acw-msg-feedback {
        display: inline-flex; align-items: center; gap: 2px;
      }
      .acw-msg-action-btn {
        border: 1px solid rgba(148, 163, 184, 0.35); background: rgba(255,255,255,0.92);
        color: #64748b; border-radius: 999px; padding: 2px 8px; font-size: 10px; font-weight: 600;
        font-family: inherit; cursor: pointer; transition: color 0.15s ease, border-color 0.15s ease, background 0.15s ease;
      }
      .acw-msg-action-btn:hover { color: ${safePrimary}; border-color: rgba(96, 165, 250, 0.55); background: #fff; }
      .acw-msg-action-btn.is-active { color: ${safePrimary}; border-color: ${safePrimary}; background: rgba(239, 246, 255, 0.95); }
      .acw-msg-action-btn.is-icon {
        padding: 2px; width: auto; height: auto; min-width: 0;
        display: inline-flex; align-items: center; justify-content: center;
        border: none; background: transparent; border-radius: 0;
      }
      .acw-msg-action-btn.is-icon:hover { color: ${safePrimary}; background: transparent; border: none; }
      .acw-msg-action-btn.is-icon.is-active { color: ${safePrimary}; background: transparent; border: none; }
      .acw-msg-action-btn.is-icon svg { display: block; flex-shrink: 0; }
      .acw-handover-ack {
        align-self: stretch; flex-shrink: 0; width: 100%;
        display: flex; align-items: center; gap: 10px;
        margin: 4px 0; padding: 0 2px; box-sizing: border-box;
        animation: acw-msg-in 0.22s cubic-bezier(0.22, 1, 0.36, 1) both;
      }
      .acw-handover-ack-line {
        flex: 1 1 auto; min-width: 0; height: 1px;
        background: rgba(148, 163, 184, 0.38);
      }
      .acw-handover-ack-icon {
        flex-shrink: 0;
        display: inline-flex; align-items: center; justify-content: center;
        width: 34px; height: 34px;
        border-radius: 999px; border: 1px solid rgba(148, 163, 184, 0.28);
        background: rgba(241, 245, 249, 0.95); color: #64748b;
      }
      .acw-handover-ack-icon svg { display: block; flex-shrink: 0; }
      .acw-rail-voice-eq {
        display: none; align-items: flex-end; justify-content: center; gap: 3px;
        width: 100%; height: 14px; pointer-events: none;
      }
      .acw-rail-voice-overlay.is-connected .acw-rail-voice-eq { display: flex; }
      .acw-rail-voice-eq span {
        width: 3px; min-height: 4px; border-radius: 2px; background: ${safePrimary}; opacity: 0.75;
        transition: height 0.08s ease;
      }
      .acw-footer {
        flex-shrink: 0; font-size: var(--acw-text-xs); color: #94a3b8; padding: 6px 10px 10px; text-align: center;
        background: ${safeBg}; border-top: 1px solid rgba(226, 232, 240, 0.9);
      }
      .acw-home-footer {
        margin-top: auto; flex-shrink: 0; border-top: none; background: transparent;
        padding: 16px 4px 4px;
      }
      .acw-footer a { color: #64748b; text-decoration: none; }
      .acw-footer a:hover { text-decoration: underline; }
      .acw-status { display: none !important; }
      .acw-status-disconnect { margin-left: auto; flex-shrink: 0; width: 32px; height: 32px; padding: 6px; }
      .acw-dot { width: 8px; height: 8px; border-radius: 999px; box-shadow: 0 0 0 6px rgba(0,0,0,0.04); }
      .acw-dot.connected { background: #22c55e; box-shadow: 0 0 0 6px rgba(34,197,94,0.18); }
      .acw-dot.disconnected { background: #ef4444; box-shadow: 0 0 0 6px rgba(239,68,68,0.18); }
      .acw-customer-form {
        position: relative;
        flex: 1 1 auto; min-height: 0;
        background: ${safeBg}; z-index: 1;
        display: none; flex-direction: column; align-items: stretch; justify-content: flex-start;
        padding: 8px 24px 28px; overflow-y: auto; overflow-x: hidden; box-sizing: border-box;
        scrollbar-gutter: stable;
      }
      .acw-panel[data-acw-view="form"] .acw-header { display: none !important; }
      .acw-form-hero-banner {
        position: relative;
        margin: -8px -24px 0; width: calc(100% + 48px); max-width: calc(100% + 48px);
        aspect-ratio: 38 / 15;
        height: auto; min-height: 120px; max-height: 180px;
        overflow: hidden; background: #e2e8f0;
        border-radius: 20px 20px 0 0;
        flex-shrink: 0;
      }
      .acw-form-hero-banner-media {
        position: absolute; inset: 0; overflow: hidden;
        border-radius: inherit; background: #e2e8f0;
      }
      .acw-form-hero-banner-media img {
        display: block; width: 100%; height: 100%; max-width: 100%;
        object-fit: cover; object-position: center 30%;
      }
      .acw-form-hero-close {
        position: absolute; top: 12px; right: 12px; z-index: 13;
        width: 32px; height: 32px; border-radius: 999px; border: none; padding: 0;
        display: flex; align-items: center; justify-content: center;
        background: rgba(15, 23, 42, 0.45); color: #fff; cursor: pointer;
        transition: background 0.15s ease;
      }
      .acw-form-hero-close:hover { background: rgba(15, 23, 42, 0.65); }
      .acw-form-hero-close svg { display: block; width: 16px; height: 16px; }
      .acw-form-hero-name {
        position: absolute; top: 14px; left: 50%; transform: translateX(-50%); z-index: 12;
        max-width: calc(100% - 90px); box-sizing: border-box;
        padding: 6px 14px; border-radius: 10px;
        background: rgba(15, 23, 42, 0.45);
        color: #fff; font-size: var(--acw-text-sm); font-weight: 700; letter-spacing: -0.01em;
        white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
      }
      .acw-form-identity { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 0 0 2px; flex-shrink: 0; position: relative; z-index: 10; }
      .acw-form-avatar-wrap { position: relative; flex-shrink: 0; display: inline-flex; margin-top: -22px; }
      .acw-form-avatar {
        width: 44px; height: 44px; border-radius: 999px;
        background: ${safePrimary}; color: #fff; line-height: 1;
        font-size: var(--acw-text-lg); font-weight: 700;
        display: flex; align-items: center; justify-content: center;
        border: 3px solid #ffffff;
        box-shadow: 0 6px 14px rgba(15, 23, 42, 0.22);
      }
      .acw-form-back {
        flex-shrink: 0; margin-top: -22px;
        width: 44px; height: 44px; border-radius: 999px;
        display: flex; align-items: center; justify-content: center;
        background: #f1f5f9; color: #475569; border: 3px solid #ffffff; cursor: pointer; padding: 0;
        box-shadow: 0 6px 14px rgba(15, 23, 42, 0.18);
        transition: background 0.15s ease, color 0.15s ease;
      }
      .acw-form-back:hover { background: #e2e8f0; color: #0f172a; }
      .acw-form-back svg { width: 18px; height: 18px; }
      .acw-form-head-col { width: 100%; min-width: 0; margin: 0 0 16px; }
      .acw-customer-form h3 { margin: 0; font-size: var(--acw-text-lg); font-weight: 700; color: #0f172a; text-align: center; letter-spacing: -0.02em; }
      .acw-customer-form p { margin: 0; font-size: var(--acw-text-sm); color: #64748b; text-align: left; line-height: var(--acw-leading); }
      .acw-customer-form .field { margin-bottom: 16px; min-width: 0; width: 100%; box-sizing: border-box; }
      .acw-customer-form .field.acw-field-phone { margin-bottom: 0; }
      .acw-customer-form label { display: block; font-size: var(--acw-text-xs); font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 8px; color: #475569; }
      .acw-customer-form input {
        display: block; width: 100%; max-width: 100%; min-height: 0; height: auto; box-sizing: border-box;
        padding: 12px 14px; border: 2px solid rgba(37, 99, 235, 0.2); border-radius: 12px; font-size: var(--acw-text-base); line-height: var(--acw-leading);
        transition: border-color 0.2s, box-shadow 0.2s;
        background: #fff;
        color: #0f172a;
      }
      .acw-customer-form .acw-phone-row { display: flex; gap: 8px; align-items: stretch; }
      .acw-customer-form .acw-phone-code-wrap {
        flex: 0 0 92px; width: 92px; min-width: 92px; max-width: 92px; position: relative;
      }
      .acw-customer-form .acw-phone-number-wrap { flex: 1 1 auto; min-width: 0; display: flex; }
      .acw-customer-form .acw-phone-number-wrap input { flex: 1; min-width: 0; }
      .acw-phone-code-switch { position: relative; width: 100%; height: 100%; box-sizing: border-box; }
      .acw-phone-code-native {
        position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;
        clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;
      }
      .acw-phone-code-trigger {
        display: flex; align-items: center; justify-content: flex-start; gap: 4px;
        width: 100%; height: 100%; min-height: 48px; box-sizing: border-box;
        padding: 12px 8px; border: 2px solid rgba(37, 99, 235, 0.2); border-radius: 12px;
        background: #fff; color: #0f172a; font-size: 14px; font-family: inherit; line-height: 1.4;
        cursor: pointer; transition: border-color 0.2s, box-shadow 0.2s;
      }
      .acw-phone-code-trigger:hover { border-color: rgba(37, 99, 235, 0.35); }
      .acw-phone-code-trigger:focus { outline: none; border-color: #60a5fa; box-shadow: 0 0 0 4px rgba(191, 219, 254, 0.55); }
      .acw-phone-code-trigger[aria-expanded="true"] { border-color: #60a5fa; box-shadow: 0 0 0 4px rgba(191, 219, 254, 0.55); }
      .acw-phone-code-flag {
        flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;
        width: 18px; height: 14px; font-size: 16px; line-height: 1;
      }
      .acw-phone-code-dial {
        flex: 0 0 auto; min-width: 0; font-weight: 600; font-size: 13px;
        font-variant-numeric: tabular-nums; letter-spacing: -0.01em; white-space: nowrap;
      }
      .acw-phone-code-chevron {
        flex-shrink: 0; margin-left: auto; display: flex; align-items: center; color: #94a3b8;
        transition: transform 0.2s;
      }
      .acw-phone-code-chevron svg { width: 14px; height: 14px; display: block; }
      .acw-phone-code-trigger[aria-expanded="true"] .acw-phone-code-chevron { transform: rotate(180deg); }
      .acw-phone-code-menu {
        display: none; flex-direction: column; position: absolute; left: 0; right: auto;
        top: calc(100% + 6px); z-index: 50;
        width: min(280px, calc(100vw - 48px)); min-width: 240px; max-height: 280px;
        box-sizing: border-box; background: #fff; border: 1px solid rgba(37, 99, 235, 0.15);
        border-radius: 12px; box-shadow: 0 12px 32px rgba(15, 23, 42, 0.12);
        overflow: hidden;
      }
      .acw-phone-code-menu.is-open { display: flex; }
      .acw-phone-code-menu.is-open.acw-phone-code-menu--floating { position: absolute; z-index: 50; }
      .acw-phone-code-search-wrap {
        flex-shrink: 0; padding: 8px; border-bottom: 1px solid rgba(37, 99, 235, 0.1); background: #fff;
      }
      .acw-phone-code-search {
        display: block; width: 100%; box-sizing: border-box;
        padding: 10px 12px; border: 1px solid rgba(37, 99, 235, 0.2); border-radius: 8px;
        font-size: 14px; line-height: 1.4; font-family: inherit; color: #0f172a; background: #f8fafc;
        transition: border-color 0.2s, box-shadow 0.2s, background 0.2s;
      }
      .acw-phone-code-search::placeholder { color: #94a3b8; }
      .acw-phone-code-search:focus {
        outline: none; background: #fff; border-color: #60a5fa;
        box-shadow: 0 0 0 3px rgba(191, 219, 254, 0.55);
      }
      .acw-phone-code-list {
        flex: 1 1 auto; min-height: 0; max-height: 220px; overflow-y: auto; padding: 6px; box-sizing: border-box;
      }
      .acw-phone-code-empty {
        display: none; padding: 16px 12px; text-align: center; font-size: 13px; color: #94a3b8;
      }
      .acw-phone-code-empty.is-visible { display: block; }
      .acw-phone-code-option {
        display: flex; align-items: center; gap: 10px; width: 100%; box-sizing: border-box;
        padding: 9px 10px; margin: 0; border: none; border-radius: 8px;
        background: transparent; text-align: left; font-size: 14px; font-family: inherit; color: #0f172a; cursor: pointer;
      }
      .acw-phone-code-option:hover,
      .acw-phone-code-option.is-active { background: rgba(37, 99, 235, 0.08); }
      .acw-phone-code-option.is-selected { background: rgba(37, 99, 235, 0.12); font-weight: 600; color: ${safePrimary}; }
      .acw-phone-code-option-flag {
        flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center;
        width: 22px; height: 16px; font-size: 18px; line-height: 1;
      }
      .acw-phone-code-option-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
      .acw-phone-code-option-dial { flex-shrink: 0; font-weight: 600; color: #64748b; font-variant-numeric: tabular-nums; }
      .acw-phone-code-option.is-selected .acw-phone-code-option-dial { color: ${safePrimary}; }
      .acw-customer-form .acw-phone-row input {
        min-height: 48px; height: 100%;
      }
      .acw-customer-form input:focus { outline: none; border-color: ${safePrimary}; box-shadow: 0 0 0 4px ${brandWashMid}; }
      .acw-customer-form .field.acw-field-invalid input,
      .acw-customer-form .field.acw-field-invalid .acw-phone-code-trigger {
        border-color: #f87171;
        box-shadow: 0 0 0 4px rgba(254, 202, 202, 0.45);
      }
      .acw-customer-form .acw-field-error {
        margin: 6px 0 0;
        font-size: 12px;
        line-height: 1.35;
        color: #dc2626;
      }
      .acw-customer-form .acw-form-error {
        margin: 0 0 10px;
        padding: 10px 12px;
        border-radius: 10px;
        font-size: 13px;
        line-height: 1.4;
        color: #b91c1c;
        background: #fef2f2;
        border: 1px solid #fecaca;
        text-align: center;
      }
      .acw-panel[data-acw-view="form"] .acw-customer-form {
        overflow-x: visible;
        overflow-y: auto;
      }
      .acw-form-hero-banner .acw-lang-switch-field {
        position: absolute;
        top: 12px;
        left: 12px;
        z-index: 14;
        width: auto;
        margin: 0;
        padding: 0;
        pointer-events: auto;
      }
      .acw-lang-switch-sr-label {
        position: absolute;
        width: 1px;
        height: 1px;
        padding: 0;
        margin: -1px;
        overflow: hidden;
        clip: rect(0, 0, 0, 0);
        white-space: nowrap;
        border: 0;
      }
      .acw-lang-switch { position: relative; width: auto; box-sizing: border-box; }
      .acw-lang-switch-native {
        position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;
        clip: rect(0,0,0,0); white-space: nowrap; border: 0; opacity: 0; pointer-events: none;
      }
      .acw-lang-switch-field .acw-lang-switch-trigger {
        display: flex;
        align-items: center;
        justify-content: center;
        gap: 0;
        width: 32px;
        height: 32px;
        padding: 0;
        box-sizing: border-box;
        border-radius: 999px;
        border: 1px solid rgba(255, 255, 255, 0.5);
        background: rgba(15, 23, 42, 0.72);
        color: #fff;
        cursor: pointer;
        font-size: 14px;
        font-family: inherit;
        text-align: left;
        transition: background 0.15s ease;
      }
      .acw-lang-switch-field .acw-lang-switch-current,
      .acw-lang-switch-field .acw-lang-switch-chevron { display: none !important; }
      .acw-lang-switch-trigger:hover { background: rgba(15, 23, 42, 0.65); }
      .acw-lang-switch-trigger:focus { outline: none; background: rgba(15, 23, 42, 0.65); }
      .acw-lang-switch-trigger[aria-expanded="true"] { background: rgba(15, 23, 42, 0.65); }
      .acw-lang-switch-field .acw-lang-switch-icon svg { width: 16px; height: 16px; stroke: #fff; }
      .acw-lang-switch-icon { flex-shrink: 0; display: flex; align-items: center; justify-content: center; color: #fff; }
      .acw-lang-switch-icon svg { width: 16px; height: 16px; stroke: #fff; }
      .acw-lang-switch-current { flex: 1; min-width: 0; font-weight: 500; line-height: 1.3; }
      .acw-lang-switch-chevron { flex-shrink: 0; display: flex; color: #94a3b8; transition: transform 0.2s; }
      .acw-lang-switch-chevron svg { width: 18px; height: 18px; }
      .acw-lang-switch-trigger[aria-expanded="true"] .acw-lang-switch-chevron { transform: rotate(180deg); }
      .acw-lang-switch-field .acw-lang-switch-menu {
        left: 0;
        right: auto;
        min-width: 220px;
        width: max-content;
        max-width: min(280px, calc(100vw - 48px));
      }
      .acw-lang-switch-menu.is-open.acw-lang-switch-menu--floating {
        position: absolute;
        z-index: 50;
        right: auto;
      }
      .acw-lang-switch-menu {
        display: none; position: absolute; left: 0; right: 0; top: calc(100% + 8px); z-index: 40;
        max-height: 220px; overflow-y: auto; box-sizing: border-box;
        background: #fff; border: 1px solid rgba(37, 99, 235, 0.15); border-radius: 12px;
        box-shadow: 0 12px 32px rgba(15, 23, 42, 0.12); padding: 6px;
      }
      .acw-lang-switch-menu.is-open { display: block; }
      .acw-lang-option {
        display: block; width: 100%; box-sizing: border-box; padding: 10px 12px; margin: 0; border: none; border-radius: 8px;
        background: transparent; text-align: left; font-size: 14px; font-family: inherit; color: #0f172a; cursor: pointer;
      }
      .acw-lang-option:hover { background: rgba(37, 99, 235, 0.08); }
      .acw-lang-option.is-selected { background: rgba(37, 99, 235, 0.12); font-weight: 600; color: ${safePrimary}; }
      .acw-customer-form .actions { display: flex; flex-direction: column; gap: 12px; margin-top: auto; padding-top: 20px; margin-bottom: 6px; min-width: 0; width: 100%; box-sizing: border-box; }
      .acw-form-footer { flex-shrink: 0; font-size: var(--acw-text-xs); color: #94a3b8; padding: 4px 0 0; text-align: center; background: transparent; border-top: none; }
      .acw-customer-form .actions-buttons { display: flex; flex-direction: row; align-items: stretch; gap: 10px; min-width: 0; width: 100%; box-sizing: border-box; }
      .acw-customer-form .actions-buttons.single { justify-content: center; }
      .acw-customer-form .actions-buttons button {
        min-width: 0; box-sizing: border-box; padding: 12px 12px; border: none; border-radius: 12px;
        font-size: var(--acw-text-md); font-weight: 600; cursor: pointer;
        white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
      }
      .acw-customer-form .submit-btn {
        flex: 13 1 0%; min-height: 48px; padding-top: 12px; padding-bottom: 12px; background: ${brandGradientBtn}; color: #fff;
        box-shadow: 0 8px 22px ${brandShadowStrong};
      }
      .acw-customer-form .actions-buttons.single .submit-btn { flex: 0 1 auto; width: 100%; max-width: 260px; }
      .acw-customer-form .submit-btn:hover { filter: brightness(1.05); transform: scale(1.02); }
      .acw-customer-form .skip-btn { flex: 7 1 0%; background: #f8fafc; color: #64748b; border: 1px solid rgba(0,0,0,0.08); }
      .acw-customer-form .skip-btn:hover { background: #f1f5f9; }
      .acw-start-cta { flex: 1 1 auto; display: flex; flex-direction: column; align-items: stretch; justify-content: flex-start; padding: 12px 14px 16px; min-height: 0; overflow-y: auto; }
      .acw-panel[data-acw-view="home"] .acw-start-cta { display: flex !important; }
      .acw-panel[data-acw-view="home"] .acw-messages,
      .acw-panel[data-acw-view="home"] .acw-composer,
      .acw-panel[data-acw-view="home"] .acw-customer-form,
      .acw-panel[data-acw-view="home"] .acw-voice-panel,
      .acw-panel[data-acw-view="home"] .acw-suggested-questions { display: none !important; }
      .acw-panel[data-acw-view="form"] .acw-composer,
      .acw-panel[data-acw-view="form"] .acw-suggested-questions,
      .acw-panel[data-acw-view="voice"] .acw-composer,
      .acw-panel[data-acw-view="voice"] .acw-suggested-questions { display: none !important; }
      .acw-panel[data-acw-view="connecting"] .acw-composer,
      .acw-panel[data-acw-view="connecting"] .acw-suggested-questions { display: none !important; }
      .acw-start-cta-mode { justify-content: flex-start; padding-top: 16px; height: auto; flex: 1 1 auto; min-height: 0; }
      .acw-start-cta button { background: ${surfaceCard}; color: #0f172a; border: 2px solid ${brandBorder}; border-radius: 25px; font-size: var(--acw-text-md); font-weight: 600; cursor: pointer; box-shadow: 0 4px 10px rgba(0,0,0,0.06); }
      .acw-start-cta button:hover { background: #f8fafc; border-color: ${safePrimary}; }
      /* Mode selector should size to its contents (no forced scroll height) */
      .acw-mode-selector { flex: 0 0 auto; display: flex; flex-direction: column; align-items: stretch; padding: 0 4px 8px; gap: 0; overflow: visible; }
      .acw-mode-hero-icon { width: 72px; height: 72px; margin: 0 auto 16px; border-radius: 50%; background: transparent; display: flex; align-items: center; justify-content: center; box-shadow: 0 12px 28px rgba(15,23,42,0.10); border: 1px solid rgba(0,0,0,0.06); overflow: hidden; }
      .acw-mode-hero-icon svg { width: 36px; height: 36px; color: ${safePrimary}; }
      .acw-mode-hero-icon img { width: 100%; height: 100%; padding: 12px; object-fit: contain; border-radius: 0; background: transparent; }
      .acw-mode-heading {
        margin: 0 0 6px; font-size: var(--acw-text-display); font-weight: 700; line-height: 1.2; text-align: center; letter-spacing: -0.02em;
        color: ${safePrimary};
      }
      .acw-mode-sub { margin: 0 0 20px; font-size: var(--acw-text-base); font-weight: 400; color: #64748b; text-align: center; line-height: var(--acw-leading); }
      .acw-mode-options { display: flex; flex-direction: column; gap: 12px; flex: 0 0 auto; height: auto; }
      .acw-mode-btn {
        display: flex; flex-direction: row; align-items: center; gap: 14px; width: 100%; min-height: 0; padding: 16px 18px;
        background: ${surfaceCard}; border: 1px solid rgba(0,0,0,0.06); border-radius: 24px; cursor: pointer;
        transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s; text-align: left;
        box-shadow: 0 4px 14px rgba(15,23,42,0.06); position: relative; overflow: hidden;
      }
      .acw-mode-btn::before {
        /* Sweeping highlight band (moves left -> right) */
        content: ""; position: absolute; top: 0; right: 0; bottom: 0; left: 0px; opacity: 0; pointer-events: none;
        border-radius: inherit;
        border-top-left-radius: 0;
        border-bottom-left-radius: 0;
        z-index: 0;
        transform: translateX(-100%);
        background: linear-gradient(
          90deg,
          rgba(255,255,255,0) 0%,
          rgba(255,255,255,0) 35%,
          var(--acw-sweep, rgba(37, 99, 235, 0.22)) 50%,
          rgba(255,255,255,0) 65%,
          rgba(255,255,255,0) 100%
        );
        transition: opacity 0.15s cubic-bezier(.4, 0, .2, 1);
        mix-blend-mode: normal;
      }
      .acw-mode-btn::after {
        /* Static subtle highlight that remains after sweep */
        content: ""; position: absolute; top: 0; right: 0; bottom: 0; left: 0px; opacity: 0; pointer-events: none;
        border-radius: inherit;
        border-top-left-radius: 0;
        border-bottom-left-radius: 0;
        z-index: 0;
        transition: opacity 0.2s cubic-bezier(.4, 0, .2, 1) 0.12s;
      }
      @keyframes acw-sweep-ltr {
        0% { transform: translateX(-60%); opacity: 0; }
        10% { opacity: 1; }
        100% { transform: translateX(60%); opacity: 0; }
      }
      .acw-mode-btn:hover { transform: translateY(-2px) scale(1.0); box-shadow: 0 12px 28px ${brandShadow}; }
      .acw-mode-btn:hover::before { animation: acw-sweep-ltr 0.65s cubic-bezier(.4, 0, .2, 1) both; }
      .acw-mode-btn:hover::after { opacity: 1; }
      .acw-mode-btn > * { position: relative; z-index: 1; }
      .acw-mode-btn .acw-mode-icon { width: 48px; height: 48px; flex-shrink: 0; border-radius: 16px; display: flex; align-items: center; justify-content: center; box-shadow: 0 10px 15px -3px rgba(15,23,42,0.12); }
      .acw-mode-icon-chat { background: ${brandGradientBtn}; box-shadow: 0 8px 18px ${brandShadow}; }
      .acw-mode-icon-voice { background: linear-gradient(135deg, rgba(255,255,255,0.18) 0%, rgba(255,255,255,0) 42%), #7c3aed; box-shadow: 0 8px 18px rgba(124,58,237,0.28); }
      .acw-mode-icon-whatsapp { background: linear-gradient(135deg, rgba(255,255,255,0.18) 0%, rgba(255,255,255,0) 42%), #16a34a; box-shadow: 0 8px 18px rgba(22,163,74,0.28); }
      .acw-mode-btn .acw-mode-icon svg { width: 22px; height: 22px; color: #fff; }
      .acw-mode-text { display: flex; flex-direction: column; gap: 4px; min-width: 0; flex: 1; }
      .acw-mode-title { font-size: var(--acw-text-md); font-weight: 700; color: #0f172a; margin-bottom: 2px; }
      .acw-mode-desc { font-size: var(--acw-text-sm); font-weight: 400; color: #64748b; line-height: var(--acw-leading); }
      .acw-mode-chevron { flex-shrink: 0; width: 22px; height: 22px; color: #94a3b8; opacity: 0.5; transition: transform 0.2s, opacity 0.2s; }
      .acw-mode-btn:hover .acw-mode-chevron { opacity: 1; transform: translateX(4px); }

      /* Per-mode borders + hover highlights */
      .acw-mode-btn-chat { border: 2px solid ${brandBorder}; --acw-sweep: ${rgbaFromHex(safePrimary, 0.22)}; }
      .acw-mode-btn-chat::after { background: ${brandWashSoft}; }
      .acw-mode-btn-chat:hover { border-color: ${brandBorderMid}; }
      .acw-mode-btn-chat:hover .acw-mode-chevron { color: ${safePrimary}; }

      .acw-mode-btn-voice { border: 2px solid #e0e7ff; --acw-sweep: rgba(99, 102, 241, 0.24); }
      .acw-mode-btn-voice::after { background: linear-gradient(to right, rgba(99, 102, 241, 0.1), rgba(168, 85, 247, 0.1), rgba(236, 72, 153, 0.1)); }
      .acw-mode-btn-voice:hover { border-color: #818cf8; }
      .acw-mode-btn-voice:hover .acw-mode-chevron { color: #4f46e5; }

      .acw-mode-btn-whatsapp { border: 2px solid #d1fae5; --acw-sweep: rgba(16, 185, 129, 0.24); }
      .acw-mode-btn-whatsapp::after { background: linear-gradient(to right, rgba(16, 185, 129, 0.1), rgba(34, 197, 94, 0.1), rgba(20, 184, 166, 0.1)); }
      .acw-mode-btn-whatsapp:hover { border-color: #34d399; }
      .acw-mode-btn-whatsapp:hover .acw-mode-chevron { color: #059669; }
      .acw-voice-panel {
        flex: 1 1 auto; min-height: 480px; display: none; flex-direction: column; align-items: stretch; justify-content: flex-start;
        padding: 0; position: relative; overflow: hidden;
        background: #0b1220;
        color: #fff;
      }
      .acw-voice-panel.visible { display: flex; }
      .acw-voice-orb { position: absolute; border-radius: 50%; filter: blur(48px); opacity: 0.16; pointer-events: none; }
      .acw-voice-orb.a { width: 280px; height: 280px; background: ${safePrimary}; top: -80px; right: -60px; }
      .acw-voice-orb.b { width: 240px; height: 240px; background: ${primaryLight}; bottom: -40px; left: -50px; }
      @keyframes acw-orb-pulse { 0%, 100% { transform: scale(1); opacity: 0.35; } 50% { transform: scale(1.08); opacity: 0.55; } }
      .acw-voice-inner { position: relative; z-index: 1; flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 28px 20px 24px; gap: 20px; }
      .acw-voice-title { font-size: var(--acw-text-2xl); font-weight: 700; letter-spacing: -0.02em; text-align: center; }
      .acw-voice-status { font-size: var(--acw-text-base); font-weight: 500; line-height: var(--acw-leading); color: rgba(255,255,255,0.92); text-align: center; min-height: 2.8em; padding: 0 12px; }
      .acw-voice-visualizer { display: flex; align-items: flex-end; justify-content: center; gap: 6px; height: 72px; }
      .acw-voice-bar {
        width: 8px; border-radius: 4px;
        background: rgba(255,255,255,0.72);
        height: 24px; transition: height 0.08s ease-out;
      }
      .acw-voice-mic-wrap { display: flex; flex-direction: column; align-items: center; gap: 20px; width: 100%; }
      .acw-voice-mic {
        width: 96px; height: 96px; border-radius: 50%; border: 2px solid rgba(255,255,255,0.35); cursor: pointer;
        display: flex; align-items: center; justify-content: center;
        background: rgba(255,255,255,0.15); color: #fff; transition: transform 0.2s, background 0.2s, box-shadow 0.2s;
      }
      .acw-voice-mic:hover { transform: scale(1.03); background: rgba(255,255,255,0.22); }
      .acw-voice-mic.recording {
        background: #fff; color: ${safePrimary}; border-color: #fff;
        box-shadow: 0 0 0 12px rgba(255,255,255,0.15);
        animation: none;
      }
      @keyframes acw-pulse-mic { 0%, 100% { box-shadow: 0 0 0 12px rgba(255,255,255,0.12); } 50% { box-shadow: 0 0 0 20px rgba(255,255,255,0.2); } }
      .acw-voice-mic svg { width: 36px; height: 36px; }
      .acw-voice-end-row {
        width: 100%; max-width: 280px; height: 52px; border-radius: 14px; border: 1px solid rgba(248, 113, 113, 0.45);
        background: rgba(239, 68, 68, 0.22); backdrop-filter: blur(8px); color: #fff; font-size: 14px; font-weight: 600;
        display: none; align-items: center; justify-content: center; gap: 10px; cursor: pointer;
        transition: background 0.18s ease, transform 0.15s ease;
      }
      .acw-voice-end-row:hover { background: rgba(239, 68, 68, 0.34); transform: translateY(-1px); }
      .acw-voice-end-row svg { width: 18px; height: 18px; flex-shrink: 0; }
      @keyframes acw-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.75; } }
      .acw-nav-offer { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
      .acw-nav-offer-btn {
        border: 1px solid ${safePrimary}; background: #fff; color: ${safePrimary};
        border-radius: 999px; padding: 6px 14px; font-size: 13px; cursor: pointer;
      }
      .acw-nav-offer-btn.primary { background: ${safePrimary}; color: #fff; }
      .acw-nav-offer-btn:disabled { opacity: 0.5; cursor: not-allowed; }

      .acw-rail {
        display: flex; flex-direction: column-reverse; align-items: center; gap: 14px;
        padding-bottom: env(safe-area-inset-bottom, 0px);
      }
      .acw-dock {
        position: relative;
        z-index: 1;
        transition: opacity 0.26s ease, transform 0.32s cubic-bezier(0.34, 1.25, 0.64, 1), visibility 0.26s;
        transform-origin: bottom center;
      }
      :host(.spark-acw-bottom-right) .acw-dock,
      :host(.spark-acw-top-right) .acw-dock { transform-origin: bottom right; }
      :host(.spark-acw-bottom-left) .acw-dock,
      :host(.spark-acw-top-left) .acw-dock { transform-origin: bottom left; }
      .acw-dock.acw-dock-hidden {
        opacity: 0;
        transform: scale(0.78) translateY(10px);
        pointer-events: none;
        visibility: hidden;
      }
      .acw-panel.is-open {
        animation: acw-panel-surface 0.34s cubic-bezier(0.22, 1, 0.36, 1);
      }
      @keyframes acw-panel-surface {
        from { opacity: 0; transform: translateY(14px) scale(0.97); }
        to { opacity: 1; transform: translateY(0) scale(1); }
      }
      /* Panel open: overlap the dock anchor; dock is hidden underneath */
      :host(.spark-acw-panel-visible) .acw-panel.bottom-right,
      :host(.spark-acw-panel-visible) .acw-panel-rail.bottom-right {
        right: 0 !important;
        bottom: 0 !important;
        left: auto !important;
        top: auto !important;
        transform: none !important;
      }
      :host(.spark-acw-panel-visible) .acw-panel.bottom-left,
      :host(.spark-acw-panel-visible) .acw-panel-rail.bottom-left {
        left: 0 !important;
        bottom: 0 !important;
        right: auto !important;
        top: auto !important;
        transform: none !important;
      }
      :host(.spark-acw-panel-visible) .acw-panel.middle-right,
      :host(.spark-acw-panel-visible) .acw-panel-rail.middle-right {
        right: 0 !important;
        top: auto !important;
        bottom: 0 !important;
        transform: none !important;
      }
      :host(.spark-acw-panel-visible) .acw-panel.middle-left,
      :host(.spark-acw-panel-visible) .acw-panel-rail.middle-left {
        left: 0 !important;
        top: auto !important;
        bottom: 0 !important;
        transform: none !important;
      }
      @media (max-width: 480px) {
        :host { --acw-panel-height: min(max(650px, 60vh), calc(100vh - 24px - env(safe-area-inset-bottom, 0px))); }
        .acw-panel { width: calc(100vw - 24px); }
        .acw-panel.is-expanded { width: calc(100vw - 24px); max-width: calc(100vw - 24px); }
        .acw-header-expand { display: none !important; }
        :host(.spark-acw-panel-visible) .acw-panel.is-open {
          box-shadow: 0 25px 50px rgba(15, 23, 42, 0.18), 0 0 0 100vmax rgba(15, 23, 42, 0.14);
        }
      }
      .acw-rail-btn {
        width: 60px; height: 60px; min-width: 60px; min-height: 60px;
        border: 2px solid rgba(255,255,255,0.95); border-radius: 50%; cursor: pointer;
        display: inline-flex; align-items: center; justify-content: center; padding: 0;
        box-shadow: 0 12px 30px rgba(15,23,42,0.20), 0 4px 10px rgba(0,0,0,0.08);
        transition: transform 0.22s cubic-bezier(0.34, 1.25, 0.64, 1), box-shadow 0.22s ease, opacity 0.2s;
      }
      .acw-rail-btn:hover { transform: translateY(-2px) scale(1.06); box-shadow: 0 16px 36px rgba(15,23,42,0.24), 0 6px 14px rgba(0,0,0,0.10); }
      .acw-rail-btn:active { transform: scale(0.96); }
      .acw-rail-btn:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(255,255,255,0.95), 0 0 0 6px ${safePrimary}66, 0 12px 30px rgba(15,23,42,0.20); }
      /* Uniform sizing across channels (primary/secondary kept for hooks, no size difference) */
      .acw-rail-btn-primary, .acw-rail-btn-secondary { width: 60px; height: 60px; min-width: 60px; min-height: 60px; }
      .acw-rail-btn svg { width: 26px; height: 26px; color: #fff; flex-shrink: 0; }
      .acw-rail-btn-chat { background: linear-gradient(135deg, rgba(255,255,255,0.18) 0%, rgba(255,255,255,0) 55%), ${safePrimary}; }
      .acw-rail-btn-voice { background: linear-gradient(135deg, rgba(255,255,255,0.18) 0%, rgba(255,255,255,0) 55%), #7c3aed; }
      .acw-rail-btn-whatsapp { background: linear-gradient(135deg, rgba(255,255,255,0.18) 0%, rgba(255,255,255,0) 55%), #16a34a; }
      .acw-rail-btn.is-active {
        box-shadow: 0 0 0 3px rgba(255,255,255,0.95), 0 0 0 6px ${safePrimary}55, 0 12px 32px rgba(15,23,42,0.2);
      }
      .acw-panel-rail.bottom-right {
        right: calc(var(--acw-rail-width, 60px) + 12px);
        bottom: 0;
      }
      .acw-panel-rail.middle-right {
        right: calc(var(--acw-rail-width, 60px) + 12px);
        top: 50%;
        transform: translateY(-50%);
      }
      .acw-panel-rail.bottom-left {
        left: calc(var(--acw-rail-width, 60px) + 12px);
        bottom: 0;
      }
      .acw-panel-rail.middle-left {
        left: calc(var(--acw-rail-width, 60px) + 12px);
        top: 50%;
        transform: translateY(-50%);
      }
      .acw-panel-rail.top-right {
        right: calc(var(--acw-rail-width, 60px) + 12px);
        top: max(88px, var(--acw-viewport-top-offset, 88px));
      }
      .acw-panel-rail.top-left {
        left: calc(var(--acw-rail-width, 60px) + 12px);
        top: max(88px, var(--acw-viewport-top-offset, 88px));
      }
      .acw-panel-rail {
        max-height: var(--acw-panel-height, min(max(650px, 60vh), calc(100vh - 48px)));
      }
      /* Rail voice: full-viewport layer so transcript (left) + mic (right) anchor to browser edges */
      :host(.spark-acw-rail-voice-active) {
        position: fixed !important;
        inset: 0 !important;
        top: 0 !important;
        right: 0 !important;
        bottom: 0 !important;
        left: 0 !important;
        width: 100% !important;
        height: 100% !important;
        max-width: none !important;
        transform: none !important;
        pointer-events: none;
      }
      /* Rail voice: floating mic + transcript (no chat panel) */
      .acw-rail-voice-overlay {
        display: none;
        pointer-events: none;
        position: absolute;
        inset: 0;
        z-index: 2147483002;
      }
      .acw-rail-voice-overlay.visible {
        display: block;
        pointer-events: none;
      }
      .acw-rail-voice-transcript-wrap {
        display: none;
        position: absolute;
        right: max(24px, env(safe-area-inset-right, 0px));
        left: auto;
        bottom: calc(168px + env(safe-area-inset-bottom, 0px));
        width: min(420px, calc(100vw - 48px));
        max-height: min(440px, calc(100vh - 176px));
        pointer-events: auto;
      }
      .acw-rail-voice-overlay.has-transcript .acw-rail-voice-transcript-wrap {
        display: flex;
        flex-direction: column;
        align-items: flex-end;
      }
      .acw-rail-voice-transcript-toolbar {
        display: none;
        justify-content: flex-end;
        margin-bottom: 8px;
      }
      .acw-rail-voice-overlay.is-ended .acw-rail-voice-transcript-toolbar {
        display: flex;
      }
      .acw-rail-voice-close {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        border: 1px solid rgba(148, 163, 184, 0.4);
        background: rgba(255, 255, 255, 0.96);
        color: #475569;
        border-radius: 999px;
        padding: 6px 12px 6px 10px;
        font-size: 13px;
        font-weight: 600;
        cursor: pointer;
        box-shadow: 0 6px 18px rgba(15, 23, 42, 0.12);
        transition: background 0.2s ease, color 0.2s ease, transform 0.15s ease;
      }
      .acw-rail-voice-close:hover { background: #fff; color: #1e293b; transform: translateY(-1px); }
      .acw-rail-voice-close svg { width: 15px; height: 15px; }
      .acw-rail-voice-transcript {
        width: 100%;
        max-height: min(400px, calc(100vh - 196px));
        overflow-y: auto;
        overflow-x: hidden;
        display: flex;
        flex-direction: column;
        gap: 12px;
        padding: 24px 4px 4px 4px;
        scroll-behavior: smooth;
        scrollbar-width: thin;
        scrollbar-color: rgba(148, 163, 184, 0.45) transparent;
        -webkit-mask-image: linear-gradient(to bottom, transparent 0%, #000 14%, #000 100%);
        mask-image: linear-gradient(to bottom, transparent 0%, #000 14%, #000 100%);
      }
      .acw-rail-voice-transcript::-webkit-scrollbar { width: 5px; }
      .acw-rail-voice-transcript::-webkit-scrollbar-thumb {
        background: rgba(148, 163, 184, 0.45);
        border-radius: 999px;
      }
      .acw-rail-tbubble {
        max-width: 92%;
        animation: acw-rail-tbubble-in 0.38s cubic-bezier(0.22, 1, 0.36, 1) both;
        transform-origin: bottom right;
      }
      @keyframes acw-rail-tbubble-in {
        from { opacity: 0; transform: translateY(18px) scale(0.97); }
        to { opacity: 1; transform: translateY(0) scale(1); }
      }
      .acw-rail-tbubble.is-bot {
        align-self: flex-end;
        background: #fff;
        border: 1px solid rgba(147, 197, 253, 0.9);
        border-radius: 16px;
        padding: 11px 15px;
        color: #111827;
        font-size: 13px;
        line-height: 1.45;
        font-weight: 400;
        box-shadow: 0 10px 28px rgba(59, 130, 246, 0.14);
      }
      .acw-rail-tbubble.is-user {
        align-self: flex-end;
        background: ${brandGradientBtn};
        border-radius: 999px;
        padding: 10px 17px;
        color: #fff;
        font-weight: 700;
        font-size: 13px;
        line-height: 1.35;
        box-shadow: 0 10px 26px ${brandShadow};
      }
      .acw-rail-tbubble.is-interim { opacity: 0.82; font-style: italic; }
      .acw-rail-tbubble.is-faded-top {
        opacity: 0.42;
        transform: perspective(900px) rotateX(10deg) scale(0.96);
        transform-origin: bottom right;
      }
      .acw-rail-nav-offer {
        align-self: flex-end;
        max-width: 92%;
        background: #fff;
        border: 1px solid rgba(147, 197, 253, 0.9);
        border-radius: 16px;
        padding: 12px 14px;
        box-shadow: 0 10px 28px rgba(59, 130, 246, 0.14);
        animation: acw-rail-tbubble-in 0.38s cubic-bezier(0.22, 1, 0.36, 1) both;
        transform-origin: bottom right;
      }
      .acw-rail-nav-offer-text {
        font-size: 13px;
        line-height: 1.45;
        color: #111827;
        margin-bottom: 10px;
      }
      .acw-rail-nav-offer-actions {
        display: flex;
        gap: 8px;
        flex-wrap: wrap;
        justify-content: flex-end;
      }
      .acw-rail-nav-offer-btn {
        border: 1px solid ${safePrimary};
        background: #fff;
        color: ${safePrimary};
        border-radius: 999px;
        padding: 7px 14px;
        font-size: 12.5px;
        font-weight: 600;
        cursor: pointer;
        transition: transform 0.15s ease, opacity 0.2s ease;
      }
      .acw-rail-nav-offer-btn:hover { transform: translateY(-1px); }
      .acw-rail-nav-offer-btn.primary {
        border: none;
        color: #fff;
        background: ${brandGradientBtn};
        box-shadow: 0 6px 18px ${brandShadow};
      }
      .acw-rail-nav-offer-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
      .acw-rail-voice-mic-dock {
        position: absolute;
        right: max(24px, env(safe-area-inset-right, 0px));
        bottom: max(24px, env(safe-area-inset-bottom, 0px));
        width: 80px;
        pointer-events: auto;
        display: flex;
        flex-direction: column;
        align-items: center;
        gap: 10px;
      }
      .acw-rail-voice-hint {
        width: max(140px, 100%);
        text-align: center;
        font-size: 12px;
        font-weight: 600;
        color: #64748b;
        line-height: 1.35;
        pointer-events: none;
        transition: color 0.2s ease;
      }
      .acw-rail-voice-overlay.is-connected .acw-rail-voice-hint { color: ${safePrimary}; }
      .acw-rail-voice-home {
        width: 40px;
        height: 40px;
        border-radius: 50%;
        border: 1px solid rgba(148, 163, 184, 0.35);
        background: rgba(255, 255, 255, 0.96);
        color: #475569;
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        box-shadow: 0 6px 16px rgba(15, 23, 42, 0.14);
        transition: transform 0.15s ease, background 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
      }
      .acw-rail-voice-home:hover {
        transform: translateY(-1px) scale(1.05);
        background: #fff;
        color: #1e293b;
        box-shadow: 0 10px 22px rgba(15, 23, 42, 0.18);
      }
      .acw-rail-voice-home:active { transform: scale(0.96); }
      .acw-rail-voice-home svg { width: 18px; height: 18px; }
      .acw-rail-voice-end {
        width: 44px; height: 44px; border-radius: 50%;
        border: none; background: #ef4444; color: #fff; cursor: pointer;
        display: flex; align-items: center; justify-content: center;
        box-shadow: 0 8px 22px rgba(239, 68, 68, 0.38);
        transition: transform 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
      }
      .acw-rail-voice-end:hover { background: #dc2626; transform: translateY(-1px) scale(1.04); }
      .acw-rail-voice-end:active { transform: scale(0.96); }
      .acw-rail-voice-end svg { width: 20px; height: 20px; }
      .acw-rail-voice-mic-inner {
        position: relative;
        width: 80px;
        height: 80px;
        display: flex;
        align-items: center;
        justify-content: center;
      }
      .acw-rail-voice-halo {
        position: absolute;
        inset: 0;
        pointer-events: none;
        opacity: 0;
        visibility: hidden;
        transition: opacity 0.25s ease, visibility 0.25s ease;
      }
      .acw-rail-voice-halo .ring {
        position: absolute;
        inset: 0;
        border-radius: 50%;
        border: 1px solid rgba(45, 212, 191, 0.35);
        opacity: 0;
        animation: none;
      }
      .acw-rail-voice-overlay.is-connected .acw-rail-voice-halo {
        opacity: 1;
        visibility: visible;
      }
      .acw-rail-voice-overlay.is-connected .acw-rail-voice-halo .ring {
        animation: acw-rail-halo-pulse 2.4s ease-out infinite;
      }
      .acw-rail-voice-halo .ring.r2 {
        animation-delay: 0.55s;
        border-color: rgba(56, 189, 248, 0.28);
      }
      .acw-rail-voice-halo .ring.r3 {
        animation-delay: 1.1s;
        border-color: rgba(99, 102, 241, 0.22);
      }
      @keyframes acw-rail-halo-pulse {
        0% { transform: scale(1); opacity: 0.55; }
        70% { transform: scale(1.85); opacity: 0; }
        100% { transform: scale(1.85); opacity: 0; }
      }
      .acw-rail-voice-overlay.is-connected.is-speaking .acw-rail-voice-halo .ring {
        animation-duration: 1.6s;
      }
      .acw-rail-voice-mic {
        position: relative;
        z-index: 1;
        width: 64px;
        height: 64px;
        border-radius: 50%;
        border: 2px solid #e2e8f0;
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        color: #64748b;
        background: #fff;
        box-shadow: 0 10px 28px rgba(15, 23, 42, 0.14);
        transition: transform 0.2s ease, box-shadow 0.2s ease, background 0.25s ease, color 0.25s ease, border-color 0.25s ease;
      }
      .acw-rail-voice-mic:hover { transform: scale(1.04); box-shadow: 0 12px 32px rgba(15, 23, 42, 0.18); }
      .acw-rail-voice-mic:active { transform: scale(0.97); }
      .acw-rail-voice-mic.connecting {
        opacity: 0.92;
        cursor: wait;
        border-color: #bae6fd;
        color: #0ea5e9;
        box-shadow: 0 0 0 6px rgba(14, 165, 233, 0.12);
      }
      .acw-rail-voice-overlay.is-connected .acw-rail-voice-mic {
        border: none;
        color: #fff;
        background: ${brandGradientSoft};
        box-shadow: 0 12px 32px ${brandShadowStrong};
      }
      .acw-rail-voice-overlay.is-connected .acw-rail-voice-mic:hover {
        box-shadow: 0 0 0 8px ${brandWashMid}, 0 14px 36px ${brandShadowStrong};
      }
      .acw-rail-voice-mic svg { width: 28px; height: 28px; }
      .acw-rail-voice-status {
        position: absolute;
        top: 4px;
        right: 4px;
        z-index: 2;
        width: 22px;
        height: 22px;
        border-radius: 50%;
        border: 2px solid #fff;
        background: #22c55e;
        box-shadow: 0 2px 8px rgba(15, 23, 42, 0.2);
        cursor: help;
        display: none;
        align-items: center;
        justify-content: center;
        transition: background 0.25s ease;
      }
      .acw-rail-voice-overlay.is-connected .acw-rail-voice-status { display: flex; }
      .acw-rail-voice-status svg { width: 12px; height: 12px; color: #fff; }
      .acw-rail-voice-overlay.is-connected .acw-rail-voice-status { background: #22c55e; }
      .acw-rail-voice-overlay.is-ended .acw-rail-voice-status { background: #ef4444; }
      .acw-rail-voice-status-tip {
        position: absolute;
        bottom: calc(100% + 8px);
        right: 0;
        white-space: nowrap;
        background: #0f172a;
        color: #fff;
        font-size: 12px;
        font-weight: 600;
        padding: 6px 10px;
        border-radius: 8px;
        box-shadow: 0 8px 20px rgba(15, 23, 42, 0.28);
        opacity: 0;
        visibility: hidden;
        transform: translateY(4px);
        transition: opacity 0.18s ease, transform 0.18s ease, visibility 0.18s ease;
        pointer-events: none;
      }
      .acw-rail-voice-status-tip::after {
        content: "";
        position: absolute;
        top: 100%;
        right: 12px;
        border: 5px solid transparent;
        border-top-color: #0f172a;
      }
      .acw-rail-voice-status:hover .acw-rail-voice-status-tip {
        opacity: 1;
        visibility: visible;
        transform: translateY(0);
      }
      /* Inline embed: panel fills shadow host (Spark console preview card) */
      :host(.spark-acw-inline) {
        position: relative !important;
        display: block;
        width: 100%;
        height: 100%;
        min-height: 0;
        z-index: 0;
        right: auto !important;
        bottom: auto !important;
        top: auto !important;
        left: auto !important;
        transform: none !important;
      }
      :host(.spark-acw-inline) .acw-panel,
      :host(.spark-acw-inline) .acw-panel.acw-panel-inline {
        position: relative !important;
        display: flex !important;
        flex-direction: column;
        width: 100% !important;
        height: 100% !important;
        max-width: none !important;
        max-height: none !important;
        right: auto !important;
        bottom: auto !important;
        top: auto !important;
        left: auto !important;
        transform: none !important;
        border-radius: 0;
        box-shadow: none;
        border: none;
        overflow: hidden;
      }
      :host(.spark-acw-inline) .acw-header,
      :host(.spark-acw-inline) .acw-status,
      :host(.spark-acw-inline) .acw-input,
      :host(.spark-acw-inline) .acw-footer {
        flex-shrink: 0;
      }
      :host(.spark-acw-inline) .acw-start-cta,
      :host(.spark-acw-inline) .acw-messages,
      :host(.spark-acw-inline) .acw-customer-form,
      :host(.spark-acw-inline) .acw-voice-panel {
        flex: 1 1 auto;
        min-height: 0;
        overflow-y: auto;
      }
      :host(.spark-acw-inline) .acw-start-cta-mode {
        justify-content: flex-start;
      }
      :host(.spark-acw-inline) .acw-mode-heading {
        font-size: var(--acw-text-xl);
      }
      :host(.spark-acw-inline) .acw-mode-hero-icon {
        width: 56px;
        height: 56px;
        margin-bottom: 12px;
      }
      :host(.spark-acw-inline) .acw-mode-btn {
        padding: 12px 14px;
      }
      :host(.spark-acw-inline) .acw-launcher,
      :host(.spark-acw-inline) .acw-launcher-wrap,
      :host(.spark-acw-inline) .acw-rail,
      :host(.spark-acw-inline) .acw-dock,
      :host(.spark-acw-inline) .acw-header-expand {
        display: none !important;
      }
      /* Dark mode — only when explicitly opted in via :host(.spark-acw-theme-dark).
         System prefers-color-scheme is ignored unless themeMode is 'auto' (JS toggles this class). */
      :host(.spark-acw-theme-dark) .acw-panel {
        background: rgba(15, 23, 42, 0.96);
        border-color: rgba(51, 65, 85, 0.65);
        box-shadow: 0 25px 50px rgba(0, 0, 0, 0.45);
      }
      :host(.spark-acw-theme-dark) .acw-header {
        background: linear-gradient(90deg, rgba(30,41,59,0.95) 0%, rgba(15,23,42,0.98) 100%);
        color: #f1f5f9; border-bottom-color: rgba(51, 65, 85, 0.55);
      }
      :host(.spark-acw-theme-dark) .acw-messages {
        background: linear-gradient(165deg, rgba(30,41,59,0.5) 0%, rgba(15,23,42,0.85) 100%);
      }
      :host(.spark-acw-theme-dark) .acw-msg-bot {
        background: rgba(30, 41, 59, 0.95); color: #e2e8f0;
        border-color: rgba(71, 85, 105, 0.65);
      }
      :host(.spark-acw-theme-dark) .acw-msg-typing {
        background: rgba(30, 41, 59, 0.95); border-color: rgba(71, 85, 105, 0.65);
      }
      :host(.spark-acw-theme-dark) .acw-input {
        background: rgba(15, 23, 42, 0.92); border-top-color: rgba(51, 65, 85, 0.55);
      }
      :host(.spark-acw-theme-dark) .acw-composer-box {
        background: rgba(30, 41, 59, 0.92); border-color: rgba(148, 163, 184, 0.45);
      }
      :host(.spark-acw-theme-dark) .acw-composer-box:focus-within {
        border-color: rgba(96, 165, 250, 0.85);
        box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.25);
      }
      :host(.spark-acw-theme-dark) .acw-composer-field {
        color: #f1f5f9;
      }
      :host(.spark-acw-theme-dark) .acw-composer-field::placeholder { color: #64748b; }
      :host(.spark-acw-theme-dark) .acw-input-attach { color: #94a3b8; }
      :host(.spark-acw-theme-dark) .acw-input-attach:hover:not(:disabled) {
        color: #bfdbfe; background: rgba(59, 130, 246, 0.15);
      }
      :host(.spark-acw-theme-dark) .acw-suggested-question {
        background: rgba(30, 41, 59, 0.9); color: #94a3b8;
        border-color: rgba(71, 85, 105, 0.65);
      }
      :host(.spark-acw-theme-dark) .acw-suggested-questions {
        background: #0f172a;
      }
      :host(.spark-acw-theme-dark) .acw-footer { color: #64748b; border-top-color: rgba(51, 65, 85, 0.55); background: #0f172a; }
      :host(.spark-acw-theme-dark) .acw-close { color: #94a3b8; }
      :host(.spark-acw-theme-dark) .acw-close:hover { color: #e2e8f0; background: rgba(255,255,255,0.06); }
      :host(.spark-acw-theme-dark) .acw-continuity-toast {
        background: rgba(30, 41, 59, 0.96); color: #bfdbfe;
        border-color: rgba(59, 130, 246, 0.45);
      }
      :host(.spark-acw-theme-dark) .acw-msg-action-btn {
        background: rgba(30, 41, 59, 0.9); color: #94a3b8; border-color: rgba(71, 85, 105, 0.65);
      }
      :host(.spark-acw-theme-dark) .acw-msg-action-btn.is-icon,
      :host(.spark-acw-theme-dark) .acw-msg-action-btn.is-icon:hover,
      :host(.spark-acw-theme-dark) .acw-msg-action-btn.is-icon.is-active {
        background: transparent; border: none;
      }
      :host(.spark-acw-theme-dark) .acw-handover-ack-line {
        background: rgba(71, 85, 105, 0.55);
      }
      :host(.spark-acw-theme-dark) .acw-handover-ack-icon {
        background: rgba(30, 41, 59, 0.92); border-color: rgba(71, 85, 105, 0.45); color: #94a3b8;
      }
      @media (prefers-reduced-motion: reduce) {
        .acw-msg, .acw-suggested-question, .acw-launcher, .acw-input-send,
        .acw-connection-banner-btn, .acw-rail-tbubble, .acw-online-dot,
        .acw-continuity-toast, .acw-rail-voice-halo .ring, .acw-panel {
          animation: none !important; transition: none !important;
        }
        .acw-rail-voice-overlay.is-connected .acw-rail-voice-halo .ring { animation: none !important; }
      }
    `
  }

  function normalizeSuggestedQuestionsList(raw) {
    var out = []
    if (Array.isArray(raw)) {
      for (var i = 0; i < raw.length; i++) {
        var q = String(raw[i] || '').trim()
        if (q) out.push(q)
        if (out.length >= 3) break
      }
    }
    return out
  }

  function normalizeSuggestedActionKind(raw) {
    var k = String(raw || 'question').trim().toLowerCase().replace(/-/g, '_')
    if (k === 'openurl') k = 'open_url'
    if (k === 'question' || k === 'navigation' || k === 'open_url' || k === 'tool') return k
    return 'question'
  }

  function questionsListToSuggestedActions(questions) {
    var out = []
    var list = normalizeSuggestedQuestionsList(questions)
    for (var i = 0; i < list.length; i++) {
      out.push({
        id: 'starter_' + i,
        kind: 'question',
        label: list[i],
        message: list[i],
      })
    }
    return out
  }

  /** Next.js Pages Router exposes window.next.router — use for client-side route changes. */
  function getNextRouter() {
    try {
      if (typeof window !== 'undefined' && window.next && window.next.router && typeof window.next.router.push === 'function') {
        return window.next.router
      }
    } catch (_) {}
    return null
  }

  function acwRoutePath(parsedUrl, anchor) {
    return parsedUrl.pathname + parsedUrl.search + (anchor ? '#' + anchor : '')
  }

  function acwFinishNavPromise(ret, onSuccess) {
    if (ret && typeof ret.then === 'function') {
      return ret.then(onSuccess).catch(function () {
        return false
      })
    }
    return new Promise(function (resolve) {
      setTimeout(function () {
        resolve(onSuccess())
      }, 0)
    })
  }

  /**
   * Resolve a framework-native client router when available.
   * Order (auto): custom callback → host bridge → Next.js → Nuxt → Gatsby.
   * Most Vue/React SPAs do not expose globals — hosts should use spaNavigate or __ACW_NAVIGATE__.
   */
  function createSpaNavigationBridge(config) {
    var disabled = config.spaUseFrameworkRouter === false || config.spaUseNextRouter === false
    var adapterPref = String(config.spaNavigationAdapter || 'auto').toLowerCase()
    var shallow = config.spaNextRouterShallow !== false
    var customNavigate = typeof config.spaNavigate === 'function' ? config.spaNavigate : null
    var bridge = null

    function detectBridge() {
      if (disabled) return null
      if (adapterPref === 'legacy' || adapterPref === 'off' || adapterPref === 'false') return null

      function pick(id, pushFn) {
        return { id: id, push: pushFn }
      }

      if (customNavigate && (adapterPref === 'auto' || adapterPref === 'custom')) {
        return pick('custom', function (path, opts) {
          return customNavigate(path, opts)
        })
      }

      var tryOrder = adapterPref === 'auto'
        ? ['host', 'next', 'nuxt', 'gatsby']
        : [adapterPref]

      for (var i = 0; i < tryOrder.length; i++) {
        var kind = tryOrder[i]
        if (kind === 'host' && typeof window.__ACW_NAVIGATE__ === 'function') {
          return pick('host', function (path, opts) {
            return window.__ACW_NAVIGATE__(path, opts)
          })
        }
        if (kind === 'next') {
          var nextRouter = getNextRouter()
          if (nextRouter) {
            return pick('next', function (path, opts) {
              return nextRouter.push(path, undefined, {
                shallow: shallow,
                scroll: opts && opts.scroll === false ? false : true,
              })
            })
          }
        }
        if (kind === 'nuxt') {
          try {
            if (window.$nuxt && window.$nuxt.$router && typeof window.$nuxt.$router.push === 'function') {
              return pick('nuxt2', function (path) {
                return window.$nuxt.$router.push(path)
              })
            }
            if (window.$nuxt && typeof window.$nuxt.navigateTo === 'function') {
              return pick('nuxt3', function (path) {
                return window.$nuxt.navigateTo(path)
              })
            }
          } catch (_) {}
        }
        if (kind === 'gatsby' && typeof window.___navigate === 'function') {
          return pick('gatsby', function (path) {
            return window.___navigate(path)
          })
        }
      }
      return null
    }

    bridge = detectBridge()

    return {
      get id() {
        return bridge ? bridge.id : null
      },
      refresh: function () {
        bridge = detectBridge()
      },
      canNavigate: function (parsedUrl) {
        if (!bridge) return false
        try {
          return parsedUrl.origin === window.location.origin
        } catch (_) {
          return false
        }
      },
      push: function (parsedUrl, anchor, opts) {
        if (!bridge || !this.canNavigate(parsedUrl)) return Promise.resolve(false)
        var routePath = acwRoutePath(parsedUrl, anchor)
        try {
          var ret = bridge.push(routePath, opts || {})
          return acwFinishNavPromise(ret, function () {
            if (opts && opts.scroll === false) {
              window.scrollTo(0, 0)
              if (anchor) {
                var el = document.getElementById(anchor)
                if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' })
              }
            }
            return true
          }).catch(function (err) {
            console.warn('[AgentChatWidget] Framework navigation failed (' + bridge.id + ')', err)
            return false
          })
        } catch (err) {
          console.warn('[AgentChatWidget] Framework navigation failed (' + bridge.id + ')', err)
          return Promise.resolve(false)
        }
      },
    }
  }

  function createWidget(config) {
    const {
      agentId,
      embedKey,
      primaryColor,
      backgroundColor,
      headerColor,
      allowedDomains = [],
      apiBaseUrl,
      voiceServiceUrl,
      wsUrl,
      title = 'Chat with us',
      greeting = null,
      captureCustomerInfo = false,
      allowSkipCustomerInfo = true,
      bannerImageUrl = null,
      position = 'bottom-right',
      iconUrl = null,
      headerIconUrl = null,
      showFavicon = true,
      showIcon = true,
      showLauncherText = true,
      launcherText,
      enableVoice = true,
      isLocalWebrtc = true,
      useDailyVoice = false,
      dailyApiKey = '',
      dailyAgentName = defaultDailyAgentName(),
      dailyScriptUrl = DEFAULT_DAILY_SCRIPT_URL,
      voiceTenant = 'default',
      liveTranscript = false,
      voiceSessionData = {},
      whatsappPhone = '',
      whatsappMessage = 'Hi! I need assistance.',
      enableWhatsApp = true,
      language = 'en',
      enableGuidedNavigation = true,
      sessionPersistTtlMs = 1800000,
      enableSpaNavigation = false,
      spaContentSelector = 'main',
      spaSyncSelectors = [],
      spaInterceptLinks = null,
      spaUseFrameworkRouter = true,
      spaUseNextRouter = true,
      spaNavigationAdapter = 'auto',
      spaNavigate = null,
      spaNextRouterShallow = true,
      embedMode = 'floating',
      mountElement = null,
      mountSelector = null,
      viewType = 'hub',
      primaryChannel = 'chat',
      railOrder = null,
      showRailTooltips = true,
      suggestedQuestions = [],
      themeMode = 'light',
      viewportTopOffset = null,
    } = config

    const uiLang = normalizeWidgetUiLanguage(language)
    function t(key) {
      return acwT(uiLang, key)
    }

    const trimmedVoiceUrl = voiceServiceUrl != null ? String(voiceServiceUrl).trim() : ''
    const voiceHttpBase = trimmedVoiceUrl
      ? trimmedVoiceUrl.replace(/\/$/, '')
      : VERSION.includes('-staging')
        ? 'https://talk2ai-staging-gcp.helllo.ai'
        : VERSION.includes('-prod')
          ? 'https://talk2ai-prod-gcp.helllo.ai'
          : ''

    const whatsappDigits = normalizeWhatsAppDigits(whatsappPhone)
    const hasWhatsAppOption = enableWhatsApp !== false && whatsappDigits.length > 0

    function resolveMountTarget() {
      if (mountElement && mountElement.nodeType === 1) return mountElement
      if (mountSelector && typeof mountSelector === 'string') {
        try {
          var found = document.querySelector(mountSelector)
          if (found && found.nodeType === 1) return found
        } catch (_) {}
      }
      return null
    }

    var resolvedMountElement = resolveMountTarget()

    const useInline =
      embedMode === 'inline' &&
      resolvedMountElement &&
      resolvedMountElement.nodeType === 1

    const normalizedViewType = viewType === 'rail' ? 'rail' : 'hub'
    const normalizedPrimaryChannel = primaryChannel === 'voice' ? 'voice' : 'chat'
    const availableChannels = ['chat']
    if (enableVoice) availableChannels.push('voice')
    if (hasWhatsAppOption) availableChannels.push('whatsapp')
    const useRail = !useInline && normalizedViewType === 'rail' && availableChannels.length >= 2
    const defaultRailOrder = ['chat', 'voice', 'whatsapp']
    let orderedRailChannels = defaultRailOrder.filter(function (c) {
      return availableChannels.indexOf(c) >= 0
    })
    if (Array.isArray(railOrder) && railOrder.length) {
      orderedRailChannels = railOrder.filter(function (c) {
        return availableChannels.indexOf(c) >= 0
      })
      availableChannels.forEach(function (c) {
        if (orderedRailChannels.indexOf(c) < 0) orderedRailChannels.push(c)
      })
    }
    var railHeightPx = 0
    var railWidthPx = 60
    if (useRail) {
      var RAIL_BTN_SIZE = 60
      var RAIL_BTN_GAP = 14
      orderedRailChannels.forEach(function (ch, idx) {
        if (RAIL_BTN_SIZE > railWidthPx) railWidthPx = RAIL_BTN_SIZE
        railHeightPx += RAIL_BTN_SIZE
        if (idx > 0) railHeightPx += RAIL_BTN_GAP
      })
      railHeightPx += 16
    }
    const effectiveLiveTranscript = liveTranscript || useRail

    // Resolve favicon for hero/header/launcher fallback (when showFavicon is true).
    const resolvedFaviconUrl = (() => {
      if (showFavicon === false) return null
      try {
        const link = document.querySelector('link[rel="icon"]') || document.querySelector('link[rel="shortcut icon"]')
        if (link && link.href) return link.href
      } catch (_) {}
      try {
        const origin = window.location.origin || ''
        if (origin) return origin + '/favicon.ico'
      } catch (_) {}
      return null
    })()

    // Resolve widget icon (only used when showIcon is true): explicit iconUrl, else favicon.
    const resolvedIconUrl = (() => {
      if (!showIcon) return null
      if (iconUrl && typeof iconUrl === 'string' && iconUrl.trim()) return iconUrl.trim()
      return resolvedFaviconUrl
    })()
    const resolvedHeaderIconUrl =
      typeof headerIconUrl === 'string' && headerIconUrl.trim()
        ? headerIconUrl.trim()
        : null
    const headerIconConfigured = headerIconUrl !== undefined
    const launcherLabel = (typeof launcherText === 'string' && launcherText.trim())
      ? launcherText.trim()
      : t('launcher_default')

    const resolvedBannerImageUrl =
      typeof bannerImageUrl === 'string' && bannerImageUrl.trim()
        ? bannerImageUrl.trim()
        : PRECALL_HERO_IMAGE_URL

    var conversationLanguageCode = null
    var supportedLanguagesList = []
    var widgetFileUploadPolicy = null
    var stagedAttachments = []
    var languageSelect = null
    var languageFieldWrap = null
    var langSwitchRoot = null
    var langSwitchTrigger = null
    var langSwitchMenu = null
    var langSwitchCurrent = null
    var langMenuDocClose = null

    function resolveLanguagesApiBase() {
      var b = voiceHttpBase || apiBaseUrl
      if (b) return String(b).replace(/\/$/, '')
      try {
        var sc = document.currentScript || document.querySelector('script[data-agent-id]')
        if (sc && sc.src) return new URL(sc.src).origin
      } catch (_) {}
      try {
        return window.location.origin
      } catch (_) {
        return ''
      }
    }

    function fetchWidgetConfig(done) {
      var base = resolveLanguagesApiBase()
      if (!base || !embedKey || !agentId) {
        if (done) done(null)
        return
      }
      var url =
        base +
        '/api/v1/agent-voice/' +
        encodeURIComponent(String(agentId)) +
        '/widget-config?embed_key=' +
        encodeURIComponent(embedKey)
      fetch(url)
        .then(function (r) {
          return r.ok ? r.json() : Promise.reject(new Error('widget-config'))
        })
        .then(function (data) {
          var langs = data && data.languages && Array.isArray(data.languages) ? data.languages : []
          supportedLanguagesList = langs
          mergeFileUploadPolicy(data && data.file_upload)
          if (done) done(data)
        })
        .catch(function () {
          if (done) done(null)
        })
    }

    function mergeFileUploadPolicy(policy) {
      if (!policy || typeof policy !== 'object') return
      var maxFiles = parseInt(policy.max_files, 10)
      if (isNaN(maxFiles)) maxFiles = 3
      maxFiles = Math.max(0, Math.min(10, maxFiles))
      var enabled = policy.enabled !== false && maxFiles > 0
      var maxSize = parseInt(policy.max_file_size_bytes, 10)
      if (isNaN(maxSize) || maxSize <= 0) maxSize = 1048576
      var allowed = Array.isArray(policy.allowed_mime_types)
        ? policy.allowed_mime_types.filter(function (m) { return typeof m === 'string' && m.trim() })
        : []
      widgetFileUploadPolicy = {
        enabled: enabled,
        max_files: enabled ? maxFiles : 0,
        max_file_size_bytes: maxSize,
        allowed_mime_types: allowed.length ? allowed : ['image/jpeg', 'image/png'],
      }
      applyFileUploadUi()
    }

    // Valid positions
    const validPositions = ['bottom-right', 'middle-right', 'top-right', 'bottom-left', 'middle-left', 'top-left']
    const finalPosition = validPositions.includes(position) ? position : 'bottom-right'
    const isVertical = finalPosition === 'middle-right' || finalPosition === 'middle-left'
    const isLeft = finalPosition.includes('left')

    if (!agentId) {
      console.warn('[AgentChatWidget] Missing agentId. Provide it via init({ agentId: "..." }) or data-agent-id attribute')
      return
    }
    if (!embedKey) {
      console.warn('[AgentChatWidget] Missing embedKey. Provide it via init({ embedKey: "..." }) or data-embed-key attribute')
      return
    }

    const hostOk = domainAllowed(window.location.hostname, allowedDomains)
    if (!hostOk) {
      console.warn('[AgentChatWidget] Host not in allowed domains, widget will not load')
      return
    }

    if (embedMode === 'inline' && !useInline) {
      console.warn('[AgentChatWidget] inline embed requires a valid mountElement (HTMLElement)')
      return null
    }

    const container = document.createElement('div')
    container.className = useInline ? 'spark-acw-container spark-acw-inline' : `spark-acw-container spark-acw-${finalPosition}`
    container.setAttribute('data-spark-widget', VERSION)
    container.style.fontFamily = "'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif"

    function applyThemeFromPreference() {
      if (themeMode === 'dark') {
        container.classList.add('spark-acw-theme-dark')
        container.classList.remove('spark-acw-theme-light')
        return
      }
      if (themeMode === 'auto' && typeof window.matchMedia === 'function' &&
          window.matchMedia('(prefers-color-scheme: dark)').matches) {
        container.classList.add('spark-acw-theme-dark')
        container.classList.remove('spark-acw-theme-light')
        return
      }
      // Default / light — always pin light so OS dark mode cannot restyle the widget
      container.classList.add('spark-acw-theme-light')
      container.classList.remove('spark-acw-theme-dark')
    }
    applyThemeFromPreference()
    if (themeMode === 'auto' && typeof window.matchMedia === 'function') {
      var themeMq = window.matchMedia('(prefers-color-scheme: dark)')
      var onThemeMq = function () { applyThemeFromPreference() }
      if (themeMq.addEventListener) themeMq.addEventListener('change', onThemeMq)
      else if (themeMq.addListener) themeMq.addListener(onThemeMq)
    }

    if (useInline) {
      container.style.position = 'relative'
      container.style.width = '100%'
      container.style.height = '100%'
      container.style.zIndex = '0'
      resolvedMountElement.appendChild(container)
    } else {
      container.style.position = 'fixed'
      elevateWidget(container)
      // Set container position based on finalPosition
      if (finalPosition === 'bottom-right') {
        container.style.right = '24px'
        container.style.bottom = '24px'
      } else if (finalPosition === 'middle-right') {
        container.style.right = '16px'
        container.style.top = '50%'
        container.style.transform = 'translateY(-50%)'
      } else if (finalPosition === 'top-right') {
        container.style.right = '16px'
        container.style.top = '16px'
      } else if (finalPosition === 'bottom-left') {
        container.style.left = '16px'
        container.style.bottom = '16px'
      } else if (finalPosition === 'middle-left') {
        container.style.left = '16px'
        container.style.top = '50%'
        container.style.transform = 'translateY(-50%)'
      } else if (finalPosition === 'top-left') {
        container.style.left = '16px'
        container.style.top = '16px'
      }
      document.body.appendChild(container)
    }

    const shadow = container.attachShadow({ mode: 'open' })
    const style = document.createElement('style')
    style.textContent = createStyles(primaryColor, backgroundColor, headerColor)
    shadow.appendChild(style)

    const panel = document.createElement('div')
    panel.setAttribute('data-acw-view', 'home')
    panel.className = useInline
      ? 'acw-panel acw-panel-inline' + (useRail ? ' acw-panel-rail' : '')
      : `acw-panel ${finalPosition}` + (useRail ? ' acw-panel-rail' : '')
    if (useRail) {
      container.style.setProperty('--acw-rail-height', railHeightPx + 'px')
      container.style.setProperty('--acw-rail-width', railWidthPx + 'px')
    }

    const showLauncherLabel = showLauncherText !== false
    const useFabLauncher = !useRail && !isVertical && (!resolvedIconUrl || !showLauncherLabel)
    const launcherWrap = useFabLauncher ? document.createElement('span') : null
    if (launcherWrap) launcherWrap.className = 'acw-launcher-wrap'
    const launcher = document.createElement('button')
    launcher.className = isVertical ? 'acw-launcher vertical' : 'acw-launcher'
    if (!isVertical && !resolvedIconUrl) {
      launcher.classList.add('acw-launcher-fab')
      launcher.innerHTML = ACW_SPARKLE_SVG + ACW_LAUNCHER_X_SVG
      launcher.setAttribute('aria-label', launcherLabel)
    } else if (!isVertical && resolvedIconUrl && !showLauncherLabel) {
      launcher.classList.add('acw-launcher-fab')
      const launcherImg = document.createElement('img')
      launcherImg.src = resolvedIconUrl
      launcherImg.alt = launcherLabel
      launcher.appendChild(launcherImg)
      launcher.setAttribute('aria-label', launcherLabel)
    } else if (resolvedIconUrl) {
      const launcherImg = document.createElement('img')
      launcherImg.src = resolvedIconUrl
      launcherImg.alt = launcherLabel
      launcher.appendChild(launcherImg)
      if (showLauncherLabel) {
        const launcherTextSpan = document.createElement('span')
        launcherTextSpan.textContent = launcherLabel
        launcher.appendChild(launcherTextSpan)
      } else {
        launcher.setAttribute('aria-label', launcherLabel)
      }
    } else if (showLauncherLabel) {
      const span = document.createElement('span')
      span.textContent = launcherLabel
      launcher.appendChild(span)
    } else {
      launcher.setAttribute('aria-label', launcherLabel)
    }

    var dockEl = null
    if (!useRail) {
      if (launcherWrap) {
        launcherWrap.classList.add('acw-dock')
        dockEl = launcherWrap
      } else {
        launcher.classList.add('acw-dock')
        dockEl = launcher
      }
    }

    const header = document.createElement('div')
    header.className = 'acw-header'
    const headerLeft = document.createElement('div')
    headerLeft.className = 'acw-header-left'
    var headerOnlineDots = []
    if (headerIconConfigured) {
      if (resolvedHeaderIconUrl) {
        const headerIconWrap = document.createElement('div')
        headerIconWrap.className = 'acw-header-icon-wrap'
        const headerIcon = document.createElement('img')
        headerIcon.className = 'acw-header-icon'
        headerIcon.src = resolvedHeaderIconUrl
        headerIcon.alt = ''
        const headerOnline = document.createElement('span')
        headerOnline.className = 'acw-online-dot'
        headerOnline.setAttribute('aria-hidden', 'true')
        headerOnlineDots.push(headerOnline)
        headerIconWrap.appendChild(headerIcon)
        headerIconWrap.appendChild(headerOnline)
        headerLeft.appendChild(headerIconWrap)
      }
    } else if (showFavicon !== false && resolvedFaviconUrl) {
      const headerIconWrap = document.createElement('div')
      headerIconWrap.className = 'acw-header-icon-wrap'
      const headerIcon = document.createElement('img')
      headerIcon.className = 'acw-header-icon'
      headerIcon.src = DEFAULT_SUPPORT_AGENT_IMAGE_URL
      headerIcon.alt = ''
      const headerOnline = document.createElement('span')
      headerOnline.className = 'acw-online-dot'
      headerOnline.setAttribute('aria-hidden', 'true')
      headerOnlineDots.push(headerOnline)
      headerIconWrap.appendChild(headerIcon)
      headerIconWrap.appendChild(headerOnline)
      headerLeft.appendChild(headerIconWrap)
    } else if (showFavicon !== false) {
      const headerIconWrap = document.createElement('div')
      headerIconWrap.className = 'acw-header-icon-wrap'
      const headerAvatar = document.createElement('div')
      headerAvatar.className = 'acw-header-icon acw-header-avatar-fallback'
      headerAvatar.style.display = 'flex'
      headerAvatar.style.alignItems = 'center'
      headerAvatar.style.justifyContent = 'center'
      headerAvatar.innerHTML = ACW_SPARKLE_SVG.replace('class="acw-launcher-sparkle"', 'style="width:22px;height:22px;color:#fff"')
      const headerOnline = document.createElement('span')
      headerOnline.className = 'acw-online-dot'
      headerOnline.setAttribute('aria-hidden', 'true')
      headerOnlineDots.push(headerOnline)
      headerIconWrap.appendChild(headerAvatar)
      headerIconWrap.appendChild(headerOnline)
      headerLeft.appendChild(headerIconWrap)
    }
    const titleWrap = document.createElement('div')
    titleWrap.className = 'acw-header-title-wrap'
    const titleBlock = document.createElement('div')
    titleBlock.className = 'acw-header-title-block'
    const titleEl = document.createElement('div')
    titleEl.className = 'acw-title'
    titleEl.textContent = title
    const subtitleEl = document.createElement('div')
    subtitleEl.className = 'acw-header-subtitle is-connected'
    subtitleEl.textContent = t('status_connected')
    const titleRow = document.createElement('div')
    titleRow.style.display = 'flex'
    titleRow.style.alignItems = 'center'
    titleRow.style.gap = '8px'
    const unreadBadge = document.createElement('span')
    unreadBadge.className = 'acw-unread-badge hidden'
    unreadBadge.textContent = '0'
    titleRow.appendChild(titleEl)
    titleRow.appendChild(unreadBadge)
    titleBlock.appendChild(titleRow)
    titleBlock.appendChild(subtitleEl)
    titleWrap.appendChild(titleBlock)
    headerLeft.appendChild(titleWrap)
    const headerActions = document.createElement('div')
    headerActions.className = 'acw-header-actions'

    const disconnectChatBtn = document.createElement('button')
    disconnectChatBtn.type = 'button'
    disconnectChatBtn.className = 'acw-close acw-header-disconnect'
    disconnectChatBtn.setAttribute('aria-label', t('end_chat'))
    disconnectChatBtn.title = t('end_chat')
    disconnectChatBtn.setAttribute('data-testid', 'acw-chat-disconnect')
    disconnectChatBtn.style.display = 'none'
    disconnectChatBtn.innerHTML = ACW_ICON_END_CHAT

    const expandBtn = document.createElement('button')
    expandBtn.type = 'button'
    expandBtn.className = 'acw-close acw-header-expand'
    expandBtn.setAttribute('aria-label', t('expand_window'))
    expandBtn.title = t('expand_window')
    expandBtn.setAttribute('aria-pressed', 'false')
    expandBtn.setAttribute('data-testid', 'acw-chat-expand')
    expandBtn.innerHTML = ACW_ICON_EXPAND_WINDOW
    if (useInline) expandBtn.style.display = 'none'

    const closeBtn = document.createElement('button')
    closeBtn.type = 'button'
    closeBtn.className = 'acw-close acw-header-close'
    closeBtn.setAttribute('aria-label', t('close_widget'))
    closeBtn.title = t('close_widget')
    closeBtn.setAttribute('data-testid', 'acw-widget-close')
    closeBtn.innerHTML = ACW_ICON_CLOSE_WIDGET

    headerActions.appendChild(disconnectChatBtn)
    headerActions.appendChild(expandBtn)
    headerActions.appendChild(closeBtn)
    header.appendChild(headerLeft)
    header.appendChild(headerActions)

    const statusEl = document.createElement('div')
    statusEl.className = 'acw-status'
    const statusDot = document.createElement('div')
    statusDot.className = 'acw-dot connected'
    const statusText = document.createElement('span')
    statusText.textContent = t('status_connected')
    statusEl.appendChild(statusDot)
    statusEl.appendChild(statusText)

    const connectionBannerEl = document.createElement('div')
    connectionBannerEl.className = 'acw-connection-banner'
    connectionBannerEl.setAttribute('role', 'alert')
    connectionBannerEl.setAttribute('aria-live', 'polite')
    connectionBannerEl.style.display = 'none'
    const connectionBannerTextEl = document.createElement('span')
    connectionBannerTextEl.className = 'acw-connection-banner-text'
    const connectionReconnectBtn = document.createElement('button')
    connectionReconnectBtn.type = 'button'
    connectionReconnectBtn.className = 'acw-connection-banner-btn'
    connectionReconnectBtn.textContent = t('reconnect')
    connectionBannerEl.appendChild(connectionBannerTextEl)
    connectionBannerEl.appendChild(connectionReconnectBtn)

    var connectionState = 'idle'

    const startCtaEl = document.createElement('div')
    startCtaEl.className = 'acw-start-cta'
    let pendingMode = null // 'chat' | 'voice' | 'whatsapp'
    var railButtons = {}
    var activeRailChannel = null

    function setRailActive(channel) {
      if (!useRail) return
      activeRailChannel = channel
      Object.keys(railButtons).forEach(function (key) {
        var btn = railButtons[key]
        if (!btn) return
        if (key === channel) btn.classList.add('is-active')
        else btn.classList.remove('is-active')
      })
    }

    const chatModeSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>'
    const micModeSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>'
    const waModeSvg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/></svg>'
    const modeChevronSvg = '<svg class="acw-mode-chevron" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 18l6-6-6-6"/></svg>'

    function buildModeButton(iconClass, title, desc, iconSvg, btnClass) {
      const btn = document.createElement('button')
      btn.className = 'acw-mode-btn' + (btnClass ? (' ' + btnClass) : '')
      btn.type = 'button'
      btn.innerHTML =
        '<span class="acw-mode-icon ' + iconClass + '">' + iconSvg + '</span>' +
        '<span class="acw-mode-text"><span class="acw-mode-title">' + title + '</span><span class="acw-mode-desc">' + desc + '</span></span>' +
        modeChevronSvg
      return btn
    }

    startCtaEl.classList.add('acw-start-cta-mode')
    const modeSelector = document.createElement('div')
    modeSelector.className = 'acw-mode-selector'
    const modeHero = document.createElement('div')
    modeHero.className = 'acw-mode-hero-icon'
    if (showFavicon !== false && resolvedFaviconUrl) {
      const heroImg = document.createElement('img')
      heroImg.src = resolvedFaviconUrl
      heroImg.alt = ''
      modeHero.appendChild(heroImg)
    } else if (showFavicon !== false) {
      modeHero.innerHTML = ACW_SPARKLE_SVG.replace('class="acw-launcher-sparkle"', 'style="width:36px;height:36px;color:#fff"')
    } else {
      modeHero.style.display = 'none'
    }
    const modeHeading = document.createElement('h2')
    modeHeading.className = 'acw-mode-heading'
    modeHeading.textContent = t('how_can_we_help')
    const modeSub = document.createElement('p')
    modeSub.className = 'acw-mode-sub'
    modeSub.textContent = t('choose_communication_style')
    modeSelector.appendChild(modeHero)
    modeSelector.appendChild(modeHeading)
    modeSelector.appendChild(modeSub)
    const modeOptions = document.createElement('div')
    modeOptions.className = 'acw-mode-options'

    const chatBtn = buildModeButton('acw-mode-icon-chat', t('text_chat_title'), t('text_chat_desc'), chatModeSvg, 'acw-mode-btn-chat')
    chatBtn.setAttribute('data-testid', 'acw-mode-chat')
    modeOptions.appendChild(chatBtn)
    chatBtn.onclick = function () {
      pendingMode = 'chat'
      conversationStarted = true
      if (captureCustomerInfo && customerForm && !customerInfoSubmitted) {
        formShownBeforeConnect = true
        showFormOnly()
        const nameInput = customerForm.querySelector('#acw-customer-name')
        if (nameInput) setTimeout(function () { nameInput.focus() }, 100)
      } else if (connected || connecting) {
        showChat()
      } else {
        connect()
      }
    }

    if (enableVoice) {
      const voiceBtn = buildModeButton('acw-mode-icon-voice', t('voice_chat_title'), t('voice_chat_desc'), micModeSvg, 'acw-mode-btn-voice')
      voiceBtn.setAttribute('data-testid', 'acw-mode-voice')
      modeOptions.appendChild(voiceBtn)
      voiceBtn.onclick = function () {
        pendingMode = 'voice'
        conversationStarted = true
        startCtaEl.style.display = 'none'
        if (captureCustomerInfo && customerForm && !customerInfoSubmitted) {
          formShownBeforeConnect = true
          showFormOnly()
          const nameInput = customerForm.querySelector('#acw-customer-name')
          if (nameInput) setTimeout(function () { nameInput.focus() }, 100)
        } else {
          showVoicePanel()
        }
      }
    }

    if (hasWhatsAppOption) {
      const waBtn = buildModeButton('acw-mode-icon-whatsapp', t('whatsapp_title'), t('whatsapp_desc'), waModeSvg, 'acw-mode-btn-whatsapp')
      waBtn.setAttribute('data-testid', 'acw-mode-whatsapp')
      modeOptions.appendChild(waBtn)
      waBtn.onclick = function () {
        const msg = typeof whatsappMessage === 'string' ? whatsappMessage : 'Hi! I need assistance.'
        const url = 'https://wa.me/' + whatsappDigits + '?text=' + encodeURIComponent(msg)
        try {
          window.open(url, '_blank', 'noopener,noreferrer')
        } catch (_) {}
        pendingMode = null
        conversationStarted = false
        showStartCta()
      }
    }

    modeSelector.appendChild(modeOptions)
    startCtaEl.appendChild(modeSelector)

    const homeFooterEl = document.createElement('div')
    homeFooterEl.className = 'acw-footer acw-home-footer'
    const homeFooterLink = document.createElement('a')
    homeFooterLink.href = 'https://helllo.ai'
    homeFooterLink.target = '_blank'
    homeFooterLink.rel = 'noopener noreferrer'
    homeFooterLink.textContent = 'helllo.ai'
    homeFooterEl.appendChild(document.createTextNode(t('powered_by')))
    homeFooterEl.appendChild(homeFooterLink)
    startCtaEl.appendChild(homeFooterEl)

    const messages = document.createElement('div')
    messages.className = 'acw-messages'
    const messagesSpacer = document.createElement('div')
    messagesSpacer.className = 'acw-messages-spacer'
    messagesSpacer.setAttribute('aria-hidden', 'true')
    messages.appendChild(messagesSpacer)

    const inputWrap = document.createElement('div')
    inputWrap.className = 'acw-input'
    const composerBox = document.createElement('div')
    composerBox.className = 'acw-composer-box'
    const attachmentsStripEl = document.createElement('div')
    attachmentsStripEl.className = 'acw-attachments'
    attachmentsStripEl.setAttribute('aria-live', 'polite')
    const composerActions = document.createElement('div')
    composerActions.className = 'acw-composer-actions'
    const fileInput = document.createElement('input')
    fileInput.type = 'file'
    fileInput.accept = 'image/*'
    fileInput.style.display = 'none'
    fileInput.setAttribute('aria-hidden', 'true')
    const attachBtn = document.createElement('button')
    attachBtn.type = 'button'
    attachBtn.className = 'acw-input-attach'
    attachBtn.setAttribute('aria-label', t('attach_image'))
    attachBtn.innerHTML = ACW_ICON_PAPERCLIP
    const input = document.createElement('textarea')
    input.className = 'acw-composer-field'
    input.rows = 1
    input.placeholder = t('placeholder_type_message')
    input.setAttribute('aria-label', t('message_aria'))
    const sendBtn = document.createElement('button')
    sendBtn.type = 'button'
    sendBtn.className = 'acw-input-send'
    sendBtn.setAttribute('aria-label', t('send_message'))
    sendBtn.innerHTML = ACW_ICON_SEND
    composerActions.appendChild(attachBtn)
    composerActions.appendChild(sendBtn)
    composerBox.appendChild(attachmentsStripEl)
    composerBox.appendChild(input)
    composerBox.appendChild(composerActions)
    inputWrap.appendChild(fileInput)
    inputWrap.appendChild(composerBox)

    const footerEl = document.createElement('div')
    footerEl.className = 'acw-footer'
    footerEl.style.display = 'none'
    const footerLink = document.createElement('a')
    footerLink.href = 'https://helllo.ai'
    footerLink.target = '_blank'
    footerLink.rel = 'noopener noreferrer'
    footerLink.textContent = 'helllo.ai'
    footerEl.appendChild(document.createTextNode(t('powered_by')))
    footerEl.appendChild(footerLink)

    // Customer info form (if enabled)
    let customerForm = null
    let sessionId = null
    let customerInfoSubmitted = false
    let customerFormGatePassed = false
    let conversationStarted = false
    let formShownBeforeConnect = false
    let pendingCustomerName = null
    let pendingCustomerPhone = null
    let unreadCount = 0
    var messageLog = []
    var persistSessionTimer = null
    var restoringSession = false
    /** PDP hosts from the in-flight carousel payload (unioned with allowedDomains during normalize). */
    var productCarouselExtraDomains = null
    var pendingNavigationOffer = null
    var navigationCommitted = false
    var lastExternalNavUrl = null
    var lastExternalNavAt = 0
    /** After cross-domain new-tab navigation the visitor stays on this page; auto-reconnect if talk2ai closes WS. */
    var externalNavKeepAliveUntil = 0
    var externalNavAutoReconnectPending = false
    var spaNavBridge = createSpaNavigationBridge({
      spaUseFrameworkRouter: spaUseFrameworkRouter,
      spaUseNextRouter: spaUseNextRouter,
      spaNavigationAdapter: spaNavigationAdapter,
      spaNextRouterShallow: spaNextRouterShallow,
      spaNavigate: spaNavigate,
    })

    function isCrossDomainNavigationUrl(url) {
      if (!url) return false
      try {
        var parsed = new URL(url, window.location.href)
        return !isSameNavigationDomain(parsed.hostname, window.location.hostname)
      } catch (_) {
        return false
      }
    }

    function scheduleKeepAliveOnCurrentPageAfterExternalNav() {
      externalNavKeepAliveUntil = Date.now() + 45000
      externalNavAutoReconnectPending = true
    }

    function clearExternalNavKeepAlive() {
      externalNavKeepAliveUntil = 0
      externalNavAutoReconnectPending = false
    }

    function shouldAutoReconnectAfterExternalNavClose() {
      return (
        externalNavAutoReconnectPending &&
        externalNavKeepAliveUntil > Date.now() &&
        !!sessionId &&
        conversationStarted
      )
    }

    function tryFrameworkNavigation(parsedUrl, anchor, label, opts) {
      spaNavBridge.refresh()
      prepareNavigationHandshake(parsedUrl, anchor, opts)
      return spaNavBridge.push(parsedUrl, anchor, opts).then(function (ok) {
        if (!ok) return false
        var scrolledOnly = opts && opts.scrolledOnly
        var msg = scrolledOnly ? t('navigation_scrolled') : t('navigation_navigated')
        appendMessage(label ? msg + ' (' + label + ')' : msg, 'bot')
        return true
      })
    }

    function canUseFrameworkNavigation(parsedUrl) {
      spaNavBridge.refresh()
      return spaNavBridge.canNavigate(parsedUrl)
    }
    var activeSuggestedActions = questionsListToSuggestedActions(suggestedQuestions)
    var initSuggestedActions = activeSuggestedActions.slice()

    if (captureCustomerInfo) {
      customerForm = document.createElement('div')
      customerForm.className = 'acw-customer-form'
      customerForm.style.display = 'none'
      
      const formHero = document.createElement('div')
      formHero.id = 'acw-form-hero'
      formHero.className = 'acw-form-hero-banner'
      const formHeroImg = document.createElement('img')
      formHeroImg.src = resolvedBannerImageUrl
      formHeroImg.alt = ''
      formHeroImg.setAttribute('aria-hidden', 'true')
      const formHeroMedia = document.createElement('div')
      formHeroMedia.className = 'acw-form-hero-banner-media'
      formHeroMedia.appendChild(formHeroImg)
      formHero.appendChild(formHeroMedia)
      const formHeroClose = document.createElement('button')
      formHeroClose.type = 'button'
      formHeroClose.className = 'acw-form-hero-close'
      formHeroClose.setAttribute('aria-label', t('close_widget'))
      formHeroClose.title = t('close_widget')
      formHeroClose.innerHTML = ACW_ICON_CLOSE_WIDGET
      formHeroClose.onclick = function () {
        softClosePanel()
      }
      formHero.appendChild(formHeroClose)

      const formHeroName = document.createElement('div')
      formHeroName.className = 'acw-form-hero-name'
      formHeroName.textContent = title
      formHero.appendChild(formHeroName)

      const formIdentity = document.createElement('div')
      formIdentity.className = 'acw-form-identity'
      const backToOptions = document.createElement('button')
      backToOptions.type = 'button'
      backToOptions.className = 'acw-form-back'
      backToOptions.setAttribute('aria-label', t('back_to_options'))
      backToOptions.title = t('back_to_options')
      backToOptions.innerHTML =
        '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>'
      formIdentity.appendChild(backToOptions)
      const formAvatarWrap = document.createElement('div')
      formAvatarWrap.className = 'acw-form-avatar-wrap'
      const formAvatar = document.createElement('div')
      formAvatar.className = 'acw-form-avatar'
      formAvatar.textContent = (typeof title === 'string' && title.trim() ? title.trim().charAt(0) : '?').toUpperCase()
      formAvatar.setAttribute('aria-hidden', 'true')
      const formAvatarDot = document.createElement('span')
      formAvatarDot.className = 'acw-online-dot'
      formAvatarDot.setAttribute('aria-hidden', 'true')
      headerOnlineDots.push(formAvatarDot)
      formAvatarWrap.appendChild(formAvatar)
      formAvatarWrap.appendChild(formAvatarDot)
      formIdentity.appendChild(formAvatarWrap)

      const formHeadCol = document.createElement('div')
      formHeadCol.className = 'acw-form-head-col'
      const formTitle = document.createElement('h3')
      formTitle.textContent = t('form_title_start')
      formHeadCol.appendChild(formTitle)
      
      const nameField = document.createElement('div')
      nameField.className = 'field'
      const nameLabel = document.createElement('label')
      nameLabel.textContent = t('label_name')
      nameLabel.setAttribute('for', 'acw-customer-name')
      const nameInput = document.createElement('input')
      nameInput.id = 'acw-customer-name'
      nameInput.type = 'text'
      nameInput.placeholder = t('placeholder_name')
      nameField.appendChild(nameLabel)
      nameField.appendChild(nameInput)
      const nameError = document.createElement('p')
      nameError.className = 'acw-field-error'
      nameError.setAttribute('role', 'alert')
      nameError.style.display = 'none'
      nameField.appendChild(nameError)
      
      const phoneField = document.createElement('div')
      phoneField.className = 'field acw-field-phone'
      const phoneLabel = document.createElement('label')
      phoneLabel.textContent = t('label_phone')
      phoneLabel.setAttribute('for', 'acw-customer-phone')
      const phoneRow = document.createElement('div')
      phoneRow.className = 'acw-phone-row'
      const phoneCodeWrap = document.createElement('div')
      phoneCodeWrap.className = 'acw-phone-code-wrap'
      const phoneCodeRoot = document.createElement('div')
      phoneCodeRoot.className = 'acw-phone-code-switch'
      const phoneCodeTrigger = document.createElement('button')
      phoneCodeTrigger.type = 'button'
      phoneCodeTrigger.className = 'acw-phone-code-trigger'
      phoneCodeTrigger.id = 'acw-customer-phone-code-trigger'
      phoneCodeTrigger.setAttribute('aria-expanded', 'false')
      phoneCodeTrigger.setAttribute('aria-haspopup', 'listbox')
      phoneCodeTrigger.setAttribute('aria-label', t('label_country_code'))
      const phoneCodeFlag = document.createElement('span')
      phoneCodeFlag.className = 'acw-phone-code-flag'
      phoneCodeFlag.setAttribute('aria-hidden', 'true')
      const phoneCodeDial = document.createElement('span')
      phoneCodeDial.className = 'acw-phone-code-dial'
      const phoneCodeChevron = document.createElement('span')
      phoneCodeChevron.className = 'acw-phone-code-chevron'
      phoneCodeChevron.setAttribute('aria-hidden', 'true')
      phoneCodeChevron.innerHTML =
        '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>'
      phoneCodeTrigger.appendChild(phoneCodeFlag)
      phoneCodeTrigger.appendChild(phoneCodeDial)
      phoneCodeTrigger.appendChild(phoneCodeChevron)
      const phoneCodeMenu = document.createElement('div')
      phoneCodeMenu.className = 'acw-phone-code-menu'
      phoneCodeMenu.id = 'acw-customer-phone-code-menu'
      phoneCodeTrigger.setAttribute('aria-controls', 'acw-customer-phone-code-menu')
      const phoneCodeSearchWrap = document.createElement('div')
      phoneCodeSearchWrap.className = 'acw-phone-code-search-wrap'
      const phoneCodeSearch = document.createElement('input')
      phoneCodeSearch.type = 'search'
      phoneCodeSearch.className = 'acw-phone-code-search'
      phoneCodeSearch.id = 'acw-customer-phone-code-search'
      phoneCodeSearch.placeholder = t('search_country')
      phoneCodeSearch.setAttribute('aria-label', t('search_country'))
      phoneCodeSearch.setAttribute('autocomplete', 'off')
      phoneCodeSearch.setAttribute('autocorrect', 'off')
      phoneCodeSearch.setAttribute('spellcheck', 'false')
      phoneCodeSearchWrap.appendChild(phoneCodeSearch)
      const phoneCodeList = document.createElement('div')
      phoneCodeList.className = 'acw-phone-code-list'
      phoneCodeList.id = 'acw-customer-phone-code-list'
      phoneCodeList.setAttribute('role', 'listbox')
      phoneCodeList.setAttribute('aria-label', t('label_country_code'))
      const phoneCodeEmpty = document.createElement('p')
      phoneCodeEmpty.className = 'acw-phone-code-empty'
      phoneCodeEmpty.textContent = t('no_country_match')
      phoneCodeEmpty.setAttribute('role', 'status')
      const phoneCodeSelect = document.createElement('select')
      phoneCodeSelect.id = 'acw-customer-phone-code'
      phoneCodeSelect.className = 'acw-phone-code-native'
      phoneCodeSelect.setAttribute('aria-hidden', 'true')
      phoneCodeSelect.setAttribute('tabindex', '-1')
      var defaultPhoneIso = acwDefaultPhoneCountryIso()
      var phoneMenuDocClose = null

      function acwFilterPhoneCountries(query) {
        if (!phoneCodeList) return
        var q = String(query || '').trim().toLowerCase().replace(/^\+/, '')
        var opts = phoneCodeList.querySelectorAll('.acw-phone-code-option')
        var visible = 0
        for (var fi = 0; fi < opts.length; fi++) {
          var btn = opts[fi]
          var name = (btn.getAttribute('data-name') || '').toLowerCase()
          var dial = btn.getAttribute('data-dial') || ''
          var iso = (btn.getAttribute('data-iso') || '').toLowerCase()
          var match = !q || name.indexOf(q) !== -1 || dial.indexOf(q) !== -1 || iso.indexOf(q) !== -1
          btn.style.display = match ? '' : 'none'
          if (match) visible++
        }
        if (phoneCodeEmpty) phoneCodeEmpty.classList.toggle('is-visible', q.length > 0 && visible === 0)
        acwSetActivePhoneOption(0)
      }

      function acwResetPhoneCountrySearch() {
        if (!phoneCodeSearch) return
        phoneCodeSearch.value = ''
        acwFilterPhoneCountries('')
      }

      phoneCodeList.appendChild(phoneCodeEmpty)

      for (var pci = 0; pci < PHONE_COUNTRY_CODES.length; pci++) {
        var phoneCountry = PHONE_COUNTRY_CODES[pci]
        var phoneCodeOpt = document.createElement('option')
        phoneCodeOpt.value = phoneCountry.iso
        phoneCodeOpt.textContent = phoneCountry.name + ' (+' + phoneCountry.dial + ')'
        if (phoneCountry.iso === defaultPhoneIso) phoneCodeOpt.selected = true
        phoneCodeSelect.appendChild(phoneCodeOpt)
        var phoneCodeBtn = document.createElement('button')
        phoneCodeBtn.type = 'button'
        phoneCodeBtn.className = 'acw-phone-code-option'
        phoneCodeBtn.setAttribute('role', 'option')
        phoneCodeBtn.setAttribute('data-iso', phoneCountry.iso)
        phoneCodeBtn.setAttribute('data-name', phoneCountry.name)
        phoneCodeBtn.setAttribute('data-dial', phoneCountry.dial)
        var phoneOptFlag = document.createElement('span')
        phoneOptFlag.className = 'acw-phone-code-option-flag'
        phoneOptFlag.setAttribute('aria-hidden', 'true')
        phoneOptFlag.textContent = acwCountryFlag(phoneCountry.iso)
        var phoneOptName = document.createElement('span')
        phoneOptName.className = 'acw-phone-code-option-name'
        phoneOptName.textContent = phoneCountry.name
        var phoneOptDial = document.createElement('span')
        phoneOptDial.className = 'acw-phone-code-option-dial'
        phoneOptDial.textContent = '+' + phoneCountry.dial
        phoneCodeBtn.appendChild(phoneOptFlag)
        phoneCodeBtn.appendChild(phoneOptName)
        phoneCodeBtn.appendChild(phoneOptDial)
        phoneCodeBtn.addEventListener('click', function (ev) {
          ev.stopPropagation()
          var iso = this.getAttribute('data-iso')
          if (!iso || !phoneCodeSelect) return
          phoneCodeSelect.value = iso
          acwSyncPhoneCodeFromSelect()
          acwSetPhoneCodeMenuOpen(false)
          clearPhoneFieldError()
        })
        phoneCodeList.appendChild(phoneCodeBtn)
      }

      var phoneCodeActiveIndex = 0

      function acwVisiblePhoneOptions() {
        var opts = phoneCodeList.querySelectorAll('.acw-phone-code-option')
        var visible = []
        for (var vi = 0; vi < opts.length; vi++) {
          if (opts[vi].style.display !== 'none') visible.push(opts[vi])
        }
        return visible
      }

      function acwSetActivePhoneOption(index) {
        var visible = acwVisiblePhoneOptions()
        var all = phoneCodeList.querySelectorAll('.acw-phone-code-option')
        for (var ai = 0; ai < all.length; ai++) all[ai].classList.remove('is-active')
        if (!visible.length) {
          phoneCodeActiveIndex = 0
          return
        }
        if (index < 0) index = visible.length - 1
        if (index >= visible.length) index = 0
        phoneCodeActiveIndex = index
        var active = visible[index]
        active.classList.add('is-active')
        try { active.scrollIntoView({ block: 'nearest' }) } catch (_) {}
      }

      function acwChoosePhoneOption(btn) {
        if (!btn || !phoneCodeSelect) return
        var iso = btn.getAttribute('data-iso')
        if (!iso) return
        phoneCodeSelect.value = iso
        acwSyncPhoneCodeFromSelect()
        acwSetPhoneCodeMenuOpen(false)
        clearPhoneFieldError()
      }

      function acwExactPhoneOption(query) {
        var raw = String(query || '').trim().toLowerCase()
        var q = raw.replace(/^\+/, '')
        if (!q) return null
        // "in" is India's ISO code and also the start of "India". Only a full
        // dial code or a full country name should commit while the list is open.
        var dialQuery = /^\+?\d+$/.test(raw)
        var visible = acwVisiblePhoneOptions()
        var exact = []
        for (var ei = 0; ei < visible.length; ei++) {
          var btn = visible[ei]
          var name = (btn.getAttribute('data-name') || '').toLowerCase()
          var dial = btn.getAttribute('data-dial') || ''
          if ((dialQuery && dial === q) || name === q) exact.push(btn)
        }
        return exact.length === 1 ? exact[0] : null
      }

      phoneCodeSearch.addEventListener('input', function () {
        acwFilterPhoneCountries(phoneCodeSearch.value)
        var exact = acwExactPhoneOption(phoneCodeSearch.value)
        if (exact) acwChoosePhoneOption(exact)
      })
      phoneCodeSearch.addEventListener('click', function (ev) {
        ev.stopPropagation()
      })
      phoneCodeSearch.addEventListener('keydown', function (ev) {
        if (ev.key === 'Escape') {
          ev.stopPropagation()
          acwSetPhoneCodeMenuOpen(false)
          return
        }
        if (ev.key === 'ArrowDown' || ev.key === 'ArrowUp') {
          ev.preventDefault()
          ev.stopPropagation()
          if (!acwVisiblePhoneOptions().length) return
          acwSetActivePhoneOption(phoneCodeActiveIndex + (ev.key === 'ArrowDown' ? 1 : -1))
          return
        }
        if (ev.key === 'Enter') {
          ev.preventDefault()
          ev.stopPropagation()
          var visible = acwVisiblePhoneOptions()
          var active = visible[phoneCodeActiveIndex] || visible[0]
          acwChoosePhoneOption(acwExactPhoneOption(phoneCodeSearch.value) || active)
        }
      })
      phoneCodeTrigger.addEventListener('keydown', function (ev) {
        if (ev.key === 'ArrowDown' || ev.key === 'Enter' || ev.key === ' ') {
          ev.preventDefault()
          acwSetPhoneCodeMenuOpen(true)
          return
        }
        if (ev.key.length !== 1 || ev.metaKey || ev.ctrlKey || ev.altKey) return
        ev.preventDefault()
        var typed = ev.key
        acwSetPhoneCodeMenuOpen(true)
        setTimeout(function () {
          if (!phoneCodeSearch) return
          phoneCodeSearch.value = typed
          phoneCodeSearch.dispatchEvent(new Event('input'))
          phoneCodeSearch.focus()
        }, 0)
      })

      function acwSyncPhoneCodeFromSelect() {
        if (!phoneCodeSelect) return
        var country = acwFindPhoneCountry(phoneCodeSelect.value)
        if (phoneCodeFlag) phoneCodeFlag.textContent = country ? acwCountryFlag(country.iso) : ''
        if (phoneCodeDial) phoneCodeDial.textContent = country ? '+' + country.dial : ''
        if (phoneCodeTrigger && country) {
          var al = t('label_country_code') + ': ' + country.name + ' +' + country.dial
          phoneCodeTrigger.setAttribute('aria-label', al)
          phoneCodeTrigger.setAttribute('title', al)
        }
        if (phoneCodeList) {
          var opts = phoneCodeList.querySelectorAll('.acw-phone-code-option')
          for (var oi = 0; oi < opts.length; oi++) {
            var optBtn = opts[oi]
            var sel = optBtn.getAttribute('data-iso') === phoneCodeSelect.value
            optBtn.setAttribute('aria-selected', sel ? 'true' : 'false')
            optBtn.classList.toggle('is-selected', sel)
          }
        }
      }

      function acwPositionPhoneCodeMenuInPanel() {
        if (!phoneCodeMenu || !phoneCodeTrigger || !panel) return
        var triggerRect = phoneCodeTrigger.getBoundingClientRect()
        var panelRect = panel.getBoundingClientRect()
        var menuW = Math.min(280, panelRect.width - 24)
        var left = triggerRect.left - panelRect.left
        var maxLeft = Math.max(0, panelRect.width - menuW)
        left = Math.max(12, Math.min(left, maxLeft))
        phoneCodeMenu.style.position = 'absolute'
        phoneCodeMenu.style.left = Math.round(left) + 'px'
        phoneCodeMenu.style.top = Math.round(triggerRect.bottom - panelRect.top + 6) + 'px'
        phoneCodeMenu.style.right = 'auto'
        phoneCodeMenu.style.width = menuW + 'px'
        phoneCodeMenu.style.minWidth = '240px'
        phoneCodeMenu.style.maxWidth = menuW + 'px'
        phoneCodeMenu.style.zIndex = '50'
      }

      function acwRestorePhoneCodeMenuInRoot() {
        if (!phoneCodeMenu || !phoneCodeRoot) return
        phoneCodeMenu.classList.remove('acw-phone-code-menu--floating')
        phoneCodeMenu.style.position = ''
        phoneCodeMenu.style.left = ''
        phoneCodeMenu.style.top = ''
        phoneCodeMenu.style.right = ''
        phoneCodeMenu.style.width = ''
        phoneCodeMenu.style.minWidth = ''
        phoneCodeMenu.style.maxWidth = ''
        phoneCodeMenu.style.zIndex = ''
        if (phoneCodeMenu.parentNode !== phoneCodeRoot) {
          phoneCodeRoot.appendChild(phoneCodeMenu)
        }
      }

      function acwSetPhoneCodeMenuOpen(open) {
        if (!phoneCodeMenu || !phoneCodeTrigger) return
        if (open) {
          if (customerForm._acwCloseLangMenu) customerForm._acwCloseLangMenu()
          acwResetPhoneCountrySearch()
          phoneCodeMenu.classList.add('is-open', 'acw-phone-code-menu--floating')
          phoneCodeTrigger.setAttribute('aria-expanded', 'true')
          if (panel && phoneCodeMenu.parentNode !== panel) {
            panel.appendChild(phoneCodeMenu)
          }
          acwPositionPhoneCodeMenuInPanel()
          setTimeout(function () {
            if (phoneCodeSearch) {
              phoneCodeSearch.focus()
              try { phoneCodeSearch.select() } catch (_) {}
            }
          }, 0)
          if (phoneMenuDocClose) {
            document.removeEventListener('click', phoneMenuDocClose, true)
            phoneMenuDocClose = null
          }
          phoneMenuDocClose = function (ev) {
            var path = typeof ev.composedPath === 'function' ? ev.composedPath() : []
            if (path.indexOf(phoneCodeRoot) === -1 && path.indexOf(phoneCodeMenu) === -1) {
              acwSetPhoneCodeMenuOpen(false)
            }
          }
          setTimeout(function () {
            document.addEventListener('click', phoneMenuDocClose, true)
          }, 0)
        } else {
          acwResetPhoneCountrySearch()
          phoneCodeMenu.classList.remove('is-open')
          acwRestorePhoneCodeMenuInRoot()
          phoneCodeTrigger.setAttribute('aria-expanded', 'false')
          if (phoneMenuDocClose) {
            document.removeEventListener('click', phoneMenuDocClose, true)
            phoneMenuDocClose = null
          }
        }
      }

      phoneCodeTrigger.addEventListener('click', function (ev) {
        ev.stopPropagation()
        acwSetPhoneCodeMenuOpen(!phoneCodeMenu.classList.contains('is-open'))
      })

      phoneCodeMenu.appendChild(phoneCodeSearchWrap)
      phoneCodeMenu.appendChild(phoneCodeList)
      phoneCodeRoot.appendChild(phoneCodeTrigger)
      phoneCodeRoot.appendChild(phoneCodeMenu)
      phoneCodeRoot.appendChild(phoneCodeSelect)
      phoneCodeWrap.appendChild(phoneCodeRoot)
      acwSyncPhoneCodeFromSelect()
      customerForm._acwSyncPhoneCode = acwSyncPhoneCodeFromSelect
      customerForm._acwClosePhoneCodeMenu = function () {
        acwSetPhoneCodeMenuOpen(false)
      }
      const phoneNumberWrap = document.createElement('div')
      phoneNumberWrap.className = 'acw-phone-number-wrap'
      const phoneInput = document.createElement('input')
      phoneInput.id = 'acw-customer-phone'
      phoneInput.type = 'tel'
      phoneInput.inputMode = 'tel'
      phoneInput.autocomplete = 'tel-national'
      phoneInput.placeholder = t('placeholder_phone')
      phoneNumberWrap.appendChild(phoneInput)
      phoneRow.appendChild(phoneCodeWrap)
      phoneRow.appendChild(phoneNumberWrap)
      phoneField.appendChild(phoneLabel)
      phoneField.appendChild(phoneRow)
      const phoneError = document.createElement('p')
      phoneError.className = 'acw-field-error'
      phoneError.setAttribute('role', 'alert')
      phoneError.style.display = 'none'
      phoneField.appendChild(phoneError)

      languageFieldWrap = document.createElement('div')
      languageFieldWrap.className = 'acw-lang-switch-field'
      languageFieldWrap.id = 'acw-customer-language-field'
      languageFieldWrap.style.display = 'none'
      const langLabel = document.createElement('label')
      langLabel.id = 'acw-customer-lang-label'
      langLabel.className = 'acw-lang-switch-sr-label'
      langLabel.textContent = t('conversation_language')
      langLabel.setAttribute('for', 'acw-customer-language')

      langSwitchRoot = document.createElement('div')
      langSwitchRoot.className = 'acw-lang-switch'

      langSwitchTrigger = document.createElement('button')
      langSwitchTrigger.type = 'button'
      langSwitchTrigger.className = 'acw-lang-switch-trigger'
      langSwitchTrigger.setAttribute('aria-expanded', 'false')
      langSwitchTrigger.setAttribute('aria-haspopup', 'listbox')
      langSwitchTrigger.setAttribute('aria-labelledby', 'acw-customer-lang-label')
      langSwitchTrigger.setAttribute('aria-label', t('conversation_language'))

      const langIconWrap = document.createElement('span')
      langIconWrap.className = 'acw-lang-switch-icon'
      langIconWrap.setAttribute('aria-hidden', 'true')
      langIconWrap.innerHTML =
        '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>'
      langSwitchCurrent = document.createElement('span')
      langSwitchCurrent.className = 'acw-lang-switch-current'
      const langChevronWrap = document.createElement('span')
      langChevronWrap.className = 'acw-lang-switch-chevron'
      langChevronWrap.setAttribute('aria-hidden', 'true')
      langChevronWrap.innerHTML =
        '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 9l6 6 6-6"/></svg>'
      langSwitchTrigger.appendChild(langIconWrap)
      langSwitchTrigger.appendChild(langSwitchCurrent)
      langSwitchTrigger.appendChild(langChevronWrap)

      langSwitchMenu = document.createElement('div')
      langSwitchMenu.className = 'acw-lang-switch-menu'
      langSwitchMenu.id = 'acw-customer-lang-menu'
      langSwitchMenu.setAttribute('role', 'listbox')
      langSwitchTrigger.setAttribute('aria-controls', 'acw-customer-lang-menu')

      languageSelect = document.createElement('select')
      languageSelect.id = 'acw-customer-language'
      languageSelect.className = 'acw-lang-switch-native'
      languageSelect.setAttribute('aria-hidden', 'true')
      languageSelect.setAttribute('tabindex', '-1')

      function acwSyncLangSwitcherFromSelect() {
        if (!languageSelect || !langSwitchCurrent) return
        var opt = languageSelect.options[languageSelect.selectedIndex]
        langSwitchCurrent.textContent = opt ? opt.textContent : ''
        if (langSwitchTrigger) {
          var al = opt && opt.textContent
            ? t('conversation_language') + ': ' + opt.textContent
            : t('conversation_language')
          langSwitchTrigger.setAttribute('aria-label', al)
          langSwitchTrigger.setAttribute('title', al)
        }
        if (langSwitchMenu) {
          var btns = langSwitchMenu.querySelectorAll('.acw-lang-option')
          for (var bi = 0; bi < btns.length; bi++) {
            var b = btns[bi]
            var sel = b.getAttribute('data-code') === languageSelect.value
            b.setAttribute('aria-selected', sel ? 'true' : 'false')
            b.classList.toggle('is-selected', sel)
          }
        }
      }

      function acwPositionLangMenuInPanel() {
        if (!langSwitchMenu || !langSwitchTrigger || !panel) return
        var triggerRect = langSwitchTrigger.getBoundingClientRect()
        var panelRect = panel.getBoundingClientRect()
        var left = triggerRect.left - panelRect.left
        var maxLeft = Math.max(0, panelRect.width - 240)
        left = Math.max(12, Math.min(left, maxLeft))
        langSwitchMenu.style.position = 'absolute'
        langSwitchMenu.style.left = Math.round(left) + 'px'
        langSwitchMenu.style.top = Math.round(triggerRect.bottom - panelRect.top + 8) + 'px'
        langSwitchMenu.style.right = 'auto'
        langSwitchMenu.style.minWidth = '220px'
        langSwitchMenu.style.maxWidth = Math.min(280, panelRect.width - 24) + 'px'
        langSwitchMenu.style.zIndex = '50'
      }

      function acwRestoreLangMenuInRoot() {
        if (!langSwitchMenu || !langSwitchRoot) return
        langSwitchMenu.classList.remove('acw-lang-switch-menu--floating')
        langSwitchMenu.style.position = ''
        langSwitchMenu.style.left = ''
        langSwitchMenu.style.top = ''
        langSwitchMenu.style.right = ''
        langSwitchMenu.style.minWidth = ''
        langSwitchMenu.style.maxWidth = ''
        langSwitchMenu.style.zIndex = ''
        if (langSwitchMenu.parentNode !== langSwitchRoot) {
          langSwitchRoot.appendChild(langSwitchMenu)
        }
      }

      function acwSetLangMenuOpen(open) {
        if (!langSwitchMenu || !langSwitchTrigger) return
        if (open) {
          if (customerForm._acwClosePhoneCodeMenu) customerForm._acwClosePhoneCodeMenu()
          langSwitchMenu.classList.add('is-open', 'acw-lang-switch-menu--floating')
          langSwitchTrigger.setAttribute('aria-expanded', 'true')
          if (panel && langSwitchMenu.parentNode !== panel) {
            panel.appendChild(langSwitchMenu)
          }
          acwPositionLangMenuInPanel()
          if (langMenuDocClose) {
            document.removeEventListener('click', langMenuDocClose, true)
            langMenuDocClose = null
          }
          langMenuDocClose = function (ev) {
            var path = typeof ev.composedPath === 'function' ? ev.composedPath() : []
            if (path.indexOf(langSwitchRoot) === -1 && path.indexOf(langSwitchMenu) === -1) {
              acwSetLangMenuOpen(false)
            }
          }
          setTimeout(function () {
            document.addEventListener('click', langMenuDocClose, true)
          }, 0)
        } else {
          langSwitchMenu.classList.remove('is-open')
          acwRestoreLangMenuInRoot()
          langSwitchTrigger.setAttribute('aria-expanded', 'false')
          if (langMenuDocClose) {
            document.removeEventListener('click', langMenuDocClose, true)
            langMenuDocClose = null
          }
        }
      }

      langSwitchTrigger.addEventListener('click', function (ev) {
        ev.stopPropagation()
        acwSetLangMenuOpen(!langSwitchMenu.classList.contains('is-open'))
      })

      langSwitchRoot.appendChild(langSwitchTrigger)
      langSwitchRoot.appendChild(langSwitchMenu)
      langSwitchRoot.appendChild(languageSelect)

      languageFieldWrap.appendChild(langLabel)
      languageFieldWrap.appendChild(langSwitchRoot)
      formHero.appendChild(languageFieldWrap)

      languageSelect.addEventListener('change', function () {
        conversationLanguageCode = languageSelect.value || null
        acwSyncLangSwitcherFromSelect()
      })

      customerForm._acwSyncLangSwitcher = acwSyncLangSwitcherFromSelect
      customerForm._acwCloseLangMenu = function () {
        acwSetLangMenuOpen(false)
      }

      customerForm.addEventListener('keydown', function (e) {
        if (e.key === 'Escape') {
          if (langSwitchMenu && langSwitchMenu.classList.contains('is-open')) {
            acwSetLangMenuOpen(false)
          }
          if (phoneCodeMenu && phoneCodeMenu.classList.contains('is-open')) {
            acwSetPhoneCodeMenuOpen(false)
          }
        }
      })
      
      const formActions = document.createElement('div')
      formActions.className = 'actions'
      const formError = document.createElement('p')
      formError.className = 'acw-form-error'
      formError.setAttribute('role', 'alert')
      formError.style.display = 'none'
      const actionButtons = document.createElement('div')
      actionButtons.className = 'actions-buttons' + (allowSkipCustomerInfo ? '' : ' single')
      const submitBtn = document.createElement('button')
      submitBtn.className = 'submit-btn'
      submitBtn.type = 'button'
      submitBtn.textContent = t('start_conversation')
      const skipBtn = document.createElement('button')
      skipBtn.className = 'skip-btn'
      skipBtn.type = 'button'
      skipBtn.textContent = t('skip')
      actionButtons.appendChild(submitBtn)
      if (allowSkipCustomerInfo) {
        actionButtons.appendChild(skipBtn)
      }
      formActions.appendChild(formError)
      formActions.appendChild(actionButtons)

      const formFooter = document.createElement('div')
      formFooter.className = 'acw-footer acw-form-footer'
      const formFooterLink = document.createElement('a')
      formFooterLink.href = 'https://helllo.ai'
      formFooterLink.target = '_blank'
      formFooterLink.rel = 'noopener noreferrer'
      formFooterLink.textContent = 'helllo.ai'
      formFooter.appendChild(document.createTextNode(t('powered_by')))
      formFooter.appendChild(formFooterLink)

      customerForm.appendChild(formHero)
      customerForm.appendChild(formIdentity)
      customerForm.appendChild(formHeadCol)
      customerForm.appendChild(nameField)
      customerForm.appendChild(phoneField)
      customerForm.appendChild(formActions)
      customerForm.appendChild(formFooter)
      
      function clearCustomerFormErrors() {
        nameError.textContent = ''
        nameError.style.display = 'none'
        phoneError.textContent = ''
        phoneError.style.display = 'none'
        formError.textContent = ''
        formError.style.display = 'none'
        nameField.classList.remove('acw-field-invalid')
        phoneField.classList.remove('acw-field-invalid')
      }

      function showCustomerFieldError(fieldEl, errorEl, message) {
        errorEl.textContent = message
        errorEl.style.display = 'block'
        fieldEl.classList.add('acw-field-invalid')
      }

      function showCustomerFormError(message) {
        formError.textContent = message
        formError.style.display = 'block'
      }

      nameInput.addEventListener('input', function () {
        if (nameInput.value.trim()) {
          nameError.style.display = 'none'
          nameField.classList.remove('acw-field-invalid')
        }
      })
      function clearPhoneFieldError() {
        phoneError.style.display = 'none'
        phoneField.classList.remove('acw-field-invalid')
      }
      phoneInput.addEventListener('input', function () {
        if (phoneInput.value.trim()) clearPhoneFieldError()
      })
      phoneCodeSelect.addEventListener('change', clearPhoneFieldError)
      customerForm._acwClearErrors = clearCustomerFormErrors
      
      function submitCustomerInfo() {
        const name = nameInput.value.trim()
        const phoneNational = phoneInput.value.trim()
        const phoneCountry = acwFindPhoneCountry(phoneCodeSelect.value)
        if (languageSelect && languageSelect.value) {
          conversationLanguageCode = languageSelect.value
        }

        clearCustomerFormErrors()

        var hasFieldError = false
        if (!name) {
          showCustomerFieldError(nameField, nameError, t('error_name_required'))
          hasFieldError = true
        }
        if (!phoneNational) {
          showCustomerFieldError(phoneField, phoneError, t('error_phone_required'))
          hasFieldError = true
        } else if (!phoneCountry || !acwValidateNationalPhone(phoneNational)) {
          showCustomerFieldError(phoneField, phoneError, t('error_phone_invalid'))
          hasFieldError = true
        }
        if (hasFieldError) {
          if (!name) nameInput.focus()
          else phoneInput.focus()
          return
        }

        pendingCustomerName = name
        pendingCustomerPhone = acwFormatPhoneE164(phoneCountry.dial, phoneNational)
        customerFormGatePassed = true
        customerForm.style.display = 'none'
        syncPanelLayout()
        if (pendingMode === 'voice') {
          if (useRail) {
            panel.style.display = 'none'
            container.classList.remove('spark-acw-panel-visible')
            panel.classList.remove('is-open')
            showRailVoiceOverlay()
          } else {
            showVoicePanel()
          }
          unlockVoiceRemoteAudio()
          setTimeout(function () { startVoiceConnection() }, 0)
        } else {
          connect()
        }
      }
      
      function skipCustomerInfo() {
        clearCustomerFormErrors()
        if (languageSelect && languageSelect.value) {
          conversationLanguageCode = languageSelect.value
        }
        pendingCustomerName = null
        pendingCustomerPhone = null
        customerFormGatePassed = true
        customerForm.style.display = 'none'
        syncPanelLayout()
        if (pendingMode === 'voice') {
          if (useRail) {
            panel.style.display = 'none'
            container.classList.remove('spark-acw-panel-visible')
            panel.classList.remove('is-open')
            showRailVoiceOverlay()
          } else {
            showVoicePanel()
          }
          unlockVoiceRemoteAudio()
          setTimeout(function () { startVoiceConnection() }, 0)
        } else {
          connect()
        }
      }
      
      submitBtn.onclick = submitCustomerInfo
      if (allowSkipCustomerInfo) skipBtn.onclick = skipCustomerInfo
      backToOptions.onclick = function () {
        conversationStarted = false
        formShownBeforeConnect = false
        customerFormGatePassed = false
        pendingMode = null
        nameInput.value = ''
        phoneInput.value = ''
        clearCustomerFormErrors()
        if (languageSelect && supportedLanguagesList.length) {
          languageSelect.selectedIndex = 0
          conversationLanguageCode = supportedLanguagesList[0].language_code || null
          if (customerForm._acwSyncLangSwitcher) customerForm._acwSyncLangSwitcher()
        }
        if (customerForm._acwCloseLangMenu) customerForm._acwCloseLangMenu()
        customerForm.style.display = 'none'
        if (useRail) {
          softClosePanel()
          return
        }
        showStartCta()
      }
      
      // Allow Enter key to submit
      nameInput.addEventListener('keydown', (e) => {
        if (e.key === 'Enter') {
          e.preventDefault()
          phoneInput.focus()
        }
      })
      phoneInput.addEventListener('keydown', (e) => {
        if (e.key === 'Enter') {
          e.preventDefault()
          submitCustomerInfo()
        }
      })
    }

    fetchWidgetConfig(function (data) {
      var langs = data && data.languages && Array.isArray(data.languages) ? data.languages : supportedLanguagesList
      if (!langs || !langs.length) {
        if (languageFieldWrap) languageFieldWrap.style.display = 'none'
        return
      }
      if (!conversationLanguageCode) {
        conversationLanguageCode = langs[0].language_code || null
      }
      if (languageSelect && languageFieldWrap && langSwitchMenu) {
        languageFieldWrap.style.display = 'block'
        languageSelect.innerHTML = ''
        langSwitchMenu.innerHTML = ''
        for (var li = 0; li < langs.length; li++) {
          var L = langs[li]
          var code = L.language_code || ''
          var lbl = L.language || L.language_code || ''
          var opt = document.createElement('option')
          opt.value = code
          opt.textContent = lbl
          languageSelect.appendChild(opt)
          var optBtn = document.createElement('button')
          optBtn.type = 'button'
          optBtn.className = 'acw-lang-option'
          optBtn.setAttribute('role', 'option')
          optBtn.setAttribute('data-code', code)
          optBtn.textContent = lbl
          optBtn.addEventListener('click', function (c) {
            return function (evt) {
              evt.stopPropagation()
              languageSelect.value = c
              conversationLanguageCode = c || null
              try {
                languageSelect.dispatchEvent(new Event('change', { bubbles: true }))
              } catch (_) {
                conversationLanguageCode = languageSelect.value || null
              }
              if (customerForm && customerForm._acwSyncLangSwitcher) {
                customerForm._acwSyncLangSwitcher()
              }
              if (customerForm && customerForm._acwCloseLangMenu) {
                customerForm._acwCloseLangMenu()
              }
            }
          }(code))
          langSwitchMenu.appendChild(optBtn)
        }
        if (langs[0] && langs[0].language_code) {
          languageSelect.value = langs[0].language_code
          conversationLanguageCode = languageSelect.value
        }
        if (customerForm && customerForm._acwSyncLangSwitcher) {
          customerForm._acwSyncLangSwitcher()
        }
      }
    })

    const voicePanelEl = document.createElement('div')
    voicePanelEl.className = 'acw-voice-panel'
    const voiceOrbA = document.createElement('div')
    voiceOrbA.className = 'acw-voice-orb a'
    const voiceOrbB = document.createElement('div')
    voiceOrbB.className = 'acw-voice-orb b'
    const voiceInner = document.createElement('div')
    voiceInner.className = 'acw-voice-inner'
    const voiceTitleEl = document.createElement('div')
    voiceTitleEl.className = 'acw-voice-title'
    voiceTitleEl.textContent = t('voice_title')
    const voiceStatusText = document.createElement('div')
    voiceStatusText.className = 'acw-voice-status'
    voiceStatusText.textContent = t('voice_tap_mic')
    const voiceVisualizerEl = document.createElement('div')
    voiceVisualizerEl.className = 'acw-voice-visualizer'
    for (var vi = 0; vi < 7; vi++) {
      var bar = document.createElement('div')
      bar.className = 'acw-voice-bar'
      voiceVisualizerEl.appendChild(bar)
    }
    const voiceMicWrap = document.createElement('div')
    voiceMicWrap.className = 'acw-voice-mic-wrap'
    const voiceMicBtn = document.createElement('button')
    voiceMicBtn.className = 'acw-voice-mic'
    voiceMicBtn.type = 'button'
    voiceMicBtn.setAttribute('aria-label', t('voice_mic_aria'))
    voiceMicBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>'
    const voiceEndBtn = document.createElement('button')
    voiceEndBtn.className = 'acw-voice-end-row'
    voiceEndBtn.type = 'button'
    voiceEndBtn.style.display = 'none'
    voiceEndBtn.innerHTML = ACW_ICON_END_CALL
    var voiceEndSpan = document.createElement('span')
    voiceEndSpan.textContent = t('end_call')
    voiceEndBtn.appendChild(voiceEndSpan)
    voiceMicWrap.appendChild(voiceMicBtn)
    voiceMicWrap.appendChild(voiceEndBtn)
    voiceInner.appendChild(voiceTitleEl)
    voiceInner.appendChild(voiceStatusText)
    voiceInner.appendChild(voiceVisualizerEl)
    voiceInner.appendChild(voiceMicWrap)
    voicePanelEl.appendChild(voiceOrbA)
    voicePanelEl.appendChild(voiceOrbB)
    voicePanelEl.appendChild(voiceInner)

    var railVoiceOverlayEl = null
    var railVoiceTranscriptEl = null
    var railVoiceMicBtn = null
    var railVoiceStatusEl = null
    var railVoiceStatusTipEl = null
    var railVoiceHintEl = null
    var railVoiceEqEl = null
    var railEqBars = []
    var railVoiceCloseBtn = null
    var railVoiceHomeBtn = null
    var railVoiceEndBtn = null
    var railVoiceOverlayActive = false
    var lastRailUserInterimEl = null
    var lastRailBotInterimEl = null
    var railBotLlmBuffer = ''
    var railBotTtsBuffer = ''
    var railBotTranscriptionText = ''
    var railBotResponseBuffer = ''
    var railPendingBotFinalText = ''

    if (useRail) {
      railVoiceOverlayEl = document.createElement('div')
      railVoiceOverlayEl.className = 'acw-rail-voice-overlay'
      railVoiceOverlayEl.setAttribute('aria-hidden', 'true')

      var railTranscriptWrap = document.createElement('div')
      railTranscriptWrap.className = 'acw-rail-voice-transcript-wrap'

      var railTranscriptToolbar = document.createElement('div')
      railTranscriptToolbar.className = 'acw-rail-voice-transcript-toolbar'
      railVoiceCloseBtn = document.createElement('button')
      railVoiceCloseBtn.type = 'button'
      railVoiceCloseBtn.className = 'acw-rail-voice-close'
      railVoiceCloseBtn.setAttribute('aria-label', t('voice_close_transcript'))
      railVoiceCloseBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg><span>' + t('voice_close_transcript') + '</span>'
      railTranscriptToolbar.appendChild(railVoiceCloseBtn)

      railVoiceTranscriptEl = document.createElement('div')
      railVoiceTranscriptEl.className = 'acw-rail-voice-transcript'
      railVoiceTranscriptEl.setAttribute('role', 'log')
      railVoiceTranscriptEl.setAttribute('aria-live', 'polite')
      railVoiceTranscriptEl.setAttribute('aria-relevant', 'additions text')
      railTranscriptWrap.appendChild(railTranscriptToolbar)
      railTranscriptWrap.appendChild(railVoiceTranscriptEl)
      railVoiceTranscriptEl.addEventListener('scroll', function () {
        updateRailTranscriptFade()
      })

      var railMicDock = document.createElement('div')
      railMicDock.className = 'acw-rail-voice-mic-dock'

      railVoiceHomeBtn = document.createElement('button')
      railVoiceHomeBtn.type = 'button'
      railVoiceHomeBtn.className = 'acw-rail-voice-home'
      railVoiceHomeBtn.setAttribute('aria-label', t('voice_back_to_options'))
      railVoiceHomeBtn.title = t('voice_back_to_options')
      railVoiceHomeBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 10.5L12 3l9 7.5"/><path d="M5 9.5V21h14V9.5"/><path d="M9.5 21v-6h5v6"/></svg>'

      railVoiceEndBtn = document.createElement('button')
      railVoiceEndBtn.type = 'button'
      railVoiceEndBtn.className = 'acw-rail-voice-end'
      railVoiceEndBtn.setAttribute('aria-label', t('end_call'))
      railVoiceEndBtn.title = t('end_call')
      railVoiceEndBtn.innerHTML = ACW_ICON_END_CALL

      var railMicInner = document.createElement('div')
      railMicInner.className = 'acw-rail-voice-mic-inner'
      var railHalo = document.createElement('div')
      railHalo.className = 'acw-rail-voice-halo'
      railHalo.innerHTML = '<span class="ring r1"></span><span class="ring r2"></span><span class="ring r3"></span>'
      railVoiceMicBtn = document.createElement('button')
      railVoiceMicBtn.type = 'button'
      railVoiceMicBtn.className = 'acw-rail-voice-mic'
      railVoiceMicBtn.setAttribute('aria-label', t('voice_mic_aria'))
      railVoiceMicBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></svg>'

      railVoiceStatusEl = document.createElement('div')
      railVoiceStatusEl.className = 'acw-rail-voice-status'
      railVoiceStatusEl.setAttribute('role', 'status')
      railVoiceStatusEl.setAttribute('aria-label', t('voice_status_disconnected'))

      railMicInner.appendChild(railHalo)
      railMicInner.appendChild(railVoiceMicBtn)
      railMicInner.appendChild(railVoiceStatusEl)
      railVoiceHintEl = document.createElement('div')
      railVoiceHintEl.className = 'acw-rail-voice-hint'
      railVoiceHintEl.textContent = t('voice_tap_mic')
      railVoiceEqEl = document.createElement('div')
      railVoiceEqEl.className = 'acw-rail-voice-eq'
      railVoiceEqEl.setAttribute('aria-hidden', 'true')
      for (var eqi = 0; eqi < 5; eqi++) {
        var eqBar = document.createElement('span')
        railVoiceEqEl.appendChild(eqBar)
        railEqBars.push(eqBar)
      }
      railMicDock.appendChild(railVoiceHomeBtn)
      railMicDock.appendChild(railVoiceEndBtn)
      railMicDock.appendChild(railMicInner)
      railMicDock.appendChild(railVoiceEqEl)
      railMicDock.appendChild(railVoiceHintEl)
      railVoiceOverlayEl.appendChild(railTranscriptWrap)
      railVoiceOverlayEl.appendChild(railMicDock)
    }

    function acwCleanTranscriptText(text) {
      var s = String(text || '').replace(/\s+/g, ' ').trim()
      if (!s) return ''
      s = s.replace(/\b(um+|uh+|er+|ah+|like|you know|i mean)\b/gi, '')
      s = s.replace(/\s+/g, ' ').replace(/\s+([,.!?;:])/g, '$1').trim()
      var words = s.split(' ')
      if (words.length > 6) {
        var half = Math.floor(words.length / 2)
        var first = words.slice(0, half).join(' ')
        var second = words.slice(half).join(' ')
        if (first && first === second) s = first
      }
      return s
    }

    function mergeRailPartialText(existingText, incomingText) {
      var existing = String(existingText || '')
      var incoming = String(incomingText || '')
      if (!incoming) return existing
      if (!existing) return incoming
      if (incoming.indexOf(existing) === 0) return incoming
      if (existing.indexOf(incoming) === 0) return existing
      if (/^[,.;:!?]/.test(incoming) || /^\s/.test(incoming) || /\s$/.test(existing)) return existing + incoming
      return existing + ' ' + incoming
    }

    function scrollRailTranscriptToBottom() {
      if (!railVoiceTranscriptEl) return
      requestAnimationFrame(function () {
        railVoiceTranscriptEl.scrollTop = railVoiceTranscriptEl.scrollHeight
      })
    }

    function updateRailTranscriptFade() {
      if (!railVoiceTranscriptEl || !railVoiceOverlayEl) return
      var bubbles = railVoiceTranscriptEl.querySelectorAll('.acw-rail-tbubble:not(.is-interim)')
      for (var i = 0; i < bubbles.length; i++) {
        bubbles[i].classList.remove('is-faded-top')
      }
      if (bubbles.length < 2) return
      var atBottom = railVoiceTranscriptEl.scrollHeight - railVoiceTranscriptEl.scrollTop - railVoiceTranscriptEl.clientHeight < 8
      if (!atBottom) return
      var visibleCount = Math.min(6, bubbles.length)
      var fadeIndex = bubbles.length - visibleCount
      if (fadeIndex >= 0 && bubbles[fadeIndex]) {
        bubbles[fadeIndex].classList.add('is-faded-top')
      }
    }

    function appendRailTranscriptBubble(text, role, opts) {
      opts = opts || {}
      if (!railVoiceTranscriptEl || !text) return null
      var displayText = opts.clean === false ? text : acwCleanTranscriptText(text)
      if (!displayText) return null
      if (railVoiceOverlayEl) railVoiceOverlayEl.classList.add('has-transcript')
      var bubble = document.createElement('div')
      bubble.className = 'acw-rail-tbubble ' + (role === 'user' ? 'is-user' : 'is-bot')
      if (opts.interim) bubble.classList.add('is-interim')
      bubble.textContent = displayText
      railVoiceTranscriptEl.appendChild(bubble)
      scrollRailTranscriptToBottom()
      updateRailTranscriptFade()
      return bubble
    }

    var railLastNavUrl = null
    var railLastNavAt = 0
    /** Navigate once; ignore duplicate requests for the same URL within a short window. */
    function railDoNavigate(nav) {
      if (!nav || !nav.url) return
      var now = Date.now()
      if (nav.url === railLastNavUrl && (now - railLastNavAt) < 4000) return
      railLastNavUrl = nav.url
      railLastNavAt = now
      performNavigation({ url: nav.url, anchor: nav.anchor, label: nav.label })
    }

    function appendRailNavigationOffer(offer) {
      if (!railVoiceTranscriptEl || !offer || !offer.url) return
      if (railVoiceOverlayEl) railVoiceOverlayEl.classList.add('has-transcript')
      var wrap = document.createElement('div')
      wrap.className = 'acw-rail-nav-offer'
      var promptText = offer.promptText || t('navigation_navigated')
      var textEl = document.createElement('div')
      textEl.className = 'acw-rail-nav-offer-text'
      textEl.textContent = promptText
      var actions = document.createElement('div')
      actions.className = 'acw-rail-nav-offer-actions'
      var yesBtn = document.createElement('button')
      yesBtn.type = 'button'
      yesBtn.className = 'acw-rail-nav-offer-btn primary'
      yesBtn.textContent = offer.yesLabel || t('nav_offer_yes')
      var noBtn = document.createElement('button')
      noBtn.type = 'button'
      noBtn.className = 'acw-rail-nav-offer-btn'
      noBtn.textContent = offer.noLabel || t('nav_offer_no')
      actions.appendChild(yesBtn)
      actions.appendChild(noBtn)
      wrap.appendChild(textEl)
      wrap.appendChild(actions)
      railVoiceTranscriptEl.appendChild(wrap)
      scrollRailTranscriptToBottom()
      updateRailTranscriptFade()
      function disableButtons() {
        yesBtn.disabled = true
        noBtn.disabled = true
      }
      yesBtn.onclick = function () {
        disableButtons()
        railDoNavigate({ url: offer.url, anchor: offer.anchor, label: offer.label })
      }
      noBtn.onclick = function () {
        disableButtons()
      }
    }

    function appendOrUpdateRailUserTranscription(text, isFinal) {
      if (!useRail || !railVoiceTranscriptEl) return
      if (isFinal) {
        if (lastRailUserInterimEl && lastRailUserInterimEl.parentNode) {
          lastRailUserInterimEl.parentNode.removeChild(lastRailUserInterimEl)
          lastRailUserInterimEl = null
        }
        if (text) appendRailTranscriptBubble(text, 'user')
        setRailVoicePhase('listening')
        return
      }
      if (!text) return
      setRailVoicePhase('listening')
      if (lastRailUserInterimEl && lastRailUserInterimEl.parentNode) {
        lastRailUserInterimEl.textContent = text
      } else {
        lastRailUserInterimEl = appendRailTranscriptBubble(text, 'user', { interim: true, clean: false })
      }
      scrollRailTranscriptToBottom()
    }

    function flushRailUserInterimToFinal() {
      if (!lastRailUserInterimEl || !lastRailUserInterimEl.parentNode) return
      var finalText = String(lastRailUserInterimEl.textContent || '').trim()
      lastRailUserInterimEl.parentNode.removeChild(lastRailUserInterimEl)
      lastRailUserInterimEl = null
      if (finalText) appendRailTranscriptBubble(finalText, 'user')
    }

    function appendOrUpdateRailBotInterim(displayText) {
      if (!useRail || !railVoiceTranscriptEl || !displayText) return
      if (lastRailBotInterimEl && lastRailBotInterimEl.parentNode) {
        lastRailBotInterimEl.textContent = displayText
      } else {
        lastRailBotInterimEl = appendRailTranscriptBubble(displayText, 'bot', { interim: true })
      }
      scrollRailTranscriptToBottom()
    }

    function finalizeRailAssistantText(text) {
      var candidates = [railBotLlmBuffer || '', railBotTranscriptionText || '', railBotTtsBuffer || '', text || '']
      var finalText = candidates.reduce(function (a, b) {
        return (b || '').trim().length > (a || '').trim().length ? b : a
      }, '').trim()
      railBotResponseBuffer = ''
      railBotLlmBuffer = ''
      railBotTtsBuffer = ''
      railBotTranscriptionText = ''
      if (lastRailBotInterimEl && lastRailBotInterimEl.parentNode) {
        lastRailBotInterimEl.parentNode.removeChild(lastRailBotInterimEl)
      }
      lastRailBotInterimEl = null
      if (!finalText) return
      railPendingBotFinalText = finalText
      appendRailTranscriptBubble(finalText, 'bot')
    }

    function appendRailBotOutput(text) {
      flushRailUserInterimToFinal()
      var trimmed = String(text || '').trim()
      if (railPendingBotFinalText) {
        if (trimmed && trimmed === railPendingBotFinalText) {
          railPendingBotFinalText = ''
          return
        }
        railPendingBotFinalText = ''
      }
      if (lastRailBotInterimEl || railBotLlmBuffer || railBotTtsBuffer || railBotTranscriptionText) {
        finalizeRailAssistantText(text)
      } else if (text) {
        appendRailTranscriptBubble(trimmed, 'bot')
      }
    }

    function bufferRailAssistantLlm(text) {
      var next = String(text || '').trim()
      if (!next) return
      railPendingBotFinalText = ''
      railBotLlmBuffer = mergeRailPartialText(railBotLlmBuffer, next)
      railBotResponseBuffer = railBotLlmBuffer
      appendOrUpdateRailBotInterim(railBotResponseBuffer)
      setRailVoicePhase('thinking')
    }

    function bufferRailAssistantTts(text) {
      var next = String(text || '').trim()
      if (!next) return
      railPendingBotFinalText = ''
      railBotTtsBuffer = mergeRailPartialText(railBotTtsBuffer, next)
      railBotResponseBuffer = railBotLlmBuffer || railBotTtsBuffer
      appendOrUpdateRailBotInterim(railBotResponseBuffer)
      setRailVoicePhase('thinking')
    }

    function handleRailRtviParsedMessage(msg, sourceLabel) {
      if (!useRail || !railVoiceOverlayActive || !msg) return
      var type = msg.type || msg.event
      var label = msg.label || sourceLabel || null
      var payload = (msg.data && typeof msg.data === 'object') ? msg.data : msg
      // RTVI wraps custom server events as { type: 'server-message', data: { type: '<real>', ... } }.
      // Unwrap so the real event type (e.g. navigation_offer) is used for routing.
      if ((type === 'server-message' || type === 'server_message' || type === 'server-response') &&
          payload && typeof payload.type === 'string') {
        type = payload.type
      }

      function handleTyped() {
        // Voice-triggered navigation offer: show the prompt with Yes/No so the visitor can confirm.
        if (enableGuidedNavigation && (type === 'navigation_offer' || type === 'navigation-offer')) {
          var offerUrl = payload.url || msg.url
          if (offerUrl) {
            appendRailNavigationOffer({
              offerId: payload.offer_id || msg.offer_id,
              url: offerUrl,
              anchor: payload.anchor || msg.anchor,
              label: payload.label || msg.label,
              promptText: payload.prompt_text || payload.text || payload.message || msg.prompt_text,
              yesLabel: payload.yes_label || msg.yes_label,
              noLabel: payload.no_label || msg.no_label,
            })
          }
          return
        }
        if (type === 'product_carousel') {
          appendProductCarousel(
            payload.intro_text || payload.text || msg.intro_text || '',
            payload.carousel || payload.product_carousel || msg.carousel || msg.product_carousel,
          )
          if (payload.suggested_actions || msg.suggested_actions) {
            applySuggestedActions(payload.suggested_actions || msg.suggested_actions, true)
          }
          return
        }
        if (type === 'handover_ack') {
          appendHandoverAck(
            payload.content || payload.text || payload.message || msg.content || msg.text || '',
          )
          return
        }
        if (type === 'assistant_message' && (payload.product_carousel || msg.product_carousel)) {
          var railAssistantIntro = payload.text || payload.content || payload.message || msg.text || ''
          if (railAssistantIntro) appendRailTranscriptBubble(railAssistantIntro, 'bot')
          appendProductCarousel('', payload.product_carousel || msg.product_carousel)
          if (payload.suggested_actions || msg.suggested_actions) {
            applySuggestedActions(payload.suggested_actions || msg.suggested_actions, true)
          }
          return
        }
        // Direct navigation action from the agent → navigate immediately (new tab).
        if (enableGuidedNavigation && (
          (type === 'client_action' && (payload.action === 'navigate' || msg.action === 'navigate')) ||
          type === 'navigation' || type === 'navigate'
        )) {
          var navUrl = payload.url || msg.url
          if (navUrl) railDoNavigate({ url: navUrl, anchor: payload.anchor || msg.anchor, label: payload.label || msg.label })
          var navFollow = payload.follow_up_text || msg.follow_up_text
          if (navFollow) appendRailTranscriptBubble(navFollow, 'bot')
          return
        }
        if (type === 'user-transcription' || type === 'user_transcription') {
          appendOrUpdateRailUserTranscription(payload.text || payload.message || payload.content || '', payload.final === true)
        } else if (type === 'user-partial' || type === 'user-tts-text' || type === 'user-stt-text' || type === 'user-transcription-partial' || type === 'user-interim-transcription') {
          var partialUserText = payload.text || payload.message || payload.content || ''
          if (partialUserText) appendOrUpdateRailUserTranscription(partialUserText, false)
        } else if (type === 'bot-output' || type === 'bot_output') {
          appendRailBotOutput(payload.text || payload.message || payload.content || '')
        } else if (type === 'bot-llm-text' || type === 'bot-partial') {
          var partialBotText = payload.text || payload.message || payload.content || ''
          if (partialBotText) { flushRailUserInterimToFinal(); bufferRailAssistantLlm(partialBotText) }
        } else if (type === 'bot-tts-text') {
          var ttsWord = payload.text || payload.message || payload.content || ''
          if (ttsWord) { flushRailUserInterimToFinal(); bufferRailAssistantTts(ttsWord) }
        } else if (type === 'bot-transcription') {
          var transcriptionText = payload.text || payload.message || payload.content || ''
          if (transcriptionText) railBotTranscriptionText = transcriptionText
        } else if (type === 'bot-final' || type === 'bot-response-final') {
          finalizeRailAssistantText(payload.text || payload.message || payload.content || '')
        } else if (type === 'bot-stopped-speaking') {
          finalizeRailAssistantText('')
        } else if (type === 'transcript' || type === 'text_response') {
          var trText = payload.text || payload.message || payload.content || ''
          var trRole = normalizeChatRole(payload.role || 'assistant')
          if (trText) appendRailTranscriptBubble(trText, trRole)
        }
      }

      if (label === 'rtvi-ai') {
        handleTyped()
        return
      }
      handleTyped()
    }

    function parseRailRtviCandidate(candidate, sourceLabel, depth) {
      if (candidate == null || !useRail || !railVoiceOverlayActive) return false
      var maxDepth = 4
      var currentDepth = typeof depth === 'number' ? depth : 0
      if (currentDepth > maxDepth) return false
      var value = candidate
      if (typeof value === 'string') {
        var text = value.trim()
        if (!text) return false
        try { value = JSON.parse(text) } catch (_) { return false }
      }
      if (Array.isArray(value)) {
        var anyHandled = false
        value.forEach(function (item) {
          if (parseRailRtviCandidate(item, sourceLabel, currentDepth + 1)) anyHandled = true
        })
        return anyHandled
      }
      if (!value || typeof value !== 'object') return false
      if (value.type || value.event || value.label) {
        handleRailRtviParsedMessage(value, sourceLabel)
        return true
      }
      var keys = ['message', 'payload', 'data', 'body']
      var nestedHandled = false
      keys.forEach(function (k) {
        if (parseRailRtviCandidate(value[k], sourceLabel, currentDepth + 1)) nestedHandled = true
      })
      return nestedHandled
    }

    function handleRailDailyRtviEvent(kind, ev) {
      if (!useRail || !railVoiceOverlayActive) return
      var data = ev && typeof ev === 'object' && Object.prototype.hasOwnProperty.call(ev, 'data') ? ev.data : ev
      var handled = parseRailRtviCandidate(data, 'rtvi-ai', 0)
      if (!handled && kind === 'transcription-message' && data && typeof data === 'object') {
        var text = data.text || data.transcript || data.message || ''
        var role = normalizeChatRole(data.role || 'assistant')
        if (text) appendRailTranscriptBubble(text, role)
      }
    }

    function setupRailRtviDataChannel(channel) {
      if (!channel || !useRail) return
      channel.onmessage = function (e) {
        if (!railVoiceOverlayActive) return
        try {
          if (typeof Blob !== 'undefined' && e.data instanceof Blob) {
            e.data.text().then(function (text) {
              try { handleRailRtviParsedMessage(JSON.parse(text)) } catch (_) {}
            }).catch(function () {})
            return
          }
          var parsed = typeof e.data === 'string' ? JSON.parse(e.data) : e.data
          if (parsed != null) parseRailRtviCandidate(parsed, 'rtvi-ai', 0)
        } catch (_) {}
      }
    }

    function clearRailTranscript() {
      if (!railVoiceTranscriptEl) return
      railVoiceTranscriptEl.innerHTML = ''
      lastRailUserInterimEl = null
      lastRailBotInterimEl = null
      railBotLlmBuffer = ''
      railBotTtsBuffer = ''
      railBotTranscriptionText = ''
      railBotResponseBuffer = ''
      railPendingBotFinalText = ''
      if (railVoiceOverlayEl) railVoiceOverlayEl.classList.remove('has-transcript')
    }

    function updateDockVisibility() {
      if (!dockEl) return
      var panelOpen = panel.style.display === 'flex' && container.classList.contains('spark-acw-panel-visible')
      var hideDock = panelOpen || railVoiceOverlayActive
      if (hideDock) {
        dockEl.classList.add('acw-dock-hidden')
        dockEl.setAttribute('aria-hidden', 'true')
      } else {
        dockEl.classList.remove('acw-dock-hidden')
        dockEl.setAttribute('aria-hidden', 'false')
      }
    }

    function showRailVoiceOverlay() {
      if (!useRail || !railVoiceOverlayEl) {
        showVoicePanelInternal()
        return
      }
      railVoiceOverlayActive = true
      container.classList.add('spark-acw-rail-voice-active')
      panel.style.display = 'none'
      container.classList.remove('spark-acw-panel-visible')
      panel.classList.remove('is-open')
      railVoiceOverlayEl.classList.add('visible')
      railVoiceOverlayEl.classList.remove('is-ended')
      railVoiceOverlayEl.setAttribute('aria-hidden', 'false')
      clearRailTranscript()
      setVoiceMicConnected(false)
      setRailVoicePhase('tap')
      if (railVoiceMicBtn) {
        railVoiceMicBtn.classList.remove('recording', 'connecting')
        railVoiceMicBtn.disabled = false
        railVoiceMicBtn.setAttribute('aria-pressed', 'false')
      }
      voiceMicIdle = true
      updateDockVisibility()
      updateTextChatHeaderActions()
    }

    function hideRailVoiceOverlay() {
      if (!railVoiceOverlayEl) return
      railVoiceOverlayActive = false
      container.classList.remove('spark-acw-rail-voice-active')
      railVoiceOverlayEl.classList.remove('visible', 'is-speaking', 'has-transcript', 'is-connected', 'is-ended')
      railVoiceOverlayEl.setAttribute('aria-hidden', 'true')
      if (railVoiceMicBtn) {
        railVoiceMicBtn.classList.remove('recording', 'connecting')
        railVoiceMicBtn.disabled = false
      }
      updateDockVisibility()
    }

    function getActiveVoiceMicBtn() {
      return useRail && railVoiceOverlayActive && railVoiceMicBtn ? railVoiceMicBtn : voiceMicBtn
    }

    function setVoiceUiStatus(text) {
      if (voiceStatusText) voiceStatusText.textContent = text
    }

    function setRailVoiceHint(text) {
      if (railVoiceHintEl) railVoiceHintEl.textContent = text || ''
    }

    var railVoicePhase = 'idle'
    function setRailVoicePhase(phase) {
      railVoicePhase = phase
      if (!useRail || !railVoiceOverlayActive) return
      if (phase === 'listening') setRailVoiceHint(t('voice_listening'))
      else if (phase === 'thinking') setRailVoiceHint(t('voice_thinking'))
      else if (phase === 'speaking') setRailVoiceHint(t('voice_speaking'))
      else if (phase === 'connecting') setRailVoiceHint(t('voice_connecting'))
      else setRailVoiceHint(t('voice_tap_mic'))
    }

    function setVoiceMicRecording(isRecording) {
      var mic = getActiveVoiceMicBtn()
      if (mic) {
        if (isRecording) mic.classList.add('recording')
        else mic.classList.remove('recording')
      }
      if (voiceMicBtn && mic !== voiceMicBtn) {
        if (isRecording) voiceMicBtn.classList.add('recording')
        else voiceMicBtn.classList.remove('recording')
      }
    }

    function setVoiceMicConnected(isConnected) {
      if (!railVoiceOverlayEl) return
      if (isConnected) {
        railVoiceOverlayEl.classList.add('is-connected')
        if (railVoiceMicBtn) railVoiceMicBtn.setAttribute('aria-pressed', 'true')
        setRailVoicePhase('listening')
      } else {
        railVoiceOverlayEl.classList.remove('is-connected', 'is-speaking')
        if (railVoiceMicBtn) railVoiceMicBtn.setAttribute('aria-pressed', 'false')
        setRailVoicePhase('tap')
      }
      if (railVoiceStatusEl) {
        var statusLabel = isConnected ? t('voice_status_connected') : t('voice_status_disconnected')
        var statusIcon = isConnected
          ? '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>'
          : ''
        railVoiceStatusEl.innerHTML = statusIcon
        railVoiceStatusEl.setAttribute('aria-label', statusLabel)
        if (railVoiceStatusTipEl && railVoiceStatusTipEl.parentNode) {
          railVoiceStatusTipEl.parentNode.removeChild(railVoiceStatusTipEl)
        }
        if (isConnected) {
          railVoiceStatusTipEl = document.createElement('span')
          railVoiceStatusTipEl.className = 'acw-rail-voice-status-tip'
          railVoiceStatusTipEl.textContent = statusLabel
          railVoiceStatusEl.appendChild(railVoiceStatusTipEl)
        } else {
          railVoiceStatusTipEl = null
        }
      }
    }

    /** End of a rail voice call: keep transcript + close button visible instead of tearing down. */
    function endRailVoiceSession() {
      if (!useRail || !railVoiceOverlayEl) return false
      var hasTranscript = railVoiceTranscriptEl && railVoiceTranscriptEl.querySelector('.acw-rail-tbubble')
      setVoiceMicConnected(false)
      setVoiceMicRecording(false)
      setVoiceMicConnecting(false)
      if (!hasTranscript) {
        hideRailVoiceOverlay()
        clearRailTranscript()
        setRailActive(null)
        return true
      }
      railVoiceOverlayEl.classList.add('is-ended')
      return true
    }

    function markVoiceSessionConnected() {
      setVoiceMicConnecting(false)
      if (useRail && railVoiceOverlayActive) {
        setVoiceMicConnected(true)
      } else {
        setVoiceMicRecording(true)
      }
    }

    function markVoiceSessionDisconnected() {
      setVoiceMicConnected(false)
      setVoiceMicRecording(false)
      setVoiceMicConnecting(false)
    }

    function setVoiceMicConnecting(isConnecting) {
      var mic = getActiveVoiceMicBtn()
      if (mic) {
        if (isConnecting) mic.classList.add('connecting')
        else mic.classList.remove('connecting')
      }
    }

    let voicePeerConnection = null
    let voiceStream = null
    let voiceRemoteAudio = null
    let voiceServerPcId = null
    let voiceDailyCall = null
    let voiceDailySessionId = null
    let voiceMicIdle = true
    let voiceVizCtx = null
    let voiceVizRaf = null
    let voiceAnalyserLocal = null
    let voiceAnalyserRemote = null
    var SILENT_WAV = 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA'

    function ensureVoiceRemoteAudio() {
      if (voiceRemoteAudio) return voiceRemoteAudio
      voiceRemoteAudio = document.createElement('audio')
      voiceRemoteAudio.setAttribute('playsinline', '')
      voiceRemoteAudio.setAttribute('aria-hidden', 'true')
      voiceRemoteAudio.autoplay = true
      voiceRemoteAudio.playsInline = true
      voiceRemoteAudio.preload = 'auto'
      voiceRemoteAudio.muted = false
      voiceRemoteAudio.volume = 1
      voiceRemoteAudio.style.cssText = 'position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;overflow:hidden'
      // Rail mode hides .acw-panel. A media element inside display:none does not play, so keep remote audio on document.body.
      document.body.appendChild(voiceRemoteAudio)
      return voiceRemoteAudio
    }

    function unlockVoiceRemoteAudio() {
      var el = ensureVoiceRemoteAudio()
      try {
        if (!el.srcObject) el.src = SILENT_WAV
        var p = el.play()
        if (p && p.catch) p.catch(function () {})
      } catch (_) {}
      try {
        var ctx = ensureVoiceAudioContext()
        if (ctx && ctx.resume) ctx.resume()
      } catch (_) {}
    }

    function playVoiceRemoteAudio() {
      if (!voiceRemoteAudio) return
      var p = voiceRemoteAudio.play()
      if (p && p.catch) {
        p.catch(function (err) {
          console.warn('[AgentChatWidget] Remote audio play failed', err)
        })
      }
    }

    function attachRemoteAudioTrack(track) {
      if (!track) return
      var el = ensureVoiceRemoteAudio()
      try { el.removeAttribute('src') } catch (_) {}
      var stream = new MediaStream([track])
      el.srcObject = stream
      el.muted = false
      el.volume = 1
      playVoiceRemoteAudio()
      if (!voiceAnalyserRemote) {
        attachVoiceAnalyserForStream(stream, true)
        if (!voiceVizRaf) startVoiceVisualizerLoop()
      }
    }

    function stopVoiceVisualizer() {
      if (voiceVizRaf) {
        cancelAnimationFrame(voiceVizRaf)
        voiceVizRaf = null
      }
      voiceAnalyserLocal = null
      voiceAnalyserRemote = null
      if (voiceVizCtx) {
        try { voiceVizCtx.close() } catch (_) {}
        voiceVizCtx = null
      }
      var bars = voiceVisualizerEl.querySelectorAll('.acw-voice-bar')
      for (var bi = 0; bi < bars.length; bi++) {
        bars[bi].style.height = '24px'
      }
    }

    function ensureVoiceAudioContext() {
      var Ctx = window.AudioContext || window.webkitAudioContext
      if (!Ctx) return null
      if (!voiceVizCtx || voiceVizCtx.state === 'closed') {
        voiceVizCtx = new Ctx()
      }
      if (voiceVizCtx.state === 'suspended') {
        voiceVizCtx.resume().catch(function () {})
      }
      return voiceVizCtx
    }

    function attachVoiceAnalyserForStream(stream, isRemote) {
      if (!stream) return
      var ctx = ensureVoiceAudioContext()
      if (!ctx) return
      try {
        var src = ctx.createMediaStreamSource(stream)
        var analyser = ctx.createAnalyser()
        analyser.fftSize = 256
        analyser.smoothingTimeConstant = 0.55
        src.connect(analyser)
        if (isRemote) voiceAnalyserRemote = analyser
        else voiceAnalyserLocal = analyser
      } catch (e) {
        console.warn('[AgentChatWidget] Voice visualizer analyser failed', e)
      }
    }

    function voiceAnalyserRms(analyser) {
      if (!analyser) return 0
      var buf = new Uint8Array(analyser.fftSize)
      analyser.getByteTimeDomainData(buf)
      var sum = 0
      for (var i = 0; i < buf.length; i++) {
        var c = (buf[i] - 128) / 128
        sum += c * c
      }
      return Math.sqrt(sum / buf.length)
    }

    function startVoiceVisualizerLoop() {
      if (voiceVizRaf) {
        cancelAnimationFrame(voiceVizRaf)
        voiceVizRaf = null
      }
      var bars = voiceVisualizerEl.querySelectorAll('.acw-voice-bar')
      var n = bars.length
      var minH = 24
      var maxH = 72
      var threshold = 0.035
      function tick() {
        var useRailOverlay = useRail && railVoiceOverlayActive
        if (!voicePanelEl.classList.contains('visible') && !useRailOverlay) {
          voiceVizRaf = null
          return
        }
        var localL = voiceAnalyserRms(voiceAnalyserLocal)
        var remoteL = voiceAnalyserRms(voiceAnalyserRemote)
        var active = Math.max(localL, remoteL) > threshold
        if (useRailOverlay && railVoiceOverlayEl) {
          var voiceConnected = railVoiceOverlayEl.classList.contains('is-connected')
          var speaking = voiceConnected && active
          railVoiceOverlayEl.classList.toggle('is-speaking', speaking)
          if (voiceConnected) {
            if (speaking) setRailVoicePhase('speaking')
            else if (railBotLlmBuffer || railBotTtsBuffer || lastRailBotInterimEl) setRailVoicePhase('thinking')
            else setRailVoicePhase('listening')
          }
        }
        var i
        var buf
        if (active) {
          if (voiceAnalyserRemote && voiceAnalyserLocal) {
            var len = voiceAnalyserRemote.frequencyBinCount
            buf = new Uint8Array(len)
            var bufL = new Uint8Array(len)
            voiceAnalyserRemote.getByteFrequencyData(buf)
            voiceAnalyserLocal.getByteFrequencyData(bufL)
            for (i = 0; i < len; i++) buf[i] = Math.max(buf[i], bufL[i])
          } else if (voiceAnalyserRemote) {
            buf = new Uint8Array(voiceAnalyserRemote.frequencyBinCount)
            voiceAnalyserRemote.getByteFrequencyData(buf)
          } else if (voiceAnalyserLocal) {
            buf = new Uint8Array(voiceAnalyserLocal.frequencyBinCount)
            voiceAnalyserLocal.getByteFrequencyData(buf)
          } else {
            buf = new Uint8Array(32)
          }
          for (i = 0; i < n; i++) {
            var idx = Math.min(buf.length - 1, Math.floor(((i + 0.5) / n) * buf.length))
            var v = buf[idx] / 255
            var h = minH + v * (maxH - minH)
            bars[i].style.height = Math.min(maxH, h) + 'px'
          }
          if (useRailOverlay && railEqBars.length) {
            var eqMin = 4
            var eqMax = 14
            for (i = 0; i < railEqBars.length; i++) {
              var eqIdx = Math.min(buf.length - 1, Math.floor(((i + 0.5) / railEqBars.length) * buf.length))
              var eqV = buf[eqIdx] / 255
              railEqBars[i].style.height = Math.round(eqMin + eqV * (eqMax - eqMin)) + 'px'
            }
          }
        } else {
          for (i = 0; i < n; i++) {
            bars[i].style.height = minH + 'px'
          }
          if (useRailOverlay && railEqBars.length) {
            for (i = 0; i < railEqBars.length; i++) {
              railEqBars[i].style.height = '4px'
            }
          }
        }
        voiceVizRaf = requestAnimationFrame(tick)
      }
      voiceVizRaf = requestAnimationFrame(tick)
    }

    function disconnectVoice() {
      stopVoiceVisualizer()
      if (railVoiceOverlayEl) railVoiceOverlayEl.classList.remove('is-speaking')
      var sid = voiceDailySessionId
      var dailyCleanupBase = voiceHttpBase
      voiceDailySessionId = null
      if (sid && dailyCleanupBase && agentId) {
        fetch(dailyCleanupBase + '/api/v1/agent-voice/agents/' + agentId + '/daily/' + sid, { method: 'DELETE' }).catch(function (err) {
          console.warn('[AgentChatWidget] Daily session cleanup DELETE failed', err)
        })
      }
      if (voiceDailyCall && !voiceDailyCall.isDestroyed()) {
        var call = voiceDailyCall
        voiceDailyCall = null
        try { call.stopRemoteParticipantsAudioLevelObserver() } catch (_) {}
        call.leave().then(function () { call.destroy() }).catch(function () {})
      }
      if (voicePeerConnection) {
        try { voicePeerConnection.close() } catch (_) {}
        voicePeerConnection = null
      }
      voiceServerPcId = null
      if (voiceStream) {
        voiceStream.getTracks().forEach(function (t) { t.stop() })
        voiceStream = null
      }
      if (voiceRemoteAudio) {
        voiceRemoteAudio.pause()
        try { voiceRemoteAudio.srcObject = null } catch (_) {}
        try { voiceRemoteAudio.removeAttribute('src') } catch (_) {}
      }
      setVoiceUiStatus(t('voice_tap_mic'))
      markVoiceSessionDisconnected()
      voiceEndBtn.style.display = 'none'
      voiceMicIdle = true
    }

    panel.appendChild(header)
    panel.appendChild(statusEl)
    panel.appendChild(connectionBannerEl)
    const continuityToastEl = document.createElement('div')
    continuityToastEl.className = 'acw-continuity-toast'
    continuityToastEl.setAttribute('role', 'status')
    continuityToastEl.setAttribute('aria-live', 'polite')
    panel.appendChild(continuityToastEl)
    panel.appendChild(startCtaEl)
    panel.appendChild(voicePanelEl)
    panel.appendChild(messages)
    var composerEl = document.createElement('div')
    composerEl.className = 'acw-composer'
    var suggestedQuestionsEl = document.createElement('div')
    suggestedQuestionsEl.className = 'acw-suggested-questions'
    suggestedQuestionsEl.style.display = 'none'
    composerEl.appendChild(suggestedQuestionsEl)
    composerEl.appendChild(inputWrap)
    composerEl.appendChild(footerEl)
    if (customerForm) panel.appendChild(customerForm)
    panel.appendChild(composerEl)
    shadow.appendChild(panel)
    if (railVoiceOverlayEl) shadow.appendChild(railVoiceOverlayEl)
    if (launcherWrap) {
      launcherWrap.appendChild(launcher)
      shadow.appendChild(launcherWrap)
    } else {
      shadow.appendChild(launcher)
    }

    function setPanelOpenState(isOpen) {
      if (isOpen) {
        container.classList.add('spark-acw-panel-visible')
        panel.classList.add('is-open')
        if (!useInline) elevateWidget(container)
        fitPanelToViewport()
      } else {
        container.classList.remove('spark-acw-panel-visible')
        panel.classList.remove('is-open')
        if (useRail && !railVoiceOverlayActive) setRailActive(null)
      }
      updateDockVisibility()
    }

    function openPanelUI() {
      panel.style.display = 'flex'
      setPanelOpenState(true)
      clearIdleDisconnectTimer()
      enablePanelFocusTrap()
      var resumingTextChat =
        conversationStarted &&
        pendingMode !== 'voice' &&
        !railVoiceOverlayActive &&
        !(captureCustomerInfo && !customerInfoSubmitted)
      if (!connected && !connecting && conversationStarted && messageLog.length > 0) {
        setConnectionState('reconnecting')
        connect()
      } else if ((connected || connecting) && resumingTextChat) {
        showChat()
        updateConnectionBanner()
      } else {
        updateConnectionBanner()
      }
      syncPanelLayout()
    }

    // Initial state: show start CTA, hide messages and input until user starts conversation
    messages.style.display = 'none'
    inputWrap.style.display = 'none'

    /** Panel never shrinks below this, even if site chrome leaves less room. */
    var ACW_PANEL_MIN_HEIGHT = 380
    var panelExpanded = false
    var viewportFitTimer = null
    var viewportFitListenersAttached = false
    /**
     * Opt-in only. The widget is a floating overlay and by default paints above site
     * chrome, so it must not resize itself while a sticky header scrolls in and out.
     * Set `viewportTopOffset` to a number (or "auto") to reserve room for a fixed header.
     */
    var viewportClampMode =
      viewportTopOffset === 'auto'
        ? 'auto'
        : !isNaN(parseInt(viewportTopOffset, 10))
          ? 'fixed'
          : 'off'

    function updateExpandButton() {
      if (!expandBtn) return
      var label = panelExpanded ? t('collapse_window') : t('expand_window')
      expandBtn.setAttribute('aria-label', label)
      expandBtn.title = label
      expandBtn.setAttribute('aria-pressed', panelExpanded ? 'true' : 'false')
      expandBtn.innerHTML = panelExpanded ? ACW_ICON_COLLAPSE_WINDOW : ACW_ICON_EXPAND_WINDOW
    }

    function setPanelExpanded(expanded) {
      if (useInline) return
      panelExpanded = !!expanded
      panel.classList.toggle('is-expanded', panelExpanded)
      container.classList.toggle('spark-acw-expanded', panelExpanded)
      updateExpandButton()
      if (viewportClampMode === 'off') {
        container.style.setProperty(
          '--acw-panel-height',
          panelExpanded
            ? 'min(max(780px, 85vh), calc(100vh - 24px))'
            : 'min(max(650px, 60vh), calc(100vh - 24px))',
        )
      } else {
        fitPanelToViewport()
      }
    }

    /**
     * Clamp the panel height to the space left by site chrome. Height only — the panel
     * keeps its single-edge CSS anchoring, because both `top` and `bottom` would resolve
     * against the small launcher anchor container rather than the viewport.
     */
    function fitPanelToViewport() {
      if (useInline || viewportClampMode === 'off') return
      var vh = window.innerHeight || document.documentElement.clientHeight || 0
      if (!vh) return

      var raw =
        viewportClampMode === 'auto'
          ? measureViewportTopObstruction()
          : Math.max(0, parseInt(viewportTopOffset, 10))
      var topOffset = Math.min(raw, Math.floor(vh * 0.5))
      container.style.setProperty('--acw-viewport-top-offset', topOffset + 'px')

      var available
      if (finalPosition === 'top-right' || finalPosition === 'top-left') {
        available = vh - Math.max(88, topOffset) - 16
      } else {
        var anchorBottom = container.getBoundingClientRect().bottom
        if (!(anchorBottom > 0)) anchorBottom = vh - 24
        available = anchorBottom - topOffset
      }

      var preferred = panelExpanded
        ? Math.max(780, Math.floor(vh * 0.85))
        : Math.max(650, Math.floor(vh * 0.6))
      var height = Math.max(ACW_PANEL_MIN_HEIGHT, Math.min(preferred, Math.floor(available)))
      container.style.setProperty('--acw-panel-height', height + 'px')
    }

    function scheduleViewportFit() {
      if (useInline || viewportClampMode === 'off') return
      if (viewportFitTimer) clearTimeout(viewportFitTimer)
      viewportFitTimer = setTimeout(function () {
        viewportFitTimer = null
        fitPanelToViewport()
      }, 120)
    }

    function teardownViewportFit() {
      if (viewportFitListenersAttached) {
        window.removeEventListener('resize', scheduleViewportFit)
        viewportFitListenersAttached = false
      }
      if (viewportFitTimer) {
        clearTimeout(viewportFitTimer)
        viewportFitTimer = null
      }
    }

    if (!useInline && viewportClampMode !== 'off') {
      fitPanelToViewport()
      // Resize only. Listening to scroll would resize the panel as sticky chrome moves.
      window.addEventListener('resize', scheduleViewportFit)
      viewportFitListenersAttached = true
    }

    function syncPanelLayout() {
      header.style.display = 'flex'
    }

    function showStartCta() {
      if (useRail) {
        hideRailVoiceOverlay()
        clearRailTranscript()
        voicePanelEl.classList.remove('visible')
        messages.style.display = 'none'
        inputWrap.style.display = 'none'
        footerEl.style.display = 'none'
        suggestedQuestionsEl.style.display = 'none'
        if (customerForm) customerForm.style.display = 'none'
        setRailActive(null)
        clearSuggestedActions()
        updateTextChatHeaderActions()
        softClosePanel()
        return
      }
      panel.setAttribute('data-acw-view', 'home')
      startCtaEl.style.display = 'flex'
      voicePanelEl.classList.remove('visible')
      messages.style.display = 'none'
      clearSuggestedActions()
      inputWrap.style.display = 'none'
      footerEl.style.display = 'none'
      if (customerForm) customerForm.style.display = 'none'
      updateTextChatHeaderActions()
      syncPanelLayout()
    }
    function showFormOnly() {
      panel.setAttribute('data-acw-view', 'form')
      startCtaEl.style.display = 'none'
      voicePanelEl.classList.remove('visible')
      messages.style.display = 'none'
      inputWrap.style.display = 'none'
      footerEl.style.display = 'none'
      clearSuggestedActions()
      if (customerForm) {
        const backBtn = customerForm.querySelector('.acw-form-back')
        if (backBtn) backBtn.style.display = formShownBeforeConnect ? 'flex' : 'none'
        customerForm.style.display = 'flex'
      }
      updateTextChatHeaderActions()
      syncPanelLayout()
    }
    function showChat() {
      panel.setAttribute('data-acw-view', 'chat')
      startCtaEl.style.display = 'none'
      voicePanelEl.classList.remove('visible')
      if (customerForm) customerForm.style.display = 'none'
      messages.style.display = 'flex'
      inputWrap.style.display = 'block'
      footerEl.style.display = 'block'
      updateTextChatHeaderActions()
      renderSuggestedActions()
      updateConnectionBanner()
      syncPanelLayout()
    }
    function showVoicePanelInternal() {
      panel.setAttribute('data-acw-view', 'voice')
      startCtaEl.style.display = 'none'
      if (customerForm) customerForm.style.display = 'none'
      messages.style.display = 'none'
      inputWrap.style.display = 'none'
      footerEl.style.display = 'none'
      clearSuggestedActions()
      voicePanelEl.classList.add('visible')
      setVoiceUiStatus(t('voice_tap_mic'))
      setVoiceMicRecording(false)
      voiceEndBtn.style.display = 'flex'
      voiceMicIdle = true
      updateTextChatHeaderActions()
      syncPanelLayout()
    }
    function showVoicePanel() {
      if (useRail) {
        showRailVoiceOverlay()
        return
      }
      showVoicePanelInternal()
    }

    function updateTextChatHeaderActions() {
      var inTextChat = messages.style.display === 'flex' && !voicePanelEl.classList.contains('visible')
      var inVoice = voicePanelEl.classList.contains('visible') || (useRail && railVoiceOverlayActive)
      var showDisconnect = inTextChat || inVoice
      disconnectChatBtn.style.display = showDisconnect ? 'inline-flex' : 'none'
      if (inVoice) {
        disconnectChatBtn.innerHTML = ACW_ICON_END_CALL
        disconnectChatBtn.setAttribute('aria-label', t('end_call'))
        disconnectChatBtn.title = t('end_call')
      } else {
        disconnectChatBtn.innerHTML = ACW_ICON_END_CHAT
        disconnectChatBtn.setAttribute('aria-label', t('end_chat'))
        disconnectChatBtn.title = t('end_chat')
      }
    }

    function returnToChannelPicker() {
      hideRailVoiceOverlay()
      disconnectVoice()
      voicePanelEl.classList.remove('visible')
      pendingMode = null
      showStartCta()
    }

    function endActiveVoiceSession() {
      disconnectVoice()
      if (useRail && railVoiceOverlayActive) {
        endRailVoiceSession()
      } else {
        voicePanelEl.classList.remove('visible')
        showStartCta()
      }
      updateTextChatHeaderActions()
    }

    function isSuggestedActionUrlAllowed(url, kind) {
      if (!url) return false
      try {
        var u = new URL(url, window.location.href)
        if (u.protocol !== 'https:' && u.protocol !== 'http:') return false
        if (kind === 'navigation') return navigationUrlAllowed(url, allowedDomains)
        if (kind === 'open_url') return true
      } catch (_) {}
      return false
    }

    function normalizeSuggestedActions(raw) {
      var out = []
      if (!Array.isArray(raw)) return out
      for (var i = 0; i < raw.length; i++) {
        var item = raw[i]
        if (!item || typeof item !== 'object') continue
        var kind = normalizeSuggestedActionKind(item.kind)
        var label = String(item.label || item.text || '').trim()
        if (!label) continue
        if (kind === 'navigation' || kind === 'open_url') {
          var actionUrl = String(item.url || '').trim()
          if (!actionUrl || !isSuggestedActionUrlAllowed(actionUrl, kind)) continue
        }
        if (kind === 'tool') continue
        out.push({
          id: String(item.id || kind + '_' + i),
          kind: kind,
          label: label,
          message: String(item.message || label).trim(),
          url: item.url ? String(item.url).trim() : null,
          anchor: item.anchor ? String(item.anchor).trim() : null,
          label_page: item.label_page ? String(item.label_page).trim() : null,
          prompt_text: item.prompt_text ? String(item.prompt_text).trim() : null,
          yes_label: item.yes_label ? String(item.yes_label).trim() : null,
          no_label: item.no_label ? String(item.no_label).trim() : null,
          open_in_new_tab: item.open_in_new_tab !== false,
          icon: item.icon ? String(item.icon).trim() : null,
          priority: typeof item.priority === 'number' ? item.priority : i,
        })
        if (out.length >= 3) break
      }
      out.sort(function (a, b) {
        return (a.priority || 0) - (b.priority || 0)
      })
      return out.slice(0, 3)
    }

    function hasNavigationChipForUrl(url) {
      if (!url) return false
      var target = normalizeActionNavUrl(url)
      for (var i = 0; i < activeSuggestedActions.length; i++) {
        var action = activeSuggestedActions[i]
        if (action.kind === 'navigation' && normalizeActionNavUrl(action.url) === target) return true
      }
      return false
    }

    function clearSuggestedActions() {
      activeSuggestedActions = []
      suggestedQuestionsEl.classList.remove('is-visible')
      suggestedQuestionsEl.innerHTML = ''
    }

    function applySuggestedActions(raw, replace) {
      if (replace === false && activeSuggestedActions.length) return
      var normalized = normalizeSuggestedActions(raw)
      if (!normalized.length && replace !== false) {
        clearSuggestedActions()
        activeSuggestedActions = []
        syncPanelLayout()
        return
      }
      if (!normalized.length) return
      activeSuggestedActions = normalized
      renderSuggestedActions()
    }

    function canShowSuggestedActions() {
      return (
        panel.getAttribute('data-acw-view') === 'chat' &&
        messages.style.display === 'flex' &&
        inputWrap.style.display !== 'none' &&
        !(customerForm && customerForm.style.display !== 'none' && !customerInfoSubmitted)
      )
    }

    function renderSuggestedActions() {
      suggestedQuestionsEl.innerHTML = ''
      if (!canShowSuggestedActions() || !activeSuggestedActions.length) {
        suggestedQuestionsEl.classList.remove('is-visible')
        syncPanelLayout()
        return
      }
      suggestedQuestionsEl.classList.add('is-visible')
      activeSuggestedActions.forEach(function (action) {
        var btn = document.createElement('button')
        btn.type = 'button'
        btn.className = 'acw-suggested-question acw-action-' + action.kind.replace(/_/g, '-')
        btn.textContent = action.label
        btn.setAttribute('data-acw-action-id', action.id)
        btn.setAttribute('data-acw-action-kind', action.kind)
        btn.onclick = function () {
          btn.classList.add('is-sending')
          setTimeout(function () {
            btn.classList.remove('is-sending')
            handleSuggestedActionTap(action)
          }, 180)
        }
        suggestedQuestionsEl.appendChild(btn)
      })
      syncPanelLayout()
    }

    function fetchSuggestedQuestionsFromApi(done) {
      var base = apiBaseUrl ? String(apiBaseUrl).replace(/\/$/, '') : ''
      if (!base || !agentId || !embedKey) {
        if (done) done(activeSuggestedActions)
        return
      }
      fetch(
        base +
          '/api/v1/ai-agents/' +
          encodeURIComponent(agentId) +
          '/embed-widget-config?embed_key=' +
          encodeURIComponent(embedKey),
      )
        .then(function (res) {
          return res.ok ? res.json() : null
        })
        .then(function (data) {
          if (data && Array.isArray(data.suggested_questions) && data.suggested_questions.length) {
            activeSuggestedActions = questionsListToSuggestedActions(data.suggested_questions)
          }
          if (done) done(activeSuggestedActions)
        })
        .catch(function () {
          if (done) done(activeSuggestedActions)
        })
    }

    function resolveAndRenderSuggestedActions(wsQuestions, wsActions) {
      if (wsActions && normalizeSuggestedActions(wsActions).length) {
        applySuggestedActions(wsActions, true)
        return
      }
      if (wsQuestions && normalizeSuggestedQuestionsList(wsQuestions).length) {
        activeSuggestedActions = questionsListToSuggestedActions(wsQuestions)
        renderSuggestedActions()
        return
      }
      if (activeSuggestedActions.length) {
        renderSuggestedActions()
        return
      }
      fetchSuggestedQuestionsFromApi(function () {
        renderSuggestedActions()
      })
    }

    function dispatchUserMessage(text) {
      var trimmed = String(text || '').trim()
      if (!trimmed) return false
      if (captureCustomerInfo && !customerInfoSubmitted) return false
      if (!ws || ws.readyState !== WebSocket.OPEN) {
        if (connecting) {
          appendMessage(t('connecting_wait'), 'bot')
        } else if (messageLog.length > 0 || conversationStarted) {
          setConnectionState('reconnecting')
          connect()
        } else {
          appendMessage(t('choose_text_chat_home'), 'bot')
        }
        return false
      }
      appendMessage(trimmed, 'user')
      var payload = {
        type: 'user_message',
        text: trimmed,
        role: 'user',
        embed_key: embedKey,
        channel: webChatMeta.channel,
        is_web: webChatMeta.is_web,
        source: webChatMeta.source,
        source_name: webChatMeta.source_name,
      }
      if (sessionId) payload.session_id = sessionId
      ws.send(JSON.stringify(payload))
      showTypingIndicator()
      return true
    }

    function handleSuggestedActionTap(action) {
      if (!action) return
      var kind = action.kind || 'question'
      clearSuggestedActions()
      if (kind === 'question') {
        dispatchUserMessage(action.message || action.label)
        return
      }
      if (kind === 'navigation' && enableGuidedNavigation) {
        if (!isSuggestedActionUrlAllowed(action.url, 'navigation')) {
          appendMessage(t('navigation_blocked'), 'bot')
          return
        }
        appendNavigationOfferButtons({
          offerId: action.id,
          url: action.url,
          anchor: action.anchor,
          label: action.label_page || action.label,
          yesLabel: action.yes_label,
          noLabel: action.no_label,
        })
        return
      }
      if (kind === 'open_url') {
        if (!isSuggestedActionUrlAllowed(action.url, 'open_url')) return
        var chipMessage = String(action.message || '').trim()
        if (chipMessage) dispatchUserMessage(chipMessage)
        try {
          if (action.open_in_new_tab !== false) {
            openNavigationInNewTab(action.url)
          } else {
            window.location.assign(action.url)
          }
        } catch (_) {}
        return
      }
      dispatchUserMessage(action.message || action.label)
    }

    function sendSuggestedQuestion(text) {
      clearSuggestedActions()
      dispatchUserMessage(text)
    }

    function setLauncherOpen(open) {
      if (!launcher.classList.contains('acw-launcher-fab')) return
      if (open) launcher.classList.add('is-open')
      else launcher.classList.remove('is-open')
    }

    function updateUnreadBadge() {
      unreadBadge.textContent = String(unreadCount)
      if (unreadCount > 0) {
        unreadBadge.classList.remove('hidden')
      } else {
        unreadBadge.classList.add('hidden')
      }
    }

    var typingIndicatorEl = null
    /** After human handover the customer waits for a person, not the AI — no typing spinner. */
    var humanHandoverActive = false
    var panelFocusTrapActive = false
    var lastFocusBeforePanel = null
    var pendingReconnectFlash = false
    var continuityToastTimer = null

    function getPanelFocusables() {
      return Array.prototype.slice.call(
        panel.querySelectorAll('button:not([disabled]), textarea:not([disabled]), input:not([disabled])')
      ).filter(function (el) {
        return el.offsetParent !== null || el === input
      })
    }

    function onPanelFocusTrapKey(e) {
      if (e.key !== 'Tab') return
      var focusables = getPanelFocusables()
      if (!focusables.length) return
      var first = focusables[0]
      var last = focusables[focusables.length - 1]
      if (e.shiftKey && document.activeElement === first) {
        e.preventDefault()
        last.focus()
      } else if (!e.shiftKey && document.activeElement === last) {
        e.preventDefault()
        first.focus()
      }
    }

    function enablePanelFocusTrap() {
      if (panelFocusTrapActive || useInline) return
      panelFocusTrapActive = true
      lastFocusBeforePanel = document.activeElement
      panel.addEventListener('keydown', onPanelFocusTrapKey)
      var focusables = getPanelFocusables()
      if (focusables.length) {
        setTimeout(function () { focusables[0].focus() }, 50)
      }
    }

    function disablePanelFocusTrap() {
      if (!panelFocusTrapActive) return
      panelFocusTrapActive = false
      panel.removeEventListener('keydown', onPanelFocusTrapKey)
      if (lastFocusBeforePanel && typeof lastFocusBeforePanel.focus === 'function') {
        try { lastFocusBeforePanel.focus() } catch (_) {}
      }
      lastFocusBeforePanel = null
    }

    function formatPageLabel() {
      var title = String(document.title || '').trim()
      if (title) {
        var shortTitle = title.split('|')[0].split('–')[0].split('-')[0].trim()
        if (shortTitle && shortTitle.length <= 48) return shortTitle
      }
      var path = window.location.pathname || ''
      if (!path || path === '/') return 'this page'
      var segment = path.split('/').filter(Boolean).pop() || 'page'
      return segment.replace(/[-_]+/g, ' ')
    }

    function showContinuityToast(text) {
      if (!continuityToastEl || !text) return
      continuityToastEl.innerHTML =
        '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12l5 5L20 7"/></svg>' +
        '<span>' + text + '</span>'
      continuityToastEl.classList.add('visible')
      if (continuityToastTimer) clearTimeout(continuityToastTimer)
      continuityToastTimer = setTimeout(function () {
        continuityToastEl.classList.remove('visible')
        continuityToastTimer = null
      }, 4200)
    }

    function stripBotMessageActions() {
      var existing = messages.querySelectorAll('.acw-msg-actions')
      for (var ai = 0; ai < existing.length; ai++) {
        var actionEl = existing[ai]
        var wrap = actionEl.closest('.acw-msg-wrap')
        if (wrap && wrap.querySelector('.acw-msg-user')) continue
        if (actionEl.parentNode) actionEl.parentNode.removeChild(actionEl)
      }
    }

    function stripUserMessageActions() {
      var userMsgs = messages.querySelectorAll('.acw-msg-user')
      for (var ui = 0; ui < userMsgs.length; ui++) {
        var parent = userMsgs[ui].parentElement
        if (!parent) continue
        var siblingActions = parent.querySelectorAll(':scope > .acw-msg-actions')
        for (var si = 0; si < siblingActions.length; si++) {
          if (siblingActions[si].parentNode) siblingActions[si].parentNode.removeChild(siblingActions[si])
        }
      }
    }

    function submitMessageFeedback(messageId, rating, messageText, messageType) {
      if (!messageId || !rating) return
      var feedback = { rating: rating, at: Date.now() }
      for (var fi = messageLog.length - 1; fi >= 0; fi--) {
        if (messageLog[fi].messageId === messageId) {
          messageLog[fi].feedback = feedback
          break
        }
      }
      schedulePersistSession()
      var pageCtx = acwPageContext()
      acwDispatchMessageFeedbackEvent({
        message_id: messageId,
        rating: rating,
        message_type: messageType || 'text',
        session_id: sessionId || null,
        agent_id: agentId || null,
        page_url: pageCtx.page_url || null,
        page_path: pageCtx.page_path || null,
      })
      if (!ws || ws.readyState !== WebSocket.OPEN || !sessionId) return
      try {
        ws.send(
          JSON.stringify({
            type: 'message_feedback',
            session_id: sessionId,
            message_id: messageId,
            rating: rating,
            message_text: String(messageText || '').slice(0, 2000),
            message_type: messageType || 'text',
            embed_key: embedKey,
            channel: webChatMeta.channel,
            is_web: webChatMeta.is_web,
            source: webChatMeta.source,
            source_name: webChatMeta.source_name,
            page_url: pageCtx.page_url || null,
            page_path: pageCtx.page_path || null,
            feedback_at: new Date(feedback.at).toISOString(),
          }),
        )
      } catch (err) {
        console.warn('[AgentChatWidget] message_feedback send failed', err)
      }
    }

    function buildBotMessageActions(text, messageId, existingFeedback) {
      var actions = document.createElement('div')
      actions.className = 'acw-msg-actions'
      var copyBtn = document.createElement('button')
      copyBtn.type = 'button'
      copyBtn.className = 'acw-msg-action-btn'
      copyBtn.textContent = t('copy_message')
      copyBtn.onclick = function () {
        var toCopy = String(text || '')
        if (!toCopy) return
        if (navigator.clipboard && navigator.clipboard.writeText) {
          navigator.clipboard.writeText(toCopy).then(function () {
            copyBtn.textContent = t('copied_message')
            setTimeout(function () { copyBtn.textContent = t('copy_message') }, 1500)
          }).catch(function () {})
        }
      }
      var upBtn = document.createElement('button')
      upBtn.type = 'button'
      upBtn.className = 'acw-msg-action-btn is-icon'
      upBtn.setAttribute('aria-label', t('feedback_helpful'))
      upBtn.innerHTML = ACW_ICON_THUMBS_UP
      var downBtn = document.createElement('button')
      downBtn.type = 'button'
      downBtn.className = 'acw-msg-action-btn is-icon'
      downBtn.setAttribute('aria-label', t('feedback_not_helpful'))
      downBtn.innerHTML = ACW_ICON_THUMBS_DOWN

      function applyFeedbackUi(rating) {
        upBtn.disabled = true
        downBtn.disabled = true
        upBtn.classList.toggle('is-active', rating === 'helpful')
        downBtn.classList.toggle('is-active', rating === 'not_helpful')
      }

      upBtn.onclick = function () {
        if (upBtn.disabled) return
        applyFeedbackUi('helpful')
        submitMessageFeedback(messageId, 'helpful', text, 'text')
      }
      downBtn.onclick = function () {
        if (downBtn.disabled) return
        applyFeedbackUi('not_helpful')
        submitMessageFeedback(messageId, 'not_helpful', text, 'text')
      }

      if (existingFeedback && existingFeedback.rating) {
        applyFeedbackUi(existingFeedback.rating)
      }

      actions.appendChild(copyBtn)
      var feedbackGroup = document.createElement('div')
      feedbackGroup.className = 'acw-msg-feedback'
      feedbackGroup.appendChild(upBtn)
      feedbackGroup.appendChild(downBtn)
      actions.appendChild(feedbackGroup)
      return actions
    }

    function attachActionsToLastBotMessage() {
      stripUserMessageActions()
      stripBotMessageActions()
      var botMsgs = messages.querySelectorAll('.acw-msg-bot:not(.acw-msg-typing)')
      if (!botMsgs.length) return
      var lastMsg = botMsgs[botMsgs.length - 1]
      if (!lastMsg.classList.contains('acw-msg-bot') || lastMsg.classList.contains('acw-msg-user')) return
      var wrap = lastMsg.parentElement
      if (!wrap || !wrap.classList.contains('acw-msg-wrap')) {
        wrap = document.createElement('div')
        wrap.className = 'acw-msg-wrap'
        lastMsg.parentNode.insertBefore(wrap, lastMsg)
        wrap.appendChild(lastMsg)
      }
      if (wrap.querySelector('.acw-msg-user')) return
      var msgText = lastMsg.textContent || ''
      if (!msgText.trim()) return
      var messageId = lastMsg.getAttribute('data-acw-message-id') || acwGenerateMessageId()
      lastMsg.setAttribute('data-acw-message-id', messageId)
      var existingFeedback = null
      for (var mi = messageLog.length - 1; mi >= 0; mi--) {
        if (messageLog[mi].messageId === messageId) {
          existingFeedback = messageLog[mi].feedback || null
          break
        }
      }
      wrap.appendChild(buildBotMessageActions(msgText, messageId, existingFeedback))
    }

    function removeTypingIndicator() {
      if (typingIndicatorEl && typingIndicatorEl.parentNode) {
        typingIndicatorEl.parentNode.removeChild(typingIndicatorEl)
      }
      typingIndicatorEl = null
    }

    function scrollMessagesToBottom() {
      requestAnimationFrame(function () {
        messages.scrollTop = messages.scrollHeight
      })
    }

    function clearMessagesDom() {
      var node = messages.lastChild
      while (node) {
        var prev = node.previousSibling
        if (node !== messagesSpacer) {
          try { messages.removeChild(node) } catch (_) {}
        }
        node = prev
      }
    }

    function showTypingIndicator() {
      if (humanHandoverActive) return
      removeTypingIndicator()
      var wrap = document.createElement('div')
      wrap.className = 'acw-msg acw-msg-bot acw-msg-typing'
      wrap.setAttribute('aria-label', 'Assistant is typing')
      wrap.setAttribute('role', 'status')
      for (var ti = 0; ti < 3; ti++) {
        wrap.appendChild(document.createElement('span'))
      }
      messages.appendChild(wrap)
      typingIndicatorEl = wrap
      scrollMessagesToBottom()
    }

    function schedulePersistSession() {
      if (restoringSession || !enableGuidedNavigation) return
      if (persistSessionTimer) clearTimeout(persistSessionTimer)
      persistSessionTimer = setTimeout(function () {
        var pageCtx = acwPageContext()
        persistSessionSnapshot(agentId, {
          sessionId: sessionId,
          panelOpen: panel.style.display === 'flex',
          messages: messageLog.slice(),
          savedAt: Date.now(),
          pagePath: pageCtx.page_path || null,
        })
      }, 300)
    }

    function collectProductCatalogDomains(sourceItems) {
      var hosts = []
      if (!Array.isArray(sourceItems)) return hosts
      function addHost(rawUrl) {
        try {
          var host = new URL(String(rawUrl || '').trim(), window.location.href).hostname.toLowerCase()
          if (!host) return
          if (hosts.indexOf(host) === -1) hosts.push(host)
          var apex = stripWwwHost(host)
          if (apex && apex !== host && hosts.indexOf(apex) === -1) hosts.push(apex)
        } catch (_) {}
      }
      for (var i = 0; i < sourceItems.length; i++) {
        var item = sourceItems[i]
        if (!item || typeof item !== 'object') continue
        addHost(item.pdp_url || item.url)
        if (Array.isArray(item.variants)) {
          for (var vi = 0; vi < item.variants.length; vi++) {
            var variant = item.variants[vi]
            if (variant && typeof variant === 'object') addHost(variant.pdp_url || variant.url)
          }
        }
      }
      return hosts
    }

    function productUrlAllowed(url) {
      var domains = allowedDomains
      if (productCarouselExtraDomains && productCarouselExtraDomains.length) {
        domains = allowedDomains.concat(productCarouselExtraDomains)
      }
      return navigationUrlAllowed(url, domains)
    }

    function sanitizeProductImageUrl(raw) {
      var imageUrl = String(raw || '').trim()
      if (!imageUrl) return ''
      try {
        var imageParsed = new URL(imageUrl, window.location.href)
        if (imageParsed.protocol !== 'https:' && imageParsed.protocol !== 'http:') return ''
        return imageUrl
      } catch (_) {
        return ''
      }
    }

    function formatPriceDisplay(rawPrice, currency) {
      if (rawPrice == null || rawPrice === '') return ''
      if (typeof rawPrice === 'object') {
        if (rawPrice.label) return String(rawPrice.label).trim()
        if (rawPrice.amount != null && rawPrice.amount !== '') {
          return formatProductPrice({
            amount: rawPrice.amount,
            currency: rawPrice.currency || currency || 'USD',
          })
        }
        return ''
      }
      var amount = parseFloat(String(rawPrice).replace(/[^0-9.-]/g, ''))
      if (isNaN(amount)) return String(rawPrice).trim()
      return formatProductPrice({ amount: amount, currency: currency || 'USD' })
    }

    function normalizeProductPdpUrl(rawUrl) {
      return String(rawUrl || '').trim()
    }

    function normalizeProductCurrency(rawCurrency, fallback) {
      var value = String(rawCurrency || fallback || 'USD').trim().toUpperCase()
      return value || 'USD'
    }

    function normalizeProductVariants(rawVariants, productFallback) {
      var out = []
      if (!Array.isArray(rawVariants)) return out
      var fallbackUrl = productFallback && productFallback.pdp_url
      var fallbackImage = productFallback && productFallback.image_url
      var fallbackCurrency = productFallback && productFallback.currency
      for (var vi = 0; vi < rawVariants.length; vi++) {
        var variant = rawVariants[vi]
        if (!variant || typeof variant !== 'object') continue
        var variantId = String(variant.id || '').trim()
        if (!variantId) continue
        var pdpUrl = normalizeProductPdpUrl(variant.pdp_url || variant.url || fallbackUrl)
        if (!pdpUrl || !productUrlAllowed(pdpUrl)) continue
        var variantCurrency = normalizeProductCurrency(
          variant.currency ||
            (variant.price && typeof variant.price === 'object' && variant.price.currency),
          fallbackCurrency,
        )
        var imageUrl = sanitizeProductImageUrl(
          variant.image_url ||
            (variant.images && variant.images[0] && variant.images[0].url) ||
            fallbackImage,
        )
        out.push({
          id: variantId,
          title: String(variant.title || '').trim() || 'Default',
          image_url: imageUrl || PRODUCT_CAROUSEL_PLACEHOLDER_IMAGE,
          pdp_url: pdpUrl,
          currency: variantCurrency,
          price: formatPriceDisplay(variant.price, variantCurrency),
        })
      }
      return out
    }

    function normalizeProductCarouselItem(item, index) {
      if (!item || typeof item !== 'object') return null
      var productId = String(item.id || '').trim()
      if (!productId) return null
      var title = String(item.title || '').trim()
      if (!title) return null
      var currency = normalizeProductCurrency(
        item.currency || (item.price && typeof item.price === 'object' && item.price.currency),
        'USD',
      )
      var imageUrl = sanitizeProductImageUrl(
        item.image_url || (item.images && item.images[0] && item.images[0].url),
      )
      var productSeed = {
        pdp_url: normalizeProductPdpUrl(item.pdp_url || item.url),
        image_url: imageUrl || PRODUCT_CAROUSEL_PLACEHOLDER_IMAGE,
        currency: currency,
      }
      var variants = normalizeProductVariants(item.variants, productSeed)
      var pdpUrl = productSeed.pdp_url || (variants[0] && variants[0].pdp_url) || ''
      if (!pdpUrl || !productUrlAllowed(pdpUrl)) return null
      return {
        id: productId,
        title: truncateProductTitle(title),
        image_url: productSeed.image_url,
        pdp_url: pdpUrl,
        currency: currency,
        price: formatPriceDisplay(item.price, currency) || (variants[0] && variants[0].price) || '',
        variants: variants,
        cta_label: String(item.cta_label || t('product_view')).trim() || t('product_view'),
      }
    }

    function normalizeProductCarousel(raw) {
      if (!raw || typeof raw !== 'object') return null
      var sourceItems = raw.items || raw.results
      if (!Array.isArray(sourceItems)) return null
      productCarouselExtraDomains = collectProductCatalogDomains(sourceItems)
      var items = []
      try {
        for (var i = 0; i < sourceItems.length && items.length < PRODUCT_CAROUSEL_MAX_ITEMS; i++) {
          var normalizedItem = normalizeProductCarouselItem(sourceItems[i], i)
          if (normalizedItem) items.push(normalizedItem)
        }
      } finally {
        productCarouselExtraDomains = null
      }
      if (!items.length) return null
      var showMore = null
      if (raw.show_more && typeof raw.show_more === 'object') {
        var showLabel = String(raw.show_more.label || '').trim()
        var showMessage = String(raw.show_more.message || '').trim()
        if (showLabel && showMessage) showMore = { label: showLabel, message: showMessage }
      }
      return {
        id: String(raw.id || 'product_carousel_' + Date.now()),
        source: raw.source ? String(raw.source) : null,
        total: typeof raw.total === 'number' ? raw.total : items.length,
        items: items,
        show_more: showMore,
      }
    }

    function handleProductPdpTap(product, pdpUrl, variantId) {
      if (!pdpUrl || !productUrlAllowed(pdpUrl)) {
        appendMessage(t('navigation_blocked'), 'bot')
        return
      }
      acwDispatchProductCarouselEvent('product_card_click', {
        carousel_id: product._carouselId || null,
        product_id: product.id,
        variant_id: variantId || null,
        url: pdpUrl,
        source: product._carouselSource || null,
      })
      performNavigation({ url: pdpUrl, label: product.title })
    }

    function buildProductCardPriceEl(priceText) {
      var row = document.createElement('div')
      row.className = 'acw-product-card-price-row'
      if (!priceText) return row
      var priceEl = document.createElement('span')
      priceEl.className = 'acw-product-card-price'
      priceEl.textContent = priceText
      row.appendChild(priceEl)
      return row
    }

    function buildVariantRow(product, variant) {
      var row = document.createElement('div')
      row.className = 'acw-product-variant-row'

      var img = document.createElement('img')
      img.className = 'acw-product-variant-row-img'
      img.loading = 'lazy'
      img.alt = variant.title || product.title
      img.src = variant.image_url || PRODUCT_CAROUSEL_PLACEHOLDER_IMAGE
      row.appendChild(img)

      var body = document.createElement('div')
      body.className = 'acw-product-variant-row-body'

      var titleEl = document.createElement('p')
      titleEl.className = 'acw-product-variant-row-title'
      titleEl.textContent = variant.title
      body.appendChild(titleEl)

      if (variant.price) {
        var priceEl = document.createElement('p')
        priceEl.className = 'acw-product-variant-row-price'
        priceEl.textContent = variant.price
        body.appendChild(priceEl)
      }

      row.appendChild(body)

      var cta = document.createElement('button')
      cta.type = 'button'
      cta.className = 'acw-product-variant-row-cta'
      cta.textContent = product.cta_label || t('product_view')
      cta.onclick = function () {
        handleProductPdpTap(product, variant.pdp_url, variant.id)
      }
      row.appendChild(cta)

      return row
    }

    function renderProductVariantsPanel(panel, product) {
      panel.innerHTML = ''
      if (!product || !product.variants || product.variants.length <= 1) {
        panel.hidden = true
        return
      }
      panel.hidden = false

      var heading = document.createElement('p')
      heading.className = 'acw-product-variants-heading'
      heading.textContent = product.title
      panel.appendChild(heading)

      product.variants.forEach(function (variant) {
        panel.appendChild(buildVariantRow(product, variant))
      })
    }

    function buildProductCard(item, toggleVariantsPanel) {
      var card = document.createElement('article')
      card.className = 'acw-product-card'
      card.setAttribute('data-acw-product-id', item.id)

      var imgWrap = document.createElement('div')
      imgWrap.className = 'acw-product-card-img-wrap'
      var img = document.createElement('img')
      img.className = 'acw-product-card-img'
      img.loading = 'lazy'
      img.alt = item.title || t('product_no_image')
      img.src = item.image_url || PRODUCT_CAROUSEL_PLACEHOLDER_IMAGE
      imgWrap.appendChild(img)
      card.appendChild(imgWrap)

      var body = document.createElement('div')
      body.className = 'acw-product-card-body'

      var titleEl = document.createElement('h4')
      titleEl.className = 'acw-product-card-title'
      titleEl.textContent = item.title
      body.appendChild(titleEl)

      body.appendChild(buildProductCardPriceEl(item.price))

      if (item.variants && item.variants.length > 1 && toggleVariantsPanel) {
        var variantsBtn = document.createElement('button')
        variantsBtn.type = 'button'
        variantsBtn.className = 'acw-product-variants-btn'
        variantsBtn.textContent = t('product_variants').replace('{count}', String(item.variants.length))
        variantsBtn.onclick = function () {
          toggleVariantsPanel(item, variantsBtn)
        }
        body.appendChild(variantsBtn)
      }

      var cta = document.createElement('button')
      cta.type = 'button'
      cta.className = 'acw-product-card-cta'
      cta.textContent = item.cta_label || t('product_view')
      cta.onclick = function () {
        handleProductPdpTap(item, item.pdp_url, null)
      }
      body.appendChild(cta)

      card.appendChild(body)
      return card
    }

    function cloneProductCarouselForPersist(carousel) {
      if (!carousel || typeof carousel !== 'object') return null
      return {
        id: carousel.id,
        source: carousel.source,
        total: carousel.total,
        show_more: carousel.show_more
          ? { label: carousel.show_more.label, message: carousel.show_more.message }
          : null,
        items: (carousel.items || []).map(function (item) {
          return {
            id: item.id,
            title: item.title,
            image_url: item.image_url,
            pdp_url: item.pdp_url,
            currency: item.currency,
            price: item.price,
            cta_label: item.cta_label,
            variants: (item.variants || []).map(function (variant) {
              return {
                id: variant.id,
                title: variant.title,
                image_url: variant.image_url,
                pdp_url: variant.pdp_url,
                currency: variant.currency,
                price: variant.price,
              }
            }),
          }
        }),
      }
    }

    function renderProductCarouselMessage(introText, carousel) {
      if (!carousel || !Array.isArray(carousel.items) || !carousel.items.length) return false
      removeTypingIndicator()
      stripBotMessageActions()

      var carouselHost = messages
      var scrollCarouselIntoView = scrollMessagesToBottom
      if (useRail && railVoiceOverlayActive && railVoiceTranscriptEl) {
        carouselHost = railVoiceTranscriptEl
        scrollCarouselIntoView = function () {
          scrollRailTranscriptToBottom()
          updateRailTranscriptFade()
        }
        if (railVoiceOverlayEl) railVoiceOverlayEl.classList.add('has-transcript')
      }

      var wrap = document.createElement('div')
      wrap.className = 'acw-msg-wrap'

      var intro = String(introText || '').trim()
      if (intro) {
        if (useRail && railVoiceOverlayActive && railVoiceTranscriptEl) {
          appendRailTranscriptBubble(intro, 'bot')
        } else {
          var introMsg = document.createElement('div')
          introMsg.className = 'acw-msg acw-msg-bot'
          introMsg.appendChild(document.createTextNode(intro))
          wrap.appendChild(introMsg)
        }
      }

      var carouselWrap = document.createElement('div')
      carouselWrap.className = 'acw-product-carousel-wrap'

      var track = document.createElement('div')
      track.className = 'acw-product-carousel'
      track.setAttribute('role', 'region')
      track.setAttribute('aria-label', t('product_carousel_aria'))

      var variantsPanel = document.createElement('div')
      variantsPanel.className = 'acw-product-variants-panel'
      variantsPanel.hidden = true
      var activeVariantsProductId = null
      var activeVariantsBtn = null

      function toggleVariantsPanel(product, btn) {
        if (activeVariantsProductId === product.id && !variantsPanel.hidden) {
          variantsPanel.hidden = true
          activeVariantsProductId = null
          if (activeVariantsBtn) activeVariantsBtn.classList.remove('is-active')
          activeVariantsBtn = null
          syncPanelLayout()
          return
        }
        if (activeVariantsBtn) activeVariantsBtn.classList.remove('is-active')
        activeVariantsProductId = product.id
        activeVariantsBtn = btn
        btn.classList.add('is-active')
        renderProductVariantsPanel(variantsPanel, product)
        acwDispatchProductCarouselEvent('product_variants_expand', {
          carousel_id: carousel.id,
          product_id: product.id,
          variant_count: product.variants.length,
          source: carousel.source,
        })
        scrollCarouselIntoView()
        syncPanelLayout()
      }

      carousel.items.forEach(function (item) {
        item._carouselId = carousel.id
        item._carouselSource = carousel.source
        track.appendChild(buildProductCard(item, toggleVariantsPanel))
      })
      carouselWrap.appendChild(track)
      carouselWrap.appendChild(variantsPanel)

      if (carousel.show_more && carousel.show_more.message) {
        var showMoreBtn = document.createElement('button')
        showMoreBtn.type = 'button'
        showMoreBtn.className = 'acw-product-show-more'
        showMoreBtn.textContent = carousel.show_more.label + ' ›'
        showMoreBtn.onclick = function () {
          acwDispatchProductCarouselEvent('product_show_more_click', {
            carousel_id: carousel.id,
            source: carousel.source,
            total: carousel.total,
          })
          dispatchUserMessage(carousel.show_more.message)
        }
        carouselWrap.appendChild(showMoreBtn)
      }

      wrap.appendChild(carouselWrap)
      carouselHost.appendChild(wrap)
      scrollCarouselIntoView()

      if (!restoringSession && panel.style.display !== 'flex') {
        unreadCount++
        updateUnreadBadge()
      }

      if (!restoringSession) {
        acwDispatchProductCarouselEvent('product_carousel_impression', {
          carousel_id: carousel.id,
          source: carousel.source,
          item_count: carousel.items.length,
          total: carousel.total,
        })
      }
      syncPanelLayout()
      return true
    }

    function appendProductCarousel(introText, rawCarousel) {
      var carousel = normalizeProductCarousel(rawCarousel)
      if (!carousel) {
        if (rawCarousel && (rawCarousel.items || rawCarousel.results)) {
          console.warn(
            '[AgentChatWidget] product_carousel dropped: no valid items after normalization (check id, title, pdp_url, allowed_domains)',
            rawCarousel,
          )
        }
        return false
      }
      var rendered = renderProductCarouselMessage(introText, carousel)
      if (rendered && !restoringSession) {
        messageLog.push({
          role: 'bot',
          type: 'product_carousel',
          introText: String(introText || '').trim(),
          carousel: cloneProductCarouselForPersist(carousel),
        })
        schedulePersistSession()
      }
      return rendered
    }

    function appendHandoverAck(content) {
      humanHandoverActive = true
      removeTypingIndicator()
      var label = String(content || t('handover_ack_default')).trim() || t('handover_ack_default')
      var existingAcks = messages.querySelectorAll('.acw-handover-ack')
      if (existingAcks.length) {
        var lastAck = existingAcks[existingAcks.length - 1]
        if (lastAck.getAttribute('data-acw-handover-label') === label) return
      }
      stripBotMessageActions()
      var el = document.createElement('div')
      el.className = 'acw-handover-ack'
      el.setAttribute('role', 'status')
      el.setAttribute('aria-label', label)
      el.setAttribute('title', label)
      el.setAttribute('data-acw-handover-label', label)
      var lineLeft = document.createElement('span')
      lineLeft.className = 'acw-handover-ack-line'
      lineLeft.setAttribute('aria-hidden', 'true')
      var iconWrap = document.createElement('span')
      iconWrap.className = 'acw-handover-ack-icon'
      iconWrap.setAttribute('aria-hidden', 'true')
      iconWrap.innerHTML = ACW_ICON_HANDOVER_ACK
      var lineRight = document.createElement('span')
      lineRight.className = 'acw-handover-ack-line'
      lineRight.setAttribute('aria-hidden', 'true')
      el.appendChild(lineLeft)
      el.appendChild(iconWrap)
      el.appendChild(lineRight)
      messages.appendChild(el)
      scrollMessagesToBottom()
      if (!restoringSession) {
        messageLog.push({ role: 'system', type: 'handover_ack', text: label })
        schedulePersistSession()
      }
      syncPanelLayout()
    }

    function restoreMessageFromLog(entry) {
      if (entry && entry.type === 'handover_ack') {
        appendHandoverAck(entry.text || t('handover_ack_default'))
        return
      }
      if (entry && entry.type === 'product_carousel' && entry.carousel) {
        renderProductCarouselMessage(entry.introText || '', entry.carousel)
        return
      }
      var opts = null
      if (entry && entry.messageId) {
        opts = { messageId: entry.messageId, feedback: entry.feedback || null }
      }
      appendMessage(entry.text, normalizeChatRole(entry.role), null, null, opts)
    }

    function appendMessage(text, role, imageUrl, imageUrls, opts) {
      role = normalizeChatRole(role)
      if (role === 'bot') removeTypingIndicator()
      const msg = document.createElement('div')
      msg.className = 'acw-msg ' + (role === 'user' ? 'acw-msg-user' : 'acw-msg-bot')
      if (text) {
        const textNode = document.createTextNode(text)
        msg.appendChild(textNode)
      }
      var urls = Array.isArray(imageUrls) && imageUrls.length ? imageUrls : (imageUrl ? [imageUrl] : [])
      for (var ui = 0; ui < urls.length; ui++) {
        const img = document.createElement('img')
        img.className = 'acw-msg-img'
        img.src = urls[ui]
        img.alt = ''
        msg.appendChild(img)
      }
      var messageId = null
      if (role === 'bot') {
        messageId =
          (opts && opts.messageId) ||
          (opts && opts.message_id) ||
          acwGenerateMessageId()
        msg.setAttribute('data-acw-message-id', messageId)
      }
      if (role === 'bot') {
        stripBotMessageActions()
        var botWrap = document.createElement('div')
        botWrap.className = 'acw-msg-wrap'
        botWrap.appendChild(msg)
        if (!restoringSession && text) {
          botWrap.appendChild(
            buildBotMessageActions(
              text,
              messageId,
              opts && opts.feedback ? opts.feedback : null,
            ),
          )
        }
        messages.appendChild(botWrap)
      } else {
        messages.appendChild(msg)
        stripUserMessageActions()
      }
      scrollMessagesToBottom()
      if (role === 'bot' && panel.style.display !== 'flex') {
        unreadCount++
        updateUnreadBadge()
      }
      if (!restoringSession && text) {
        if (role === 'bot') {
          messageLog.push({
            role: 'bot',
            text: text,
            messageId: messageId,
            messageType: 'text',
            feedback: opts && opts.feedback ? opts.feedback : null,
          })
        } else {
          messageLog.push({ role: 'user', text: text })
        }
        schedulePersistSession()
      }
      syncPanelLayout()
    }

    function sendNavigationReply(confirmed, offer) {
      clearSuggestedActions()
      if (!ws || ws.readyState !== WebSocket.OPEN) return
      var label = confirmed ? t('nav_offer_yes') : t('nav_offer_no')
      appendMessage(label, 'user')
      var isCrossDomainExternal =
        confirmed && offer && offer.url && isCrossDomainNavigationUrl(offer.url)
      try {
        var confirmPayload = {
          type: 'navigation_confirm',
          confirmed: confirmed,
          offer_id: offer && offer.offerId,
          url: offer && offer.url,
          anchor: offer && offer.anchor,
          session_id: sessionId,
          embed_key: embedKey,
          channel: webChatMeta.channel,
          is_web: webChatMeta.is_web,
          source: webChatMeta.source,
          source_name: webChatMeta.source_name,
        }
        if (isCrossDomainExternal) {
          confirmPayload.open_in_new_tab = true
          confirmPayload.stays_on_current_page = true
        }
        ws.send(JSON.stringify(confirmPayload))
        ws.send(JSON.stringify({
          type: 'user_message',
          text: label,
          role: 'user',
          session_id: sessionId,
          embed_key: embedKey,
          channel: webChatMeta.channel,
          is_web: webChatMeta.is_web,
          source: webChatMeta.source,
          source_name: webChatMeta.source_name,
        }))
      } catch (e) {
        console.error('[AgentChatWidget] navigation confirm error', e)
      }
      showTypingIndicator()
      schedulePersistSession()
      if (confirmed && offer && offer.url) {
        if (isCrossDomainExternal) scheduleKeepAliveOnCurrentPageAfterExternalNav()
        performNavigation({ url: offer.url, anchor: offer.anchor, label: offer.label })
      }
    }

    function appendNavigationOfferButtons(offer) {
      pendingNavigationOffer = offer
      var wrap = document.createElement('div')
      wrap.className = 'acw-msg acw-msg-bot'
      var yesBtn = document.createElement('button')
      yesBtn.type = 'button'
      yesBtn.className = 'acw-nav-offer-btn primary'
      yesBtn.textContent = (offer && offer.yesLabel) || t('nav_offer_yes')
      var noBtn = document.createElement('button')
      noBtn.type = 'button'
      noBtn.className = 'acw-nav-offer-btn'
      noBtn.textContent = (offer && offer.noLabel) || t('nav_offer_no')
      var row = document.createElement('div')
      row.className = 'acw-nav-offer'
      row.appendChild(yesBtn)
      row.appendChild(noBtn)
      wrap.appendChild(row)
      messages.appendChild(wrap)
      scrollMessagesToBottom()

      function disableButtons() {
        yesBtn.disabled = true
        noBtn.disabled = true
      }
      yesBtn.onclick = function () {
        disableButtons()
        sendNavigationReply(true, offer)
      }
      noBtn.onclick = function () {
        disableButtons()
        sendNavigationReply(false, offer)
      }
    }

    function spaRegionSelectors() {
      var list = spaContentSelector ? [spaContentSelector] : []
      if (spaSyncSelectors && spaSyncSelectors.length) {
        spaSyncSelectors.forEach(function (sel) {
          if (sel && list.indexOf(sel) === -1) list.push(sel)
        })
      }
      return list
    }

    function scrollToPageAnchor(anchor) {
      if (!anchor) return false
      var el = document.getElementById(anchor)
      if (!el) return false
      el.scrollIntoView({ behavior: 'smooth', block: 'start' })
      return true
    }

    function fetchAndSwapSpaPage(parsedUrl) {
      var fetchUrl = parsedUrl.href.split('#')[0]
      return fetch(fetchUrl, {
        credentials: 'same-origin',
        headers: { Accept: 'text/html' },
      })
        .then(function (res) {
          if (!res.ok) return null
          return res.text()
        })
        .then(function (html) {
          if (!html) return false
          var doc = parseHtmlDocument(html)
          if (!swapDocumentRegions(doc, spaRegionSelectors())) return false
          if (doc.title) document.title = doc.title
          return true
        })
        .catch(function () {
          return false
        })
    }

    function performSpaNavigation(parsedUrl, anchor, label, historyMode) {
      prepareNavigationHandshake(parsedUrl, anchor, null)
      return fetchAndSwapSpaPage(parsedUrl).then(function (ok) {
        if (!ok) return false
        var historyUrl = parsedUrl.pathname + parsedUrl.search + (anchor ? '#' + anchor : '')
        var state = { acwSpa: true, url: parsedUrl.href.split('#')[0] }
        if (historyMode === 'replace') {
          history.replaceState(state, document.title, historyUrl)
        } else {
          history.pushState(state, document.title, historyUrl)
        }
        window.scrollTo(0, 0)
        scrollToPageAnchor(anchor)
        appendMessage(label ? t('navigation_navigated') + ' (' + label + ')' : t('navigation_navigated'), 'bot')
        return true
      })
    }

    function persistSessionForNavigation(nextPageUrl, nextPagePath) {
      if (!enableGuidedNavigation) return
      if (persistSessionTimer) {
        clearTimeout(persistSessionTimer)
        persistSessionTimer = null
      }
      persistSessionSnapshot(agentId, {
        sessionId: sessionId,
        panelOpen: panel.style.display === 'flex',
        messages: messageLog.slice(),
        savedAt: Date.now(),
        navigationPending: true,
        nextPageUrl: nextPageUrl || null,
        nextPagePath: nextPagePath || null,
      })
    }

    function sendSessionPauseForNavigation(parsedUrl, anchor, opts) {
      if (!enableGuidedNavigation || !sessionId) return
      if (!ws || ws.readyState !== WebSocket.OPEN) return
      var routePath = acwRoutePath(parsedUrl, anchor)
      var pageCtx = acwPageContext()
      try {
        var payload = {
          type: 'session_pause',
          session_id: sessionId,
          reason: opts && opts.scrolledOnly ? 'anchor_scroll' : 'navigation',
          embed_key: embedKey,
          channel: webChatMeta.channel,
          is_web: webChatMeta.is_web,
          source: webChatMeta.source,
          source_name: webChatMeta.source_name,
          next_page_url: parsedUrl.origin + routePath,
          next_page_path: routePath,
        }
        if (pageCtx.page_url) payload.page_url = pageCtx.page_url
        if (pageCtx.page_path) payload.page_path = pageCtx.page_path
        ws.send(JSON.stringify(payload))
      } catch (err) {
        console.warn('[AgentChatWidget] session_pause send failed', err)
      }
    }

    /** Persist sessionStorage + tell talk2ai to defer session close before route change. */
    function prepareNavigationHandshake(parsedUrl, anchor, opts) {
      if (!enableGuidedNavigation) return
      var routePath = acwRoutePath(parsedUrl, anchor)
      var nextUrl = parsedUrl.origin + routePath
      persistSessionForNavigation(nextUrl, routePath)
      if (!(opts && opts.scrolledOnly)) {
        sendSessionPauseForNavigation(parsedUrl, anchor, opts)
      }
    }

    function normalizeNavDedupeUrl(url) {
      try {
        return new URL(url, window.location.href).href.replace(/#.*$/, '')
      } catch (_) {
        return String(url || '').replace(/#.*$/, '')
      }
    }

    function wasExternalNavigationRecently(url) {
      var key = normalizeNavDedupeUrl(url)
      return !!key && key === lastExternalNavUrl && Date.now() - lastExternalNavAt < 5000
    }

    function markExternalNavigationHandled(url) {
      lastExternalNavUrl = normalizeNavDedupeUrl(url)
      lastExternalNavAt = Date.now()
      scheduleKeepAliveOnCurrentPageAfterExternalNav()
    }

    /** Cross-domain guided navigation: new tab only — never change the current page. */
    function navigateCrossDomainExternal(target, label) {
      if (wasExternalNavigationRecently(target)) return true
      if (openNavigationInNewTab(target)) {
        markExternalNavigationHandled(target)
        appendMessage(
          label ? t('navigation_opened_new_tab') + ' (' + label + ')' : t('navigation_opened_new_tab'),
          'bot'
        )
        return true
      }
      appendMessage(t('navigation_popup_blocked'), 'bot')
      return false
    }

    function assignFullPageNavigation(target, parsed, anchor) {
      navigationCommitted = true
      if (enableGuidedNavigation) {
        if (parsed) {
          prepareNavigationHandshake(parsed, anchor, null)
        } else {
          try {
            prepareNavigationHandshake(new URL(target, window.location.href), anchor, null)
          } catch (_) {
            persistSessionForNavigation(target, null)
          }
        }
      }
      window.location.assign(target)
    }

    function performNavigation(opts) {
      if (navigationCommitted) return
      var url = opts && opts.url
      var anchor = opts && opts.anchor
      var label = opts && opts.label
      if (!url || !navigationUrlAllowed(url, allowedDomains)) {
        appendMessage(t('navigation_blocked'), 'bot')
        return
      }

      var parsed
      try {
        parsed = new URL(url, window.location.href)
      } catch (_) {
        appendMessage(t('navigation_blocked'), 'bot')
        return
      }

      var base = parsed.origin + parsed.pathname + parsed.search
      var currentParsed = new URL(window.location.href)
      var currentBase = currentParsed.origin + currentParsed.pathname + currentParsed.search
      var onSamePage = currentBase === base
      var target = anchor ? base + '#' + anchor : parsed.href
      var crossDomain = !isSameNavigationDomain(parsed.hostname, currentParsed.hostname)

      if (onSamePage && anchor) {
        tryFrameworkNavigation(parsed, anchor, label, { scrolledOnly: true, scroll: false }).then(function (ok) {
          if (ok) return
          if (scrollToPageAnchor(anchor)) {
            if (parsed.hash !== window.location.hash) {
              history.pushState({ acwSpa: true, anchor: anchor }, document.title, base + '#' + anchor)
            }
            appendMessage(t('navigation_scrolled'), 'bot')
            return
          }
        })
        return
      }

      if (crossDomain) {
        navigateCrossDomainExternal(target, label)
        return
      }

      // Same-origin SPA: use framework router when detected (Next, Nuxt, Gatsby, host hook).
      if (canUseFrameworkNavigation(parsed)) {
        tryFrameworkNavigation(parsed, anchor, label).then(function (ok) {
          if (!ok) performNavigationFallback(parsed, anchor, label, target, onSamePage)
        })
        return
      }

      performNavigationFallback(parsed, anchor, label, target, onSamePage)
    }

    function performNavigationFallback(parsed, anchor, label, target, onSamePage) {
      var crossDomain = !isSameNavigationDomain(parsed.hostname, window.location.hostname)

      if (crossDomain) {
        navigateCrossDomainExternal(target, label)
        return
      }

      if (enableSpaNavigation && parsed.origin === window.location.origin) {
        navigationCommitted = true
        performSpaNavigation(parsed, anchor, label, 'push').then(function (ok) {
          navigationCommitted = false
          if (!ok) assignFullPageNavigation(target, parsed, anchor)
        })
        return
      }

      assignFullPageNavigation(target, parsed, anchor)
    }

    if (enableSpaNavigation) {
      if (spaClickHandlerRef) {
        document.removeEventListener('click', spaClickHandlerRef, true)
      }
      spaClickHandlerRef = function (e) {
        if (!spaInterceptLinks) return
        var link = e.target && e.target.closest ? e.target.closest(spaInterceptLinks) : null
        if (!link || link.target === '_blank' || link.hasAttribute('download')) return
        var href = link.getAttribute('href')
        if (!href || href.charAt(0) === '#' || href.indexOf('javascript:') === 0) return
        var linkUrl
        try {
          linkUrl = new URL(href, window.location.href)
        } catch (_) {
          return
        }
        if (linkUrl.origin !== window.location.origin) return
        if (!navigationUrlAllowed(linkUrl.href, allowedDomains)) return
        e.preventDefault()
        performNavigation({
          url: linkUrl.href,
          anchor: linkUrl.hash ? linkUrl.hash.slice(1) : null,
        })
      }
      document.addEventListener('click', spaClickHandlerRef, true)

      if (spaPopstateHandlerRef) {
        window.removeEventListener('popstate', spaPopstateHandlerRef)
      }
      spaPopstateHandlerRef = function () {
        if (canUseFrameworkNavigation(new URL(window.location.href))) return
        var parsed = new URL(window.location.href)
        var anchor = parsed.hash ? parsed.hash.slice(1) : null
        fetchAndSwapSpaPage(parsed).then(function (ok) {
          if (!ok) {
            window.location.reload()
            return
          }
          window.scrollTo(0, 0)
          scrollToPageAnchor(anchor)
        })
      }
      window.addEventListener('popstate', spaPopstateHandlerRef)
    }

    // No default greeting; only show messages after connection

    let ws = null
    let connected = false
    let connecting = false
    let idleDisconnectTimer = null
    const IDLE_DISCONNECT_MS = 30000

    function clearIdleDisconnectTimer() {
      if (idleDisconnectTimer) {
        clearTimeout(idleDisconnectTimer)
        idleDisconnectTimer = null
      }
    }

    /** Hide panel but keep text WebSocket when connected; disconnect after idle timeout. */
    function softClosePanel() {
      disablePanelFocusTrap()
      if (useInline) {
        disconnectVoice()
        hideRailVoiceOverlay()
        voicePanelEl.classList.remove('visible')
        panel.style.display = 'flex'
        setPanelOpenState(true)
        showStartCta()
        clearIdleDisconnectTimer()
        var wsInline = ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)
        if (wsInline) {
          idleDisconnectTimer = setTimeout(function () {
            idleDisconnectTimer = null
            disconnect()
          }, IDLE_DISCONNECT_MS)
        }
        return
      }
      var wasVoice = voicePanelEl.classList.contains('visible') || railVoiceOverlayActive
      hideRailVoiceOverlay()
      clearRailTranscript()
      panel.style.display = 'none'
      setPanelOpenState(false)
      setLauncherOpen(false)
      disconnectVoice()
      voicePanelEl.classList.remove('visible')
      if (wasVoice) showStartCta()
      clearIdleDisconnectTimer()
      var wsActive = ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)
      if (wsActive) {
        idleDisconnectTimer = setTimeout(function () {
          idleDisconnectTimer = null
          disconnect()
        }, IDLE_DISCONNECT_MS)
      }
    }

    function acwWebChannelMeta(kind) {
      var host = (typeof window !== 'undefined' && window.location && window.location.hostname) ? window.location.hostname : ''
      var source = host.indexOf('helllo.ai') !== -1 ? 'internal' : 'partner'
      return { channel: kind, is_web: true, source: source, source_name: host }
    }
    var webChatMeta = acwWebChannelMeta('web_chat')
    var webVoiceMeta = acwWebChannelMeta('web_voice')

    /** Current page context so the agent knows where the visitor is (for navigation). */
    function acwPageContext() {
      try {
        var loc = window.location
        return {
          page_url: loc.href,
          page_path: loc.pathname + loc.search,
          page_title: document.title || '',
        }
      } catch (_) {
        return {}
      }
    }

    function getTextChatWsUrl() {
      if (wsUrl) {
        try {
          var abs = new URL(wsUrl, window.location.href)
          if (conversationLanguageCode) abs.searchParams.set('language_code', conversationLanguageCode)
          if (sessionId) abs.searchParams.set('session_id', sessionId)
          return abs.toString()
        } catch (_) {
          return wsUrl
        }
      }
      let wsBase
      if (VERSION.includes('-staging')) {
        wsBase = 'wss://talk2ai-staging-gcp.helllo.ai'
      } else if (VERSION.includes('-prod')) {
        wsBase = 'wss://talk2ai-prod-gcp.helllo.ai'
      } else {
        const baseCandidate = voiceHttpBase || apiBaseUrl || new URL(document.currentScript?.src || window.location.href).origin
        const voiceBase = (baseCandidate || '').replace(/\/$/, '')
        if (voiceBase.startsWith('http://')) {
          wsBase = voiceBase.replace(/^http/, 'ws')
        } else if (voiceBase.startsWith('https://')) {
          wsBase = voiceBase.replace(/^https/, 'wss')
        } else if (voiceBase.startsWith('ws://') || voiceBase.startsWith('wss://')) {
          wsBase = voiceBase
        } else {
          wsBase = 'ws://' + voiceBase
        }
      }
      const params = new URLSearchParams()
      params.set('embed_key', embedKey)
      params.set('host', window.location.hostname)
      params.set('channel', webChatMeta.channel)
      params.set('is_web', 'true')
      params.set('source', webChatMeta.source)
      params.set('source_name', webChatMeta.source_name)
      var chatPageCtx = acwPageContext()
      if (chatPageCtx.page_url) params.set('page_url', chatPageCtx.page_url)
      if (chatPageCtx.page_path) params.set('page_path', chatPageCtx.page_path)
      if (conversationLanguageCode) params.set('language_code', conversationLanguageCode)
      if (sessionId) params.set('session_id', sessionId)
      return `${wsBase}/api/v1/agent-voice/agents/${agentId}/text-chat?${params.toString()}`
    }

    try {
      console.info('[AgentChatWidget] Resolved WS URL:', getTextChatWsUrl())
    } catch (_) {}

    function formatFileSize(bytes) {
      if (bytes >= 1048576) return Math.round(bytes / 1048576) + ' MB'
      if (bytes >= 1024) return Math.round(bytes / 1024) + ' KB'
      return bytes + ' B'
    }

    function createUploadId() {
      try {
        if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID()
      } catch (_) {}
      return 'upload-' + Date.now() + '-' + Math.random().toString(36).slice(2, 10)
    }

    function applyFileUploadUi() {
      var policy = widgetFileUploadPolicy
      var on = !!(policy && policy.enabled)
      composerBox.classList.toggle('acw-file-upload-on', on)
      if (on && policy.allowed_mime_types && policy.allowed_mime_types.length) {
        fileInput.accept = policy.allowed_mime_types.join(',')
      } else {
        fileInput.accept = 'image/*'
      }
      fileInput.multiple = !!(on && policy.max_files > 1)
      updateComposerSendState()
    }

    function getReadyStagedAttachments() {
      return stagedAttachments.filter(function (a) {
        return a.status === 'ready' && a.attachmentId
      })
    }

    function updateComposerSendState() {
      if (input.disabled) return
      var ready = getReadyStagedAttachments()
      var hasText = !!input.value.trim()
      var uploading = stagedAttachments.some(function (a) { return a.status === 'uploading' })
      sendBtn.disabled = uploading || (!hasText && !ready.length)
      if (sendBtn.disabled) sendBtn.classList.add('is-muted')
      else sendBtn.classList.remove('is-muted')
    }

    function findStagedByUploadId(uploadId) {
      for (var i = 0; i < stagedAttachments.length; i++) {
        if (stagedAttachments[i].uploadId === uploadId) return stagedAttachments[i]
      }
      return null
    }

    function findStagedByAttachmentId(attachmentId) {
      for (var j = 0; j < stagedAttachments.length; j++) {
        if (stagedAttachments[j].attachmentId === attachmentId) return stagedAttachments[j]
      }
      return null
    }

    function renderAttachmentsStrip() {
      if (!attachmentsStripEl) return
      attachmentsStripEl.innerHTML = ''
      if (!stagedAttachments.length) {
        attachmentsStripEl.style.display = 'none'
        return
      }
      attachmentsStripEl.style.display = 'flex'
      stagedAttachments.forEach(function (item) {
        var chip = document.createElement('div')
        chip.className = 'acw-attachment-chip'
        if (item.status === 'uploading') chip.classList.add('is-uploading')
        if (item.status === 'error') chip.classList.add('is-error')
        if (item.previewUrl && String(item.mimeType || '').indexOf('image/') === 0) {
          var thumb = document.createElement('img')
          thumb.className = 'acw-attachment-thumb'
          thumb.src = item.previewUrl
          thumb.alt = ''
          chip.appendChild(thumb)
        }
        var nameEl = document.createElement('span')
        nameEl.className = 'acw-attachment-name'
        nameEl.textContent = item.status === 'uploading'
          ? (item.name + '…')
          : (item.status === 'error' ? (item.errorMessage || item.name) : item.name)
        chip.appendChild(nameEl)
        if (item.status === 'ready' && item.attachmentId) {
          var removeBtn = document.createElement('button')
          removeBtn.type = 'button'
          removeBtn.className = 'acw-attachment-remove'
          removeBtn.setAttribute('aria-label', t('remove_attachment'))
          removeBtn.textContent = '×'
          removeBtn.addEventListener('click', function (id) {
            return function () { removeStagedAttachment(id) }
          }(item.attachmentId))
          chip.appendChild(removeBtn)
        } else if (item.status === 'error') {
          var dismissBtn = document.createElement('button')
          dismissBtn.type = 'button'
          dismissBtn.className = 'acw-attachment-remove'
          dismissBtn.setAttribute('aria-label', t('remove_attachment'))
          dismissBtn.textContent = '×'
          dismissBtn.addEventListener('click', function (uid) {
            return function () { dismissFailedUpload(uid) }
          }(item.uploadId))
          chip.appendChild(dismissBtn)
        }
        attachmentsStripEl.appendChild(chip)
      })
    }

    function revokeAttachmentPreview(item) {
      if (!item || !item.previewUrl || String(item.previewUrl).indexOf('blob:') !== 0) return
      try { URL.revokeObjectURL(item.previewUrl) } catch (_) {}
    }

    function clearStagedAttachments(notifyServer) {
      if (notifyServer && ws && ws.readyState === WebSocket.OPEN) {
        stagedAttachments.forEach(function (a) {
          if (a.attachmentId) {
            try {
              ws.send(JSON.stringify({
                type: 'attachment_remove',
                session_id: sessionId,
                attachment_id: a.attachmentId,
              }))
            } catch (_) {}
          }
        })
      }
      stagedAttachments.forEach(revokeAttachmentPreview)
      stagedAttachments = []
      renderAttachmentsStrip()
      updateComposerSendState()
    }

    function dismissFailedUpload(uploadId) {
      var item = findStagedByUploadId(uploadId)
      if (item) revokeAttachmentPreview(item)
      stagedAttachments = stagedAttachments.filter(function (a) { return a.uploadId !== uploadId })
      renderAttachmentsStrip()
      updateComposerSendState()
    }

    function removeStagedAttachment(attachmentId) {
      var item = findStagedByAttachmentId(attachmentId)
      if (!item) return
      if (ws && ws.readyState === WebSocket.OPEN && attachmentId) {
        try {
          ws.send(JSON.stringify({
            type: 'attachment_remove',
            session_id: sessionId,
            attachment_id: attachmentId,
          }))
        } catch (_) {}
      }
      revokeAttachmentPreview(item)
      stagedAttachments = stagedAttachments.filter(function (a) {
        return a.attachmentId !== attachmentId
      })
      renderAttachmentsStrip()
      updateComposerSendState()
    }

    function validateFileForUpload(file) {
      var policy = widgetFileUploadPolicy
      if (!policy || !policy.enabled) return t('file_upload_disabled')
      if (stagedAttachments.length >= policy.max_files) return t('max_files_reached')
      var maxSize = policy.max_file_size_bytes || 1048576
      if (file.size > maxSize) {
        return t('file_too_large').replace('{size}', formatFileSize(maxSize))
      }
      var mime = file.type || 'application/octet-stream'
      var allowed = policy.allowed_mime_types || []
      if (allowed.length && allowed.indexOf(mime) < 0) return t('file_type_not_allowed')
      return null
    }

    function markAttachmentUploadError(uploadId, message) {
      var item = findStagedByUploadId(uploadId)
      if (!item) {
        if (message) appendMessage(message, 'bot')
        return
      }
      item.status = 'error'
      item.errorMessage = message || t('file_upload_failed')
      renderAttachmentsStrip()
      updateComposerSendState()
    }

    function handleAttachmentUploaded(data) {
      var item = findStagedByUploadId(data.upload_id)
      if (!item || !data.attachment) return
      item.status = 'ready'
      item.attachmentId = data.attachment.id
      item.name = data.attachment.name || item.name
      item.mimeType = data.attachment.mime_type || item.mimeType
      if (data.attachment.url && String(item.mimeType || '').indexOf('image/') === 0) {
        revokeAttachmentPreview(item)
        item.previewUrl = data.attachment.url
      }
      renderAttachmentsStrip()
      updateComposerSendState()
    }

    function readFileAsDataUrl(file) {
      return new Promise(function (resolve, reject) {
        var reader = new FileReader()
        reader.onload = function () { resolve(String(reader.result || '')) }
        reader.onerror = function () { reject(new Error('read')) }
        reader.readAsDataURL(file)
      })
    }

    function uploadFileAttachment(file) {
      var err = validateFileForUpload(file)
      if (err) {
        appendMessage(err, 'bot')
        return
      }
      if (!ws || ws.readyState !== WebSocket.OPEN) {
        appendMessage(t('placeholder_connect_first'), 'bot')
        return
      }
      var uploadId = createUploadId()
      var mime = file.type || 'application/octet-stream'
      var previewUrl = null
      if (mime.indexOf('image/') === 0) {
        previewUrl = URL.createObjectURL(file)
      }
      stagedAttachments.push({
        uploadId: uploadId,
        attachmentId: null,
        name: file.name || 'file',
        mimeType: mime,
        previewUrl: previewUrl,
        status: 'uploading',
        errorMessage: null,
      })
      renderAttachmentsStrip()
      updateComposerSendState()
      readFileAsDataUrl(file)
        .then(function (dataUrl) {
          if (!ws || ws.readyState !== WebSocket.OPEN) {
            markAttachmentUploadError(uploadId, t('file_upload_failed'))
            return
          }
          ws.send(JSON.stringify({
            type: 'attachment_upload',
            session_id: sessionId,
            upload_id: uploadId,
            file: {
              name: file.name || 'file',
              mime_type: mime,
              data_base64: dataUrl,
            },
          }))
        })
        .catch(function () {
          markAttachmentUploadError(uploadId, t('image_read_error'))
        })
    }

    function setInputEnabled(enabled) {
      input.disabled = !enabled
      attachBtn.disabled = !enabled
      fileInput.disabled = !enabled
      if (enabled) {
        updateComposerSendState()
      } else {
        sendBtn.disabled = true
        sendBtn.classList.add('is-muted')
      }
      input.placeholder = enabled ? t('placeholder_type_message') : t('placeholder_connect_first')
    }

    function autoResizeTextarea() {
      input.style.minHeight = '0'
      input.style.height = '0px'
      var next = Math.min(Math.max(input.scrollHeight, 40), 100)
      input.style.minHeight = ''
      input.style.height = Math.round(next) + 'px'
    }

    function updateConnectionBanner() {
      var inChat = messages.style.display === 'flex' && !voicePanelEl.classList.contains('visible')
      var showBanner = inChat && messageLog.length > 0 &&
        (connectionState === 'disconnected' || connectionState === 'reconnecting' ||
         connectionState === 'connecting' || connectionState === 'ended')
      if (!showBanner || connectionState === 'connected') {
        connectionBannerEl.style.display = 'none'
        connectionBannerEl.classList.remove('is-reconnecting')
        return
      }
      connectionBannerEl.style.display = 'flex'
      if (connectionState === 'ended') {
        connectionBannerEl.classList.remove('is-reconnecting')
        connectionBannerTextEl.textContent = t('conversation_ended')
        connectionReconnectBtn.textContent = t('start_again')
        connectionReconnectBtn.disabled = false
        connectionReconnectBtn.style.display = 'inline-flex'
        return
      }
      var isReconnecting = connectionState === 'reconnecting' || connectionState === 'connecting'
      connectionBannerEl.classList.toggle('is-reconnecting', isReconnecting)
      connectionBannerTextEl.textContent = isReconnecting ? t('connection_reconnecting') : t('connection_lost')
      connectionReconnectBtn.textContent = t('reconnect')
      connectionReconnectBtn.disabled = isReconnecting || connecting
      connectionReconnectBtn.style.display = isReconnecting ? 'none' : 'inline-flex'
    }

    function setConnectionState(state) {
      connectionState = state
      var isConnected = state === 'connected'
      // Always present as Online in the header — connection details live in the banner.
      subtitleEl.textContent = t('status_connected')
      subtitleEl.className = 'acw-header-subtitle is-connected'
      headerOnlineDots.forEach(function (dot) {
        dot.classList.remove('is-connecting', 'is-disconnected')
      })
      setInputEnabled(isConnected)
      updateConnectionBanner()
    }

    function updateStatus(text, isConnected) {
      setConnectionState(isConnected ? 'connected' : 'disconnected')
    }

    function showConnectingState() {
      panel.setAttribute('data-acw-view', 'connecting')
      startCtaEl.style.display = 'none'
      voicePanelEl.classList.remove('visible')
      if (customerForm) customerForm.style.display = 'none'
      messages.style.display = 'none'
      inputWrap.style.display = 'none'
      footerEl.style.display = 'none'
      clearSuggestedActions()
      updateTextChatHeaderActions()
      syncPanelLayout()
    }

    function restoreCustomerFormAfterConnectFailure() {
      customerFormGatePassed = false
      if (!captureCustomerInfo || !customerForm) return
      formShownBeforeConnect = true
      if (customerForm._acwClearErrors) customerForm._acwClearErrors()
      showFormOnly()
    }

    function connect() {
      if (connected || connecting) {
        if (!(captureCustomerInfo && !customerInfoSubmitted)) {
          showChat()
          updateConnectionBanner()
        }
        return
      }
      if (captureCustomerInfo && !customerInfoSubmitted && !customerFormGatePassed) {
        formShownBeforeConnect = true
        showFormOnly()
        return
      }
      clearIdleDisconnectTimer()
      connecting = true
      startCtaEl.style.display = 'none'
      if (captureCustomerInfo && !customerInfoSubmitted) {
        showConnectingState()
        setConnectionState('connecting')
      } else {
        showChat()
        setConnectionState(messageLog.length > 0 ? 'reconnecting' : 'connecting')
      }
      pendingReconnectFlash = messageLog.length > 0
      const url = getTextChatWsUrl()
      try {
        ws = new WebSocket(url)
      } catch (err) {
        connecting = false
        setConnectionState('disconnected')
        removeTypingIndicator()
        if (captureCustomerInfo && !customerInfoSubmitted) {
          restoreCustomerFormAfterConnectFailure()
        }
        if (messageLog.length === 0) appendMessage(t('failed_connect'), 'bot')
        console.error('[AgentChatWidget] WS error', err)
        return
      }

      ws.onopen = () => {
        connected = true
        connecting = false
        if (captureCustomerInfo && !customerInfoSubmitted) {
          setConnectionState('connecting')
        } else {
          setConnectionState('connected')
          clearExternalNavKeepAlive()
          headerOnlineDots.forEach(function (dot) {
            dot.classList.add('is-just-connected')
            setTimeout(function () { dot.classList.remove('is-just-connected') }, 650)
          })
          if (pendingReconnectFlash) {
            connectionBannerEl.classList.add('is-success')
            connectionBannerTextEl.textContent = t('reconnected_success')
            connectionBannerEl.style.display = 'flex'
            connectionReconnectBtn.style.display = 'none'
            setTimeout(function () {
              connectionBannerEl.classList.remove('is-success')
              updateConnectionBanner()
            }, 2200)
          }
        }
        pendingReconnectFlash = false
      }

      ws.onmessage = (event) => {
        try {
          const data = JSON.parse(event.data)
          removeTypingIndicator()

          // Handle connection_established message
          if (data.type === 'connection_established') {
            if (data.session_id) sessionId = data.session_id
            if (data.file_upload) mergeFileUploadPolicy(data.file_upload)
            clearExternalNavKeepAlive()
            if (enableGuidedNavigation && !restoringSession) schedulePersistSession()

            if (captureCustomerInfo && !customerInfoSubmitted && !customerFormGatePassed) {
              try { ws.close() } catch (_) {}
              connected = false
              connecting = false
              ws = null
              restoreCustomerFormAfterConnectFailure()
              setConnectionState('disconnected')
              return
            }

            if (captureCustomerInfo && !customerInfoSubmitted) {
              if (pendingCustomerName && pendingCustomerPhone && ws && ws.readyState === WebSocket.OPEN) {
                try {
                  var pendingCust = {
                    type: 'customer_info',
                    session_id: sessionId,
                    phone_number: pendingCustomerPhone,
                    customer_name: pendingCustomerName,
                    embed_key: embedKey,
                    channel: webChatMeta.channel,
                    is_web: webChatMeta.is_web,
                    source: webChatMeta.source,
                    source_name: webChatMeta.source_name
                  }
                  if (conversationLanguageCode) pendingCust.language_code = conversationLanguageCode
                  ws.send(JSON.stringify(pendingCust))
                } catch (e) {
                  console.error('[AgentChatWidget] Error sending customer info:', e)
                }
              }
              pendingCustomerName = null
              pendingCustomerPhone = null
              customerInfoSubmitted = true
              customerFormGatePassed = false
              if (data.welcome_message) appendMessage(data.welcome_message, 'bot')
              resolveAndRenderSuggestedActions(data.suggested_questions, data.suggested_actions)
              setConnectionState('connected')
              headerOnlineDots.forEach(function (dot) {
                dot.classList.add('is-just-connected')
                setTimeout(function () { dot.classList.remove('is-just-connected') }, 650)
              })
              showChat()
              return
            }

            if (data.welcome_message) appendMessage(data.welcome_message, 'bot')
            resolveAndRenderSuggestedActions(data.suggested_questions, data.suggested_actions)
            setConnectionState('connected')
            showChat()
            return
          }

          if (data.type === 'suggested_actions') {
            applySuggestedActions(data.actions || data.suggested_actions, data.replace !== false)
            return
          }

          if (data.type === 'product_carousel') {
            appendProductCarousel(
              data.intro_text || data.text || '',
              data.carousel || data.product_carousel,
            )
            if (data.suggested_actions) applySuggestedActions(data.suggested_actions, true)
            return
          }

          if (data.type === 'attachment_uploaded') {
            handleAttachmentUploaded(data)
            return
          }

          if (data.type === 'attachment_error') {
            markAttachmentUploadError(data.upload_id, data.message || t('file_upload_failed'))
            return
          }

          if (data.type === 'attachment_removed') {
            if (data.attachment_id) {
              var removed = findStagedByAttachmentId(data.attachment_id)
              if (removed) revokeAttachmentPreview(removed)
              stagedAttachments = stagedAttachments.filter(function (a) {
                return a.attachmentId !== data.attachment_id
              })
              renderAttachmentsStrip()
              updateComposerSendState()
            }
            return
          }

          if (data.type === 'handover') {
            humanHandoverActive = true
            removeTypingIndicator()
            var handoverText = data.text || data.content || data.message || ''
            if (handoverText) {
              appendMessage(handoverText, 'bot', null, null, {
                messageId: data.message_id || data.id || undefined,
              })
            }
            return
          }

          if (data.type === 'handover_ack') {
            appendHandoverAck(data.content || data.text || data.message || '')
            return
          }

          if (data.type === 'handover_resolved' || data.type === 'handover_end') {
            humanHandoverActive = false
            removeTypingIndicator()
            var resolvedText = data.text || data.content || data.message || ''
            if (resolvedText) {
              appendMessage(resolvedText, 'bot', null, null, {
                messageId: data.message_id || data.id || undefined,
              })
            }
            return
          }

          if (data.type === 'assistant_message') {
            var assistantText = data.text || data.content || data.message || ''
            var assistantMsgId = data.message_id || data.id || null
            if (assistantText) {
              appendMessage(assistantText, 'bot', null, null, {
                messageId: assistantMsgId || undefined,
              })
            }
            if (data.product_carousel) {
              appendProductCarousel('', data.product_carousel)
            }
            if (data.suggested_actions) applySuggestedActions(data.suggested_actions, true)
            return
          }

          if (data.type === 'navigation_offer' && enableGuidedNavigation) {
            if (hasNavigationChipForUrl(data.url)) {
              if (data.prompt_text || data.text) appendMessage(data.prompt_text || data.text || '', 'bot')
              return
            }
            appendMessage(data.prompt_text || data.text || '', 'bot')
            appendNavigationOfferButtons({
              offerId: data.offer_id,
              url: data.url,
              anchor: data.anchor,
              label: data.label,
              yesLabel: data.yes_label,
              noLabel: data.no_label,
            })
            return
          }

          if (data.type === 'client_action' && data.action === 'navigate' && enableGuidedNavigation) {
            if (!navigationCommitted && data.url) {
              var clientNavUrl = data.url
              var clientNavAnchor = data.anchor
              var clientNavLabel = data.label
              try {
                var clientNavParsed = new URL(clientNavUrl, window.location.href)
                var clientTarget = clientNavAnchor
                  ? clientNavParsed.origin + clientNavParsed.pathname + clientNavParsed.search + '#' + clientNavAnchor
                  : clientNavParsed.href
                if (!isSameNavigationDomain(clientNavParsed.hostname, window.location.hostname)) {
                  navigateCrossDomainExternal(clientTarget, clientNavLabel)
                } else {
                  performNavigation({
                    url: clientNavUrl,
                    anchor: clientNavAnchor,
                    label: clientNavLabel,
                  })
                }
              } catch (_) {
                performNavigation({
                  url: clientNavUrl,
                  anchor: clientNavAnchor,
                  label: clientNavLabel,
                })
              }
            }
            if (data.follow_up_text) appendMessage(data.follow_up_text, 'bot')
            if (data.suggested_actions) applySuggestedActions(data.suggested_actions, true)
            return
          }
          
          // Handle regular chat messages
          const text = data.text || data.content || data.message || ''
          const rawRole = data.role || 'assistant'
          if (String(rawRole).toLowerCase() === 'agent') {
            humanHandoverActive = true
          }
          if (String(rawRole).toLowerCase() === 'system') {
            if (data.type === 'handover_ack' || /forwarded to human/i.test(text)) {
              appendHandoverAck(text || t('handover_ack_default'))
            }
            return
          }
          const role = normalizeChatRole(rawRole)
          if (text) {
            appendMessage(
              text,
              role,
              null,
              null,
              role === 'user'
                ? null
                : { messageId: data.message_id || data.id || undefined },
            )
          }
          if (data.product_carousel && role !== 'user') {
            appendProductCarousel('', data.product_carousel)
          }
          if (data.suggested_actions && role !== 'user') {
            applySuggestedActions(data.suggested_actions, true)
          }
        } catch (e) {
          removeTypingIndicator()
          // Fallback to raw text
          if (event.data) {
            appendMessage(String(event.data), 'bot')
          }
        }
      }

      ws.onerror = (e) => {
        console.error('[AgentChatWidget] WS error', e)
        removeTypingIndicator()
        clearIdleDisconnectTimer()
        setConnectionState('disconnected')
        if (captureCustomerInfo && !customerInfoSubmitted) {
          restoreCustomerFormAfterConnectFailure()
        }
        if (messageLog.length === 0) appendMessage(t('failed_connect'), 'bot')
      }

      ws.onclose = (ev) => {
        removeTypingIndicator()
        clearIdleDisconnectTimer()
        connected = false
        connecting = false
        ws = null
        clearStagedAttachments(false)
        if (shouldAutoReconnectAfterExternalNavClose()) {
          externalNavAutoReconnectPending = false
          setConnectionState('reconnecting')
          connect()
          return
        }
        if (ev.wasClean || ev.code === 1000) {
          setConnectionState('ended')
        } else {
          setConnectionState('disconnected')
        }
        if (captureCustomerInfo && !customerInfoSubmitted) {
          restoreCustomerFormAfterConnectFailure()
        }
        if (!ev.wasClean && messageLog.length === 0) {
          appendMessage(t('failed_connect'), 'bot')
        }
      }
    }

    function disconnect(clearPersisted) {
      removeTypingIndicator()
      clearIdleDisconnectTimer()
      clearExternalNavKeepAlive()
      if (ws) {
        ws.close()
        ws = null
      }
      connected = false
      connecting = false

      if (clearPersisted !== false) {
        try {
          clearMessagesDom()
        } catch (_) {}
        sessionId = null
        messageLog = []
        humanHandoverActive = false
        if (enableGuidedNavigation) clearPersistedSession(agentId)
        customerInfoSubmitted = false
        customerFormGatePassed = false
        conversationStarted = false
        formShownBeforeConnect = false
        activeSuggestedActions = initSuggestedActions.slice()
        clearSuggestedActions()
        pendingMode = null
        pendingCustomerName = null
        pendingCustomerPhone = null
        if (customerForm) {
          customerForm.style.display = 'none'
          const nameInput = customerForm.querySelector('#acw-customer-name')
          const phoneInput = customerForm.querySelector('#acw-customer-phone')
          const phoneCodeSelect = customerForm.querySelector('#acw-customer-phone-code')
          const langSel = customerForm.querySelector('#acw-customer-language')
          if (nameInput) nameInput.value = ''
          if (phoneInput) phoneInput.value = ''
          if (phoneCodeSelect) {
            phoneCodeSelect.value = acwDefaultPhoneCountryIso()
            if (customerForm._acwSyncPhoneCode) customerForm._acwSyncPhoneCode()
          }
          if (customerForm._acwClosePhoneCodeMenu) customerForm._acwClosePhoneCodeMenu()
          if (langSel && supportedLanguagesList.length) {
            langSel.selectedIndex = 0
            conversationLanguageCode = supportedLanguagesList[0].language_code || null
            if (customerForm._acwSyncLangSwitcher) customerForm._acwSyncLangSwitcher()
          }
          if (customerForm._acwCloseLangMenu) customerForm._acwCloseLangMenu()
          if (customerForm._acwClearErrors) customerForm._acwClearErrors()
        }
        clearStagedAttachments(false)
        try { fileInput.value = '' } catch (_) {}
        setConnectionState('idle')
        showStartCta()
      } else {
        if (enableGuidedNavigation) {
          persistSessionSnapshot(agentId, {
            sessionId: sessionId,
            panelOpen: panel.style.display === 'flex',
            messages: messageLog.slice(),
            savedAt: Date.now(),
          })
        }
        if (conversationStarted && messageLog.length > 0) {
          showChat()
          setConnectionState('disconnected')
        } else {
          setConnectionState('idle')
        }
      }
    }

    function sendMessage() {
      const text = input.value.trim()
      var readyAttachments = getReadyStagedAttachments()
      if (!text && !readyAttachments.length) return

      if (captureCustomerInfo && !customerInfoSubmitted) {
        return
      }

      if (stagedAttachments.some(function (a) { return a.status === 'uploading' })) {
        return
      }

      if (!ws || ws.readyState !== WebSocket.OPEN) {
        if (connecting) {
          appendMessage(t('connecting_wait'), 'bot')
        } else if (messageLog.length > 0 || conversationStarted) {
          setConnectionState('reconnecting')
          connect()
        } else {
          appendMessage(t('choose_text_chat_home'), 'bot')
        }
        return
      }

      var previewUrls = readyAttachments
        .map(function (a) { return a.previewUrl })
        .filter(function (u) { return !!u })
      clearSuggestedActions()
      appendMessage(
        text,
        'user',
        previewUrls.length === 1 ? previewUrls[0] : null,
        previewUrls.length > 1 ? previewUrls : null,
      )
      var payload = {
        type: 'user_message',
        text: text || '',
        role: 'user',
        embed_key: embedKey,
        channel: webChatMeta.channel,
        is_web: webChatMeta.is_web,
        source: webChatMeta.source,
        source_name: webChatMeta.source_name,
      }
      if (sessionId) payload.session_id = sessionId
      if (readyAttachments.length) {
        payload.attachment_ids = readyAttachments.map(function (a) { return a.attachmentId })
      }
      ws.send(JSON.stringify(payload))
      showTypingIndicator()
      readyAttachments.forEach(revokeAttachmentPreview)
      stagedAttachments = stagedAttachments.filter(function (a) { return a.status !== 'ready' })
      renderAttachmentsStrip()
      input.value = ''
      try { fileInput.value = '' } catch (_) {}
      autoResizeTextarea()
      updateComposerSendState()
    }

    function activateChannel(mode) {
      if (mode === 'whatsapp') {
        var waMsg = typeof whatsappMessage === 'string' ? whatsappMessage : 'Hi! I need assistance.'
        var waUrl = 'https://wa.me/' + whatsappDigits + '?text=' + encodeURIComponent(waMsg)
        try {
          window.open(waUrl, '_blank', 'noopener,noreferrer')
        } catch (_) {}
        return
      }
      var panelOpen = panel.style.display === 'flex'
      if ((panelOpen || railVoiceOverlayActive) && activeRailChannel === mode) {
        softClosePanel()
        return
      }
      pendingMode = mode
      conversationStarted = true
      unreadCount = 0
      updateUnreadBadge()
      setRailActive(mode)
      if (captureCustomerInfo && customerForm && !customerInfoSubmitted) {
        formShownBeforeConnect = true
        showFormOnly()
        openPanelUI()
        var railNameInput = customerForm.querySelector('#acw-customer-name')
        if (railNameInput) setTimeout(function () { railNameInput.focus() }, 100)
      } else if (mode === 'voice') {
        showRailVoiceOverlay()
      } else {
        openPanelUI()
        if (connected || connecting) {
          showChat()
          updateConnectionBanner()
        } else {
          connect()
        }
      }
    }

    if (useRail) {
      startCtaEl.style.display = 'none'
      if (launcherWrap) launcherWrap.style.display = 'none'
      else launcher.style.display = 'none'
      var railEl = document.createElement('div')
      railEl.className = 'acw-rail acw-dock'
      railEl.setAttribute('role', 'group')
      railEl.setAttribute('aria-label', t('choose_communication_style'))
      dockEl = railEl
      orderedRailChannels.forEach(function (channel) {
        var railBtn = document.createElement('button')
        railBtn.type = 'button'
        var isPrimarySize = channel === normalizedPrimaryChannel
        railBtn.className =
          'acw-rail-btn acw-rail-btn-' +
          channel +
          (isPrimarySize ? ' acw-rail-btn-primary' : ' acw-rail-btn-secondary')
        var railLabel =
          channel === 'chat'
            ? t('text_chat_title')
            : channel === 'voice'
              ? t('voice_chat_title')
              : t('whatsapp_title')
        railBtn.setAttribute('aria-label', railLabel)
        if (showRailTooltips !== false) railBtn.title = railLabel
        railBtn.innerHTML =
          channel === 'chat' ? chatModeSvg : channel === 'voice' ? micModeSvg : waModeSvg
        railBtn.setAttribute('data-testid', 'acw-rail-' + channel)
        railBtn.onclick = function () {
          activateChannel(channel)
        }
        railButtons[channel] = railBtn
        railEl.appendChild(railBtn)
      })
      shadow.appendChild(railEl)
    }

    launcher.onclick = function () {
      var isOpen = panel.style.display === 'flex'
      if (isOpen) {
        softClosePanel()
      } else {
        openPanelUI()
        setLauncherOpen(true)
        unreadCount = 0
        updateUnreadBadge()
        if (!connected && !connecting && conversationStarted && messageLog.length > 0) {
          setConnectionState('reconnecting')
          connect()
        } else if (!connected && !connecting && !conversationStarted) {
          showStartCta()
        }
      }
    }

    closeBtn.onclick = function () {
      softClosePanel()
    }

    expandBtn.onclick = function () {
      setPanelExpanded(!panelExpanded)
    }

    disconnectChatBtn.onclick = function () {
      var inVoice = voicePanelEl.classList.contains('visible') || (useRail && railVoiceOverlayActive)
      if (inVoice) {
        endActiveVoiceSession()
        return
      }
      disconnect()
    }

    connectionReconnectBtn.onclick = function () {
      if (connectionState === 'ended') {
        disconnect(true)
        return
      }
      if (connected || connecting) return
      setConnectionState('reconnecting')
      connect()
    }

    attachBtn.onclick = function () {
      if (attachBtn.disabled) return
      fileInput.click()
    }
    fileInput.addEventListener('change', function () {
      var files = fileInput.files
      if (!files || !files.length) return
      for (var fi = 0; fi < files.length; fi++) {
        uploadFileAttachment(files[fi])
      }
      try { fileInput.value = '' } catch (_) {}
    })

    sendBtn.onclick = sendMessage
    input.addEventListener('input', function () {
      autoResizeTextarea()
      updateComposerSendState()
    })
    input.addEventListener('keydown', function (e) {
      if (e.key === 'Enter' && !e.shiftKey) {
        e.preventDefault()
        sendMessage()
      }
    })

    async function startVoiceConnection() {
      if (!voiceMicIdle) return
      voiceMicIdle = false
      unlockVoiceRemoteAudio()
      setVoiceUiStatus(t('voice_connecting'))
      if (useRail && railVoiceOverlayActive) setRailVoicePhase('connecting')
      setVoiceMicConnecting(true)
      if (!useRail || !railVoiceOverlayActive) {
        voiceEndBtn.style.display = 'flex'
        setVoiceMicRecording(true)
      }

      if (useDailyVoice) {
        try {
          var dailyBase = voiceHttpBase
          var selfHostedDaily = !!dailyBase
          if (!selfHostedDaily && !dailyApiKey) {
            setVoiceUiStatus(t('voice_url_or_key_required'))
            markVoiceSessionDisconnected()
            if (!useRail || !railVoiceOverlayActive) voiceEndBtn.style.display = 'none'
            voiceMicIdle = true
            return
          }

          var Daily = window.Daily
          if (!Daily) {
            setVoiceUiStatus(t('voice_loading_sdk'))
            await new Promise(function (resolve, reject) {
              var s = document.createElement('script')
              s.src = dailyScriptUrl
              s.onload = function () {
                if (window.Daily) resolve(); else reject(new Error('Daily SDK not loaded'))
              }
              s.onerror = function () { reject(new Error('Failed to load Daily SDK')) }
              document.head.appendChild(s)
            })
            Daily = window.Daily
          }

          function attachRemote(track) {
            attachRemoteAudioTrack(track)
          }

          function wireDailyEvents(dailyCall) {
            voiceDailyCall = dailyCall
            ensureVoiceRemoteAudio()
            dailyCall.on('track-started', function (ev) {
              if (!ev || !ev.track || ev.track.kind !== 'audio') return
              if (ev.participant && ev.participant.local) return
              attachRemote(ev.track)
            })
            dailyCall.on('participant-joined', function (ev) {
              if (ev.participant.local) return
              var t = ev.participant.tracks && ev.participant.tracks.audio && ev.participant.tracks.audio.persistentTrack
              if (t) attachRemote(t)
            })
            dailyCall.on('participant-updated', function (ev) {
              if (ev.participant.local) return
              var t = ev.participant.tracks && ev.participant.tracks.audio && ev.participant.tracks.audio.persistentTrack
              if (t) attachRemote(t)
            })
            dailyCall.on('participant-left', function (ev) {
              if (ev.participant && ev.participant.user_name === 'talk2ai-bot') {
                disconnectVoice()
                if (useRail && railVoiceOverlayActive) {
                  endRailVoiceSession()
                } else {
                  voicePanelEl.classList.remove('visible')
                  showStartCta()
                }
              }
            })
            dailyCall.on('left-meeting', function () {
              disconnectVoice()
              if (useRail && railVoiceOverlayActive) {
                endRailVoiceSession()
              } else {
                voicePanelEl.classList.remove('visible')
                showStartCta()
              }
            })
            dailyCall.on('app-message', function (ev) {
              handleRailDailyRtviEvent('app-message', ev)
            })
            dailyCall.on('transcription-message', function (ev) {
              handleRailDailyRtviEvent('transcription-message', ev)
            })
            dailyCall.on('error', function (ev) {
              console.error('[AgentChatWidget] Daily error', ev)
              setVoiceUiStatus(ev.errorMsg || t('voice_error'))
              disconnectVoice()
            })
          }

          function completeDailyJoin(dailyCall) {
            try {
              var parts = dailyCall.participants()
              if (parts) {
                for (var lid in parts) {
                  var part = parts[lid]
                  if (!part || !part.local) continue
                  var localTrack = part.tracks && part.tracks.audio && part.tracks.audio.persistentTrack
                  if (localTrack && !voiceAnalyserLocal) {
                    attachVoiceAnalyserForStream(new MediaStream([localTrack]), false)
                    if (!voiceVizRaf) startVoiceVisualizerLoop()
                  }
                  break
                }
              }
            } catch (_) {}
            var participants = dailyCall.participants()
            for (var pid in participants) {
              if (participants[pid].local) continue
              var tr = participants[pid].tracks && participants[pid].tracks.audio && participants[pid].tracks.audio.persistentTrack
              if (tr) { attachRemote(tr); break }
            }
            setVoiceUiStatus(t('voice_connected_speak'))
            markVoiceSessionConnected()
          }

          if (selfHostedDaily) {
            setVoiceUiStatus(t('voice_starting_session'))
            var voicePageCtx = acwPageContext()
            var dataPayload = {
              tenant: voiceTenant,
              live_transcript: effectiveLiveTranscript,
              show_transcript: effectiveLiveTranscript,
              channel: webVoiceMeta.channel,
              is_web: webVoiceMeta.is_web,
              source: webVoiceMeta.source,
              source_name: webVoiceMeta.source_name,
              page_url: voicePageCtx.page_url,
              page_path: voicePageCtx.page_path,
              page_title: voicePageCtx.page_title
            }
            if (pendingCustomerName) dataPayload.customer_name = pendingCustomerName
            if (pendingCustomerPhone) dataPayload.phone_number = pendingCustomerPhone
            if (voiceSessionData && typeof voiceSessionData === 'object' && !Array.isArray(voiceSessionData)) {
              for (var vk in voiceSessionData) {
                if (Object.prototype.hasOwnProperty.call(voiceSessionData, vk)) {
                  dataPayload[vk] = voiceSessionData[vk]
                }
              }
            }
            if (conversationLanguageCode) dataPayload.language_code = conversationLanguageCode
            var startRes = await fetch(dailyBase + '/api/v1/agent-voice/agents/' + agentId + '/daily/start', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({
                data: dataPayload
              })
            })
            if (!startRes.ok) {
              var errJson = await startRes.json().catch(function () { return {} })
              var detail = errJson && errJson.detail
              throw new Error(detail ? String(detail) : ('Failed to start Daily session: ' + startRes.status))
            }
            var shData = await startRes.json()
            var roomUrl = shData.room_url
            var dToken = shData.token
            var sessId = shData.session_id
            if (!roomUrl || !dToken) throw new Error('Missing room_url or token')
            voiceDailySessionId = sessId || null
            setVoiceUiStatus(t('voice_joining_room'))
            var dailyCallSh = createDailyCallObject(Daily, { audioSource: true, videoSource: false })
            wireDailyEvents(dailyCallSh)
            await dailyCallSh.join({ url: roomUrl, token: dToken, startVideoOff: true, videoSource: false })
            completeDailyJoin(dailyCallSh)
          } else {
            setVoiceUiStatus(t('voice_starting_session'))
            var pipecatPageCtx = acwPageContext()
            var bodyPayload = {
              agent_id: agentId,
              tenant: 'default',
              channel: webVoiceMeta.channel,
              is_web: webVoiceMeta.is_web,
              source: webVoiceMeta.source,
              source_name: webVoiceMeta.source_name,
              data: {
                page_url: pipecatPageCtx.page_url,
                page_path: pipecatPageCtx.page_path,
                page_title: pipecatPageCtx.page_title
              }
            }
            if (pendingCustomerName) bodyPayload.data.customer_name = pendingCustomerName
            if (pendingCustomerPhone) bodyPayload.data.phone_number = pendingCustomerPhone
            if (conversationLanguageCode) {
              if (!bodyPayload.data) bodyPayload.data = {}
              bodyPayload.data.language_code = conversationLanguageCode
            }
            if (effectiveLiveTranscript) {
              if (!bodyPayload.data) bodyPayload.data = {}
              bodyPayload.data.live_transcript = effectiveLiveTranscript
              bodyPayload.data.show_transcript = effectiveLiveTranscript
            }
            var startResP = await fetch('https://api.pipecat.daily.co/v1/public/' + dailyAgentName + '/start', {
              method: 'POST',
              headers: { 'Authorization': 'Bearer ' + dailyApiKey, 'Content-Type': 'application/json' },
              body: JSON.stringify({ createDailyRoom: true, body: bodyPayload })
            })
            if (!startResP.ok) throw new Error('Daily start failed: ' + startResP.status)
            var startDataP = await startResP.json()
            var dailyRoom = startDataP.dailyRoom
            var dailyToken = startDataP.dailyToken
            if (!dailyRoom || !dailyToken) throw new Error('Missing dailyRoom or dailyToken')
            setVoiceUiStatus(t('voice_joining_room'))
            var dailyCallP = createDailyCallObject(Daily, { startVideoOff: true, videoSource: false })
            wireDailyEvents(dailyCallP)
            await dailyCallP.join({ url: dailyRoom, token: dailyToken, startVideoOff: true, videoSource: false })
            completeDailyJoin(dailyCallP)
          }
        } catch (err) {
          console.error('[AgentChatWidget] Daily voice error', err)
          setVoiceUiStatus((err && err.message) ? err.message : t('voice_connection_failed'))
          disconnectVoice()
        }
        return
      }

      if (isLocalWebrtc || voiceHttpBase) {
        const baseUrl = voiceHttpBase
        if (!baseUrl) {
          setVoiceUiStatus(t('voice_url_not_set'))
          markVoiceSessionDisconnected()
          if (!useRail || !railVoiceOverlayActive) voiceEndBtn.style.display = 'none'
          voiceMicIdle = true
          return
        }
        const startUrl = baseUrl + '/api/v1/agent-voice/agents/' + agentId + '/webrtc/start'
        const customerInfo = (pendingCustomerName || pendingCustomerPhone)
          ? { customer_name: pendingCustomerName || undefined, phone_number: pendingCustomerPhone || undefined }
          : {}
        try {
          setVoiceUiStatus(t('voice_establishing_link'))
          const startRes = await fetch(startUrl, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              embed_key: embedKey,
              host: window.location.hostname,
              ...customerInfo,
              ...(conversationLanguageCode && { language_code: conversationLanguageCode }),
              channel: webVoiceMeta.channel,
              is_web: webVoiceMeta.is_web,
              source: webVoiceMeta.source,
              source_name: webVoiceMeta.source_name
            })
          })
          if (!startRes.ok) throw new Error('Start failed: ' + startRes.status)
          const startData = await startRes.json()
          const iceConfig = (startData && startData.iceConfig) || {}
          const iceServers = iceConfig.iceServers || []
          const pcConfig = { iceServers: iceServers }
          if (iceConfig.iceTransportPolicy === 'relay' || iceConfig.iceTransportPolicy === 'all') {
            pcConfig.iceTransportPolicy = iceConfig.iceTransportPolicy
          }
          const pc = new RTCPeerConnection(pcConfig)

          function onVoiceClosed() {
            disconnectVoice()
            if (useRail && railVoiceOverlayActive) {
              endRailVoiceSession()
            } else {
              voicePanelEl.classList.remove('visible')
              showStartCta()
            }
          }
          pc.onconnectionstatechange = function () {
            if (pc.connectionState === 'closed' || pc.connectionState === 'failed' || pc.connectionState === 'disconnected') onVoiceClosed()
          }
          pc.oniceconnectionstatechange = function () {
            var ice = pc.iceConnectionState
            if (ice === 'closed' || ice === 'failed' || ice === 'disconnected') {
              onVoiceClosed()
              return
            }
            if (ice === 'checking' || ice === 'connecting') {
              setVoiceUiStatus(t('voice_connecting_agent'))
            } else if (ice === 'connected' || ice === 'completed') {
              setVoiceUiStatus(t('voice_connected_speak'))
              markVoiceSessionConnected()
            }
          }
          pc.ondatachannel = function (event) {
            if (event && event.channel) setupRailRtviDataChannel(event.channel)
          }
          var rtviChannel = pc.createDataChannel('rtvi-ai', { ordered: true })
          setupRailRtviDataChannel(rtviChannel)
          var pendingVoiceIce = []
          function postVoiceIce(payload) {
            fetch(baseUrl + '/api/v1/agent-voice/agents/' + agentId + '/webrtc/offer', {
              method: 'PATCH',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ pc_id: voiceServerPcId, candidates: [payload] })
            }).catch(function () {})
          }
          pc.onicecandidate = function (event) {
            if (!event.candidate) return
            var payload = {
              candidate: event.candidate.candidate,
              sdp_mid: event.candidate.sdpMid,
              sdp_mline_index: event.candidate.sdpMLineIndex
            }
            if (!voiceServerPcId) {
              pendingVoiceIce.push(payload)
              return
            }
            postVoiceIce(payload)
          }
          pc.ontrack = function (event) {
            const remoteStream = event.streams && event.streams[0]
            const remoteTrack = (event.track && event.track.kind === 'audio')
              ? event.track
              : (remoteStream && remoteStream.getAudioTracks()[0])
            if (remoteTrack) {
              attachRemoteAudioTrack(remoteTrack)
            } else if (remoteStream) {
              var el = ensureVoiceRemoteAudio()
              try { el.removeAttribute('src') } catch (_) {}
              el.srcObject = remoteStream
              el.muted = false
              el.volume = 1
              playVoiceRemoteAudio()
              if (!voiceAnalyserRemote) {
                attachVoiceAnalyserForStream(remoteStream, true)
                if (!voiceVizRaf) startVoiceVisualizerLoop()
              }
            }
          }

          voiceStream = await navigator.mediaDevices.getUserMedia({
            audio: { echoCancellation: true, noiseSuppression: true }
          })
          attachVoiceAnalyserForStream(voiceStream, false)
          if (!voiceVizRaf) startVoiceVisualizerLoop()
          setVoiceUiStatus(t('voice_negotiating'))
          voiceStream.getTracks().forEach(function (track) { pc.addTrack(track, voiceStream) })
          const offer = await pc.createOffer()
          await pc.setLocalDescription(offer)

          const offerRes = await fetch(baseUrl + '/api/v1/agent-voice/agents/' + agentId + '/webrtc/offer', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              session_id: startData.sessionId,
              sdp: offer.sdp,
              type: offer.type,
              embed_key: embedKey,
              host: window.location.hostname,
              request_data: {
                tenant: 'default',
                live_transcript: effectiveLiveTranscript,
                show_transcript: effectiveLiveTranscript,
                ...(pendingCustomerName && { customer_name: pendingCustomerName }),
                ...(pendingCustomerPhone && { phone_number: pendingCustomerPhone }),
                ...(conversationLanguageCode && { language_code: conversationLanguageCode }),
                channel: webVoiceMeta.channel,
                is_web: webVoiceMeta.is_web,
                source: webVoiceMeta.source,
                source_name: webVoiceMeta.source_name,
                ...acwPageContext()
              }
            })
          })
          if (!offerRes.ok) throw new Error('Offer failed: ' + offerRes.status)
          const offerData = await offerRes.json()
          voiceServerPcId = offerData.pc_id || null
          setVoiceUiStatus(t('voice_signing_in'))
          await pc.setRemoteDescription(new RTCSessionDescription({ type: offerData.type, sdp: offerData.sdp }))
          var queuedVoiceIce = pendingVoiceIce.splice(0, pendingVoiceIce.length)
          for (var iceIdx = 0; iceIdx < queuedVoiceIce.length; iceIdx++) postVoiceIce(queuedVoiceIce[iceIdx])
          voicePeerConnection = pc
          var iceNow = pc.iceConnectionState
          if (iceNow === 'connected' || iceNow === 'completed') {
            setVoiceUiStatus(t('voice_connected_speak'))
            markVoiceSessionConnected()
          } else if (iceNow === 'checking' || iceNow === 'connecting') {
            setVoiceUiStatus(t('voice_connecting_agent'))
          }
        } catch (err) {
          console.error('[AgentChatWidget] Voice WebRTC error', err)
          setVoiceUiStatus(t('voice_connection_failed'))
          disconnectVoice()
        }
        return
      }

      setTimeout(function () {
        setVoiceUiStatus(t('voice_coming_soon'))
        markVoiceSessionDisconnected()
        if (!useRail || !railVoiceOverlayActive) voiceEndBtn.style.display = 'none'
        voiceMicIdle = true
      }, 1500)
    }
    voiceMicBtn.onclick = startVoiceConnection
    if (railVoiceMicBtn) {
      railVoiceMicBtn.onclick = function () {
        if (!voiceMicIdle) {
          // Connected or connecting: end the call but keep the transcript + close button.
          disconnectVoice()
          endRailVoiceSession()
          return
        }
        // Idle: start a fresh session (drop any prior ended transcript).
        if (railVoiceOverlayEl && railVoiceOverlayEl.classList.contains('is-ended')) {
          railVoiceOverlayEl.classList.remove('is-ended')
          clearRailTranscript()
        }
        startVoiceConnection()
      }
    }
    if (railVoiceCloseBtn) {
      railVoiceCloseBtn.onclick = function () {
        disconnectVoice()
        hideRailVoiceOverlay()
        clearRailTranscript()
        setRailActive(null)
      }
    }
    if (railVoiceHomeBtn) {
      // Return to the rail so the visitor can pick any conversation option again.
      railVoiceHomeBtn.onclick = function () {
        disconnectVoice()
        hideRailVoiceOverlay()
        clearRailTranscript()
        setRailActive(null)
      }
    }
    voiceEndBtn.onclick = function () {
      endActiveVoiceSession()
    }
    if (railVoiceEndBtn) {
      railVoiceEndBtn.onclick = function () {
        endActiveVoiceSession()
      }
    }

    if (enableGuidedNavigation) {
      var savedSession = loadPersistedSession(agentId, sessionPersistTtlMs)
      if (savedSession && savedSession.messages && savedSession.messages.length) {
        restoringSession = true
        if (savedSession.sessionId) sessionId = savedSession.sessionId
        conversationStarted = true
        customerInfoSubmitted = true
        savedSession.messages.forEach(function (m) {
          restoreMessageFromLog(m)
        })
        for (var hi = 0; hi < messageLog.length; hi++) {
          if (messageLog[hi].type === 'handover_ack') {
            humanHandoverActive = true
            break
          }
        }
        restoringSession = false
        showChat()
        attachActionsToLastBotMessage()
        if (savedSession.navigationPending || savedSession.nextPagePath || savedSession.pagePath) {
          setTimeout(function () {
            showContinuityToast(t('navigation_continued').replace('{page}', formatPageLabel()))
          }, 700)
        }
        if (savedSession.panelOpen) {
          openPanelUI()
        }
        setConnectionState('reconnecting')
        connect()
      }
    }

    if (useInline) {
      closeBtn.style.display = 'none'
      disconnectChatBtn.style.display = 'none'
      openPanelUI()
      if (!connected && !connecting && !conversationStarted) {
        showStartCta()
      }
    }

    return {
      destroy() {
        teardownViewportFit()
        if (enableSpaNavigation) {
          if (spaClickHandlerRef) {
            document.removeEventListener('click', spaClickHandlerRef, true)
            spaClickHandlerRef = null
          }
          if (spaPopstateHandlerRef) {
            window.removeEventListener('popstate', spaPopstateHandlerRef)
            spaPopstateHandlerRef = null
          }
        }
        disconnect(false)
        if (voiceRemoteAudio && voiceRemoteAudio.parentNode) {
          voiceRemoteAudio.parentNode.removeChild(voiceRemoteAudio)
        }
        container.remove()
      },
      open() {
        openPanelUI()
        if (!useRail && !connected && !connecting && !conversationStarted) {
          showStartCta()
        } else if (!connected && !connecting && conversationStarted && messageLog.length > 0) {
          setConnectionState('reconnecting')
          connect()
        }
      },
      close() {
        softClosePanel()
      },
      getSessionId() { return sessionId },
      navigateTo(url, anchor) { performNavigation({ url: url, anchor: anchor }) },
      renderProductCarousel(introText, carousel) {
        showChat()
        return appendProductCarousel(introText, carousel)
      },
    }
  }

  function buildConfig(userConfig) {
    const scriptEl = document.currentScript || document.querySelector('script[data-agent-id]')
    const ds = (scriptEl && scriptEl.dataset) || {}
    const allowed = (ds.allowedDomains || ds.allowed_domains || '').split(',').map((d) => d.trim()).filter(Boolean)
    const captureCustomerInfo = userConfig.captureCustomerInfo !== undefined 
      ? userConfig.captureCustomerInfo 
      : ds.captureCustomerInfo === 'true' || ds.capture_customer_info === 'true'
    const allowSkipCustomerInfo = userConfig.allowSkipCustomerInfo !== undefined 
      ? userConfig.allowSkipCustomerInfo 
      : ds.allowSkipCustomerInfo !== 'false' && ds.allow_skip_customer_info !== 'false'
    const bannerImageUrlRaw =
      userConfig.bannerImageUrl ??
      userConfig.banner_image_url ??
      userConfig.bannerImage ??
      userConfig.banner_image ??
      ds.bannerImageUrl ??
      ds.banner_image_url ??
      ds.bannerImage ??
      ds.banner_image ??
      null
    const bannerImageUrl =
      typeof bannerImageUrlRaw === 'string' && bannerImageUrlRaw.trim() ? bannerImageUrlRaw.trim() : null
    const position = userConfig.position || ds.position || ds.launcherPosition || 'bottom-right'
    const validPositions = ['bottom-right', 'middle-right', 'top-right', 'bottom-left', 'middle-left', 'top-left']
    const validPosition = validPositions.includes(position) ? position : 'bottom-right'
    const showIcon = userConfig.showIcon !== undefined ? userConfig.showIcon : ds.showIcon !== 'false' && ds.show_icon !== 'false'
    const showFavicon = userConfig.showFavicon !== undefined
      ? userConfig.showFavicon !== false
      : userConfig.show_favicon !== undefined
        ? userConfig.show_favicon !== false
        : ds.showFavicon !== 'false' && ds.show_favicon !== 'false'
    const showLauncherText = userConfig.showLauncherText !== undefined
      ? userConfig.showLauncherText
      : userConfig.show_launcher_text !== undefined
        ? userConfig.show_launcher_text
        : ds.showLauncherText !== 'false' && ds.show_launcher_text !== 'false'
    const launcherTextRaw =
      userConfig.launcherText ?? userConfig.launcher_text ?? ds.launcherText ?? ds.launcher_text
    const launcherText =
      typeof launcherTextRaw === 'string' && launcherTextRaw.trim() ? launcherTextRaw.trim() : undefined
    const enableVoice = userConfig.enableVoice !== undefined ? userConfig.enableVoice : ds.enableVoice !== 'false' && ds.enable_voice !== 'false'
    const isLocalWebrtc = userConfig.isLocalWebrtc !== undefined ? !!userConfig.isLocalWebrtc : (ds.isLocalWebrtc !== 'false' && ds.is_local_webrtc !== 'false')
    const voiceServiceUrlResolved = userConfig.voiceServiceUrl || userConfig.voice_service_url || ds.voiceServiceUrl || ds.voice_service_url
    const explicitUseDailyVoice =
      userConfig.useDailyVoice !== undefined ||
      userConfig.use_daily_voice !== undefined ||
      ds.useDailyVoice !== undefined ||
      ds.use_daily_voice !== undefined
    // Hosted embeds use SmallWebRTC (/webrtc/start). Daily stays opt-in via useDailyVoice: true.
    const useDailyVoice = explicitUseDailyVoice
      ? (userConfig.useDailyVoice === true ||
        userConfig.use_daily_voice === true ||
        ds.useDailyVoice === 'true' ||
        ds.use_daily_voice === 'true')
      : false
    const dailyApiKey = userConfig.dailyApiKey || userConfig.daily_api_key || ds.dailyApiKey || ds.daily_api_key || ''
    const dailyAgentName = userConfig.dailyAgentName || userConfig.daily_agent_name || ds.dailyAgentName || ds.daily_agent_name || defaultDailyAgentName()
    const dailyScriptUrl = userConfig.dailyScriptUrl || userConfig.daily_script_url || ds.dailyScriptUrl || ds.daily_script_url || DEFAULT_DAILY_SCRIPT_URL
    const voiceTenantRaw = userConfig.voiceTenant ?? userConfig.voice_tenant ?? ds.voiceTenant ?? ds.voice_tenant ?? 'default'
    const voiceTenant = (typeof voiceTenantRaw === 'string' ? voiceTenantRaw : String(voiceTenantRaw || 'default')).trim() || 'default'
    const liveTranscript = userConfig.liveTranscript === true || userConfig.live_transcript === true || ds.liveTranscript === 'true' || ds.live_transcript === 'true'
    const rawVoiceSession = userConfig.voiceSessionData ?? userConfig.voice_session_data ?? ds.voiceSessionData ?? ds.voice_session_data
    let voiceSessionData = {}
    if (rawVoiceSession != null && rawVoiceSession !== '') {
      if (typeof rawVoiceSession === 'object' && !Array.isArray(rawVoiceSession)) {
        voiceSessionData = rawVoiceSession
      } else if (typeof rawVoiceSession === 'string') {
        try {
          var parsedSd = JSON.parse(rawVoiceSession)
          if (parsedSd && typeof parsedSd === 'object' && !Array.isArray(parsedSd)) voiceSessionData = parsedSd
        } catch (_) {}
      }
    }
    const enableWhatsApp = userConfig.enableWhatsApp !== undefined
      ? userConfig.enableWhatsApp
      : ds.enableWhatsApp !== 'false' && ds.enable_whatsapp !== 'false'
    const whatsappPhone = userConfig.whatsappPhone || userConfig.whatsapp_phone || ds.whatsappPhone || ds.whatsapp_phone || ''
    const whatsappMessage = userConfig.whatsappMessage || userConfig.whatsapp_message || ds.whatsappMessage || ds.whatsapp_message || 'Hi! I need assistance.'
    const language = normalizeWidgetUiLanguage(
      userConfig.language ?? userConfig.lang ?? ds.language ?? ds.lang ?? 'en'
    )
    const enableGuidedNavigation = userConfig.enableGuidedNavigation !== undefined
      ? userConfig.enableGuidedNavigation !== false
      : ds.enableGuidedNavigation !== 'false' && ds.enable_guided_navigation !== 'false'
    const sessionPersistTtlMsRaw = userConfig.sessionPersistTtlMs ?? userConfig.session_persist_ttl_ms ?? ds.sessionPersistTtlMs ?? ds.session_persist_ttl_ms
    const sessionPersistTtlMs = Number(sessionPersistTtlMsRaw) > 0 ? Number(sessionPersistTtlMsRaw) : 1800000
    const enableSpaNavigation = userConfig.enableSpaNavigation === true ||
      userConfig.enable_spa_navigation === true ||
      ds.enableSpaNavigation === 'true' ||
      ds.enable_spa_navigation === 'true'
    const spaContentSelectorRaw = userConfig.spaContentSelector ?? userConfig.spa_content_selector ?? ds.spaContentSelector ?? ds.spa_content_selector
    const spaContentSelector = typeof spaContentSelectorRaw === 'string' && spaContentSelectorRaw.trim()
      ? spaContentSelectorRaw.trim()
      : 'main'
    const spaSyncRaw = userConfig.spaSyncSelectors ?? userConfig.spa_sync_selectors ?? ds.spaSyncSelectors ?? ds.spa_sync_selectors
    let spaSyncSelectors = []
    if (Array.isArray(spaSyncRaw)) {
      spaSyncSelectors = spaSyncRaw.filter(function (s) { return typeof s === 'string' && s.trim() }).map(function (s) { return s.trim() })
    } else if (typeof spaSyncRaw === 'string' && spaSyncRaw.trim()) {
      spaSyncSelectors = spaSyncRaw.split(',').map(function (s) { return s.trim() }).filter(Boolean)
    }
    const spaInterceptLinksRaw = userConfig.spaInterceptLinks ?? userConfig.spa_intercept_links ?? ds.spaInterceptLinks ?? ds.spa_intercept_links
    const spaInterceptLinks = typeof spaInterceptLinksRaw === 'string' && spaInterceptLinksRaw.trim()
      ? spaInterceptLinksRaw.trim()
      : null
    const spaUseFrameworkRouter = userConfig.spaUseFrameworkRouter !== false &&
      userConfig.spa_use_framework_router !== false &&
      ds.spaUseFrameworkRouter !== 'false' &&
      ds.spa_use_framework_router !== 'false'
    const spaUseNextRouter = userConfig.spaUseNextRouter !== false &&
      userConfig.spa_use_next_router !== false &&
      ds.spaUseNextRouter !== 'false' &&
      ds.spa_use_next_router !== 'false'
    const spaNavigationAdapterRaw = userConfig.spaNavigationAdapter ?? userConfig.spa_navigation_adapter ?? ds.spaNavigationAdapter ?? ds.spa_navigation_adapter ?? 'auto'
    const spaNavigationAdapter = typeof spaNavigationAdapterRaw === 'string' ? spaNavigationAdapterRaw : 'auto'
    const spaNavigate = typeof userConfig.spaNavigate === 'function'
      ? userConfig.spaNavigate
      : typeof userConfig.spa_navigate === 'function'
        ? userConfig.spa_navigate
        : null
    const spaNextRouterShallow = userConfig.spaNextRouterShallow !== false &&
      userConfig.spa_next_router_shallow !== false &&
      ds.spaNextRouterShallow !== 'false' &&
      ds.spa_next_router_shallow !== 'false'
    const viewTypeRaw = userConfig.viewType ?? userConfig.view_type ?? ds.viewType ?? ds.view_type ?? 'hub'
    const viewType = viewTypeRaw === 'rail' ? 'rail' : 'hub'
    const primaryChannelRaw = userConfig.primaryChannel ?? userConfig.primary_channel ?? ds.primaryChannel ?? ds.primary_channel ?? 'chat'
    const primaryChannel = primaryChannelRaw === 'voice' ? 'voice' : 'chat'
    const railOrderRaw = userConfig.railOrder ?? userConfig.rail_order ?? ds.railOrder ?? ds.rail_order
    let railOrder = null
    if (Array.isArray(railOrderRaw)) {
      railOrder = railOrderRaw
    } else if (typeof railOrderRaw === 'string' && railOrderRaw.trim()) {
      railOrder = railOrderRaw.split(',').map(function (s) { return s.trim() }).filter(Boolean)
    }
    const showRailTooltips = userConfig.showRailTooltips !== undefined
      ? userConfig.showRailTooltips !== false
      : ds.showRailTooltips !== 'false' && ds.show_rail_tooltips !== 'false'
    const embedModeRaw = userConfig.embedMode ?? userConfig.embed_mode ?? ds.embedMode ?? ds.embed_mode ?? 'floating'
    const embedMode = embedModeRaw === 'inline' ? 'inline' : 'floating'
    const suggestedQuestionsRaw = userConfig.suggestedQuestions ?? userConfig.suggested_questions ?? ds.suggestedQuestions ?? ds.suggested_questions
    const themeModeRaw = userConfig.themeMode ?? userConfig.theme_mode ?? ds.themeMode ?? ds.theme_mode ?? 'light'
    const themeMode = themeModeRaw === 'dark' || themeModeRaw === 'auto' ? themeModeRaw : 'light'
    const viewportTopOffsetRaw =
      userConfig.viewportTopOffset ?? userConfig.viewport_top_offset ?? ds.viewportTopOffset ?? ds.viewport_top_offset
    const viewportTopOffsetParsed = parseInt(viewportTopOffsetRaw, 10)
    const viewportTopOffset =
      String(viewportTopOffsetRaw).trim() === 'auto'
        ? 'auto'
        : isNaN(viewportTopOffsetParsed)
          ? null
          : viewportTopOffsetParsed
    let suggestedQuestions = []
    if (Array.isArray(suggestedQuestionsRaw)) {
      suggestedQuestions = suggestedQuestionsRaw
    } else if (typeof suggestedQuestionsRaw === 'string' && suggestedQuestionsRaw.trim()) {
      try {
        var parsedQuestions = JSON.parse(suggestedQuestionsRaw)
        if (Array.isArray(parsedQuestions)) suggestedQuestions = parsedQuestions
      } catch (_) {}
    }
    return {
      agentId: userConfig.agentId || ds.agentId,
      embedKey: userConfig.embedKey || ds.embedKey || ds.embed_key,
      primaryColor: userConfig.primaryColor || ds.primaryColor || ds.primary_color,
      backgroundColor: userConfig.backgroundColor || ds.backgroundColor || ds.background_color,
      headerColor: userConfig.headerColor || userConfig.header_color || ds.headerColor || ds.header_color,
      allowedDomains: userConfig.allowedDomains || allowed,
      apiBaseUrl: userConfig.apiBaseUrl || ds.apiBaseUrl || ds.api_base_url,
      voiceServiceUrl: voiceServiceUrlResolved,
      wsUrl: userConfig.wsUrl || ds.wsUrl || ds.ws_url,
      title: userConfig.title || ds.title,
      greeting: userConfig.greeting || ds.greeting,
      captureCustomerInfo: captureCustomerInfo,
      allowSkipCustomerInfo: allowSkipCustomerInfo,
      bannerImageUrl: bannerImageUrl,
      position: validPosition,
      iconUrl: userConfig.iconUrl || userConfig.icon_url || ds.iconUrl || ds.icon_url,
      headerIconUrl:
        userConfig.headerIconUrl !== undefined
          ? userConfig.headerIconUrl
          : userConfig.header_icon_url !== undefined
            ? userConfig.header_icon_url
            : ds.headerIcon !== undefined
              ? ds.headerIcon
              : ds.header_icon !== undefined
                ? ds.header_icon
                : undefined,
      showIcon: showIcon,
      showFavicon: showFavicon,
      showLauncherText: showLauncherText,
      launcherText: launcherText,
      enableVoice: enableVoice,
      isLocalWebrtc: isLocalWebrtc,
      useDailyVoice: useDailyVoice,
      dailyApiKey: dailyApiKey,
      dailyAgentName: dailyAgentName,
      dailyScriptUrl: dailyScriptUrl,
      voiceTenant: voiceTenant,
      liveTranscript: liveTranscript,
      voiceSessionData: voiceSessionData,
      enableWhatsApp: enableWhatsApp,
      whatsappPhone: whatsappPhone,
      whatsappMessage: whatsappMessage,
      language: language,
      enableGuidedNavigation: enableGuidedNavigation,
      sessionPersistTtlMs: sessionPersistTtlMs,
      enableSpaNavigation: enableSpaNavigation,
      spaContentSelector: spaContentSelector,
      spaSyncSelectors: spaSyncSelectors,
      spaInterceptLinks: spaInterceptLinks,
      spaUseFrameworkRouter: spaUseFrameworkRouter,
      spaUseNextRouter: spaUseNextRouter,
      spaNavigationAdapter: spaNavigationAdapter,
      spaNavigate: spaNavigate,
      spaNextRouterShallow: spaNextRouterShallow,
      viewType: viewType,
      primaryChannel: primaryChannel,
      railOrder: railOrder,
      showRailTooltips: showRailTooltips,
      embedMode: embedMode,
      suggestedQuestions: suggestedQuestions,
      themeMode: themeMode,
      viewportTopOffset: viewportTopOffset,
    }
  }

  let instance = null

  function init(userConfig = {}) {
    if (instance) {
      destroy()
    }
    const cfg = buildConfig(userConfig)
    if (userConfig.embedMode === 'inline' || userConfig.embed_mode === 'inline') {
      cfg.embedMode = 'inline'
    }
    if (userConfig.mountElement) {
      cfg.mountElement = userConfig.mountElement
    }
    if (userConfig.mountSelector) {
      cfg.mountSelector = userConfig.mountSelector
    }
    instance = createWidget(cfg)
    return instance
  }

  function destroy() {
    if (instance && typeof instance.destroy === 'function') {
      instance.destroy()
      instance = null
    }
  }

  window.AgentChatWidget = { init, destroy, version: VERSION }

  // Auto-init only when the embed script includes data-agent-id (manual init() otherwise)
  const autoScript = document.currentScript || document.querySelector('script[data-agent-id]')
  if (
    autoScript &&
    autoScript.dataset &&
    autoScript.dataset.autoInit !== 'false' &&
    (autoScript.dataset.agentId || autoScript.getAttribute('data-agent-id'))
  ) {
    init()
  }
})()