PageSourceSearch

https://www.styleguru.io/apps/style-guru/widget.js

js styleguru.io collected 2026-09-28 06:26:09 UTC 313,539 bytes, 6,378 lines download raw bytes

1(function() {
2  "use strict";
3
4  // Prevent double-initialisation if both the liquid tag and bootstrap load this file
5  if (window.__sgLoaded) return;
6  window.__sgLoaded = true;
7
8  var WIDGET_VERSION = "8.44.2";
9  var WIDGET_ID = "style-guru-widget";
10  var API_BASE = "";
11  var PROXY_PATH = "";
12  var shopDomain = "";
13  var config = {
14    primaryColor: "#C6A85B",
15    fontColor: "#333333",
16    fontSize: "14",
17    backgroundColor: "#ffffff",
18    borderRadius: "24",
19    borderColor: "#e0e0e0",
20    borderWidth: "1",
21    widgetHeight: "400",
22    widgetWidth: "100",
23    widgetPadding: "10",
24    boxShadow: "none",
25    stylistName: "Style Guru",
26    welcomeMessage: "Good evening. What's the occasion?",
27    interactionMode: "text_only",
28    activationMode: "both",
29    topButtonLabel: null,
30    topButtonColor: null,
31    bellPosition: "bottom_left",
32    _version: "v8.3.0"
33  };
34  var messages = [];
35  var isLoading = false;
36  var isModalOpen = false;
37  var shownProductIds = [];
38  var isListening = false;
39  var isSpeaking = false;
40  var currentAudio = null;
41  var recognition = null;
42  var sessionId = "";
43  var detectedCurrency = "";
44  var autoTriggeredThisSession = false;
45  // Scope bridges: implemented inside the widget bind closure (they capture
46  // closure state), but called from top-level functions like sendMessage and
47  // renderProducts. No-ops until the widget has been opened and bound.
48  var updateScrollFade = function() {};
49  var detectAndApplyProactiveFilter = function() {};
50  // Complete-look zone renderers live in the bind closure but are called from
51  // the top-level response handlers — bridge them like the vars above.
52  var hideCompleteLook = function() {};
53  var showCompleteLook = function() {};
54  // AI grid filters — bound when the filter pane initialises; the streaming
55  // reply handler lives outside that closure and calls it through this bridge.
56  var applyAIFilters = function() {};
57  var devCustomerId = "";
58  // True when the client-side proactive filter updated the product grid for
59  // the in-flight message — the mobile chip must fire for this path too.
60  var proactiveFilterApplied = false;
61  // Set when an AI grid command is applied: the next grid load echoes its top
62  // results as slim thumbnails inside the assistant's chat reply.
63  var pendingChatPreview = false;
64  // Last user text sent to the API — the "Try again" chip re-sends it after
65  // an error without making the customer retype.
66  var lastSentText = "";
67  var feedbackVotes = {}; // message index -> "up"/"down" (survives re-renders)
68
69  // ── Locale & chrome translations ─────────────────────────────────────────
70  // The AI replies in the shopper's language on its own; this table localises
71  // the widget CHROME (tabs, buttons, placeholders). Product names, sizes and
72  // prices are never translated.
73  var sgLocale = "en";
74  try {
75    sgLocale = String((window.Shopify && window.Shopify.locale) || document.documentElement.lang || "en").toLowerCase().split("-")[0];
76  } catch (e) {}
77  var I18N = {
78    en: { storeReply:"from the store team", teaserSizing:"Not sure about sizing? Ask me", chat:"Chat", outfits:"Outfits", browse:"Browse", results:"Results", searchProducts:"Search products...", category:"Category", colour:"Colour", size:"Size", price:"Price", addToBag:"Add to Bag", add:"Add", added:"Added", askPlaceholder:"Ask about outfits, sizes, styles...", micPlaceholder:"Type or use the mic...", starterOutfit:"Build me an outfit", starterSize:"What's my size?", starterOrder:"Where's my order?", welcomeBack:"Welcome back — what else can I help you with?", viewOutfit:"View the outfit", viewProducts:"View the products", tryAgain:"Try again", thanksFeedback:"Thanks for the feedback", helpful:"Helpful", notRight:"Not right", noProducts:"No products found", adjustFilters:"Try adjusting your filters.", noRecs:"No recommendations yet", noRecsSub:"Ask me about outfits, sizes, or styles and I'll show you matching products here.", leadGateTitle:"Your look is ready \u2014 who shall I show it to?", leadGateBody:"Leave your name and email and I\u2019ll reveal it, plus keep your looks and sizes saved for next time.", leadGateName:"Your name", leadGateEmail:"Your email address", leadGateCta:"Show my look" },
79    fr: { storeReply:"de l'équipe de la boutique", teaserSizing:"Un doute sur la taille ? Demandez-moi", chat:"Discussion", outfits:"Tenues", browse:"Parcourir", results:"Résultats", searchProducts:"Rechercher des produits...", category:"Catégorie", colour:"Couleur", size:"Taille", price:"Prix", addToBag:"Ajouter au panier", add:"Ajouter", added:"Ajouté", askPlaceholder:"Tenues, tailles, styles — demandez-moi...", micPlaceholder:"Écrivez ou utilisez le micro...", starterOutfit:"Composez-moi une tenue", starterSize:"Quelle est ma taille ?", starterOrder:"Où est ma commande ?", welcomeBack:"Ravi de vous revoir — que puis-je faire pour vous ?", viewOutfit:"Voir la tenue", viewProducts:"Voir les produits", tryAgain:"Réessayer", thanksFeedback:"Merci pour votre retour", helpful:"Utile", notRight:"Inexact", noProducts:"Aucun produit trouvé", adjustFilters:"Essayez d'ajuster vos filtres.", noRecs:"Pas encore de recommandations", noRecsSub:"Demandez-moi des tenues, des tailles ou des styles et je vous montrerai des produits ici.", leadGateTitle:"Votre tenue est pr\u00eate \u2014 \u00e0 qui dois-je la montrer ?", leadGateBody:"Laissez votre nom et votre e-mail et je vous la r\u00e9v\u00e8le \u2014 vos tenues et tailles seront gard\u00e9es pour la prochaine fois.", leadGateName:"Votre nom", leadGateEmail:"Votre adresse e-mail", leadGateCta:"Voir ma tenue" },
80    de: { storeReply:"vom Team des Geschäfts", teaserSizing:"Unsicher bei der Größe? Fragen Sie mich", chat:"Chat", outfits:"Outfits", browse:"Stöbern", results:"Ergebnisse", searchProducts:"Produkte suchen...", category:"Kategorie", colour:"Farbe", size:"Größe", price:"Preis", addToBag:"In den Warenkorb", add:"Hinzufügen", added:"Hinzugefügt", askPlaceholder:"Fragen Sie nach Outfits, Größen, Stilen...", micPlaceholder:"Tippen oder Mikrofon nutzen...", starterOutfit:"Stell mir ein Outfit zusammen", starterSize:"Welche Größe habe ich?", starterOrder:"Wo ist meine Bestellung?", welcomeBack:"Schön, Sie wiederzusehen — womit kann ich noch helfen?", viewOutfit:"Outfit ansehen", viewProducts:"Produkte ansehen", tryAgain:"Erneut versuchen", thanksFeedback:"Danke für Ihr Feedback", helpful:"Hilfreich", notRight:"Nicht richtig", noProducts:"Keine Produkte gefunden", adjustFilters:"Passen Sie Ihre Filter an.", noRecs:"Noch keine Empfehlungen", noRecsSub:"Fragen Sie mich nach Outfits, Größen oder Stilen — passende Produkte erscheinen hier.", leadGateTitle:"Ihr Outfit ist fertig \u2014 wem darf ich es zeigen?", leadGateBody:"Hinterlassen Sie Name und E-Mail und ich zeige es Ihnen \u2014 Outfits und Gr\u00f6\u00dfen bleiben f\u00fcrs n\u00e4chste Mal gespeichert.", leadGateName:"Ihr Name", leadGateEmail:"Ihre E-Mail-Adresse", leadGateCta:"Outfit anzeigen" },
81    es: { storeReply:"del equipo de la tienda", teaserSizing:"¿Dudas con la talla? Pregúntame", chat:"Chat", outfits:"Conjuntos", browse:"Explorar", results:"Resultados", searchProducts:"Buscar productos...", category:"Categoría", colour:"Color", size:"Talla", price:"Precio", addToBag:"Añadir a la cesta", add:"Añadir", added:"Añ
81adido", askPlaceholder:"Pregunta por conjuntos, tallas, estilos...", micPlaceholder:"Escribe o usa el micrófono...", starterOutfit:"Créame un conjunto", starterSize:"¿Cuál es mi talla?", starterOrder:"¿Dónde está mi pedido?", welcomeBack:"Qué bueno verte de nuevo — ¿en qué más puedo ayudarte?", viewOutfit:"Ver el conjunto", viewProducts:"Ver los productos", tryAgain:"Reintentar", thanksFeedback:"Gracias por tu opinión", helpful:"Útil", notRight:"Incorrecto", noProducts:"No se encontraron productos", adjustFilters:"Prueba a ajustar los filtros.", noRecs:"Aún no hay recomendaciones", noRecsSub:"Pregúntame por conjuntos, tallas o estilos y te mostraré productos aquí.", leadGateTitle:"Tu conjunto est\u00e1 listo \u2014 \u00bfa qui\u00e9n se lo muestro?", leadGateBody:"Deja tu nombre y tu correo y te lo revelo \u2014 guardamos tus conjuntos y tallas para la pr\u00f3xima vez.", leadGateName:"Tu nombre", leadGateEmail:"Tu correo electr\u00f3nico", leadGateCta:"Ver mi conjunto" },
82    it: { storeReply:"dal team del negozio", teaserSizing:"Dubbi sulla taglia? Chiedimi", chat:"Chat", outfits:"Outfit", browse:"Sfoglia", results:"Risultati", searchProducts:"Cerca prodotti...", category:"Categoria", colour:"Colore", size:"Taglia", price:"Prezzo", addToBag:"Aggiungi al carrello", add:"Aggiungi", added:"Aggiunto", askPlaceholder:"Chiedimi outfit, taglie, stili...", micPlaceholder:"Scrivi o usa il microfono...", starterOutfit:"Creami un outfit", starterSize:"Qual è la mia taglia?", starterOrder:"Dov'è il mio ordine?", welcomeBack:"Bentornato — come posso aiutarla ancora?", viewOutfit:"Vedi l'outfit", viewProducts:"Vedi i prodotti", tryAgain:"Riprova", thanksFeedback:"Grazie per il feedback", helpful:"Utile", notRight:"Non corretto", noProducts:"Nessun prodotto trovato", adjustFilters:"Prova a modificare i filtri.", noRecs:"Ancora nessun consiglio", noRecsSub:"Chiedimi outfit, taglie o stili e ti mostrerò i prodotti qui.", leadGateTitle:"Il tuo look \u00e8 pronto \u2014 a chi lo mostro?", leadGateBody:"Lascia nome ed email e te lo rivelo \u2014 look e taglie restano salvati per la prossima volta.", leadGateName:"Il tuo nome", leadGateEmail:"La tua email", leadGateCta:"Mostra il mio look" },
83    nl: { storeReply:"van het winkelteam", teaserSizing:"Twijfel over je maat? Vraag het mij", chat:"Chat", outfits:"Outfits", browse:"Bladeren", results:"Resultaten", searchProducts:"Producten zoeken...", category:"Categorie", colour:"Kleur", size:"Maat", price:"Prijs", addToBag:"In winkelwagen", add:"Toevoegen", added:"Toegevoegd", askPlaceholder:"Vraag naar outfits, maten, stijlen...", micPlaceholder:"Typ of gebruik de microfoon...", starterOutfit:"Stel een outfit voor me samen", starterSize:"Welke maat heb ik?", starterOrder:"Waar is mijn bestelling?", welcomeBack:"Welkom terug — waarmee kan ik u nog meer helpen?", viewOutfit:"Bekijk de outfit", viewProducts:"Bekijk de producten", tryAgain:"Opnieuw", thanksFeedback:"Bedankt voor je feedback", helpful:"Nuttig", notRight:"Niet juist", noProducts:"Geen producten gevonden", adjustFilters:"Pas je filters aan.", noRecs:"Nog geen aanbevelingen", noRecsSub:"Vraag me naar outfits, maten of stijlen en ik toon hier passende producten.", leadGateTitle:"Je look is klaar \u2014 aan wie zal ik hem laten zien?", leadGateBody:"Laat je naam en e-mail achter en ik onthul hem \u2014 je looks en maten blijven bewaard voor de volgende keer.", leadGateName:"Je naam", leadGateEmail:"Je e-mailadres", leadGateCta:"Toon mijn look" }
84  };
85  function T(k) { var d = I18N[sgLocale]; return (d && d[k]) || I18N.en[k] || k; }
86
87  // Non-fashion stores (jewellery, health & supplements, general retail):
88  // the fashion wording above would misfire ("Build me an outfit" in a
89  // vitamin shop), so these keys overlay I18N when storeVertical says so.
90  var GENERIC_I18N = {
91    en: { teaserSizing:"Questions? I know every product here", outfits:"Picks", askPlaceholder:"Ask about products, gifts, prices...", starterOutfit:"What do you recommend?", starterSize:"Help me choose a gift", viewOutfit:"View the picks", noRecsSub:"Ask me about products, gifts or prices and I'll show you matching products here." },
92    fr: { teaserSizing:"Des questions ? Je connais tous nos produits", outfits:"S\u00e9lection", askPlaceholder:"Produits, id\u00e9es cadeaux, prix — demandez-moi...", starterOutfit:"Que me conseillez-vous ?", starterSize:"Aidez-moi \u00e0 choisir un cadeau", viewOutfit:"Voir la s\u00e9lection", noRecsSub:"Demandez-moi des produits, des id\u00e9es cadeaux ou des prix et je vous montrerai des produits ici." },
93    de: { teaserSizing:"Fragen? Ich kenne jedes Produkt hier", outfits:"Auswahl", askPlaceholder:"Fragen Sie nach Produkten, Geschenken, Preisen...", starterOutfit:"Was empfehlen Sie mir?", starterSize:"Helfen Sie mir, ein Geschenk zu finden", viewOutfit:"Auswahl ansehen", noRecsSub:"Fragen Sie mich nach Produkten, Geschenken oder Preisen — passende Produkte erscheinen hier." },
94    es: { teaserSizing:"\u00bfPreguntas? Conozco todos los productos", outfits:"Selecci\u00f3n", askPlaceholder:"Pregunta por productos, regalos, precios...", starterOutfit:"\u00bfQu\u00e9 me recomiendas?", starterSize:"Ay\u00fadame a elegir un regalo", viewOutfit:"Ver la selecci\u00f3n", noRecsSub:"Preg\u00fantame por productos, regalos o precios y te mostrar\u00e9 productos aqu\u00ed." },
95    it: { teaserSizing:"Domande? Conosco ogni prodotto qui", outfits:"Selezione", askPlaceholder:"Chiedimi prodotti, regali, prezzi...", starterOutfit:"Cosa mi consigli?", starterSize:"Aiutami a scegliere un regalo", viewOutfit:"Vedi la selezione", noRecsSub:"Chiedimi prodotti, regali o prezzi e ti mostrer\u00f2 i prodotti qui." },
96    nl: { teaserSizing:"Vragen? Ik ken elk product hier", outfits:"Selectie", askPlaceholder:"Vraag naar producten, c
96adeaus, prijzen...", starterOutfit:"Wat raad je aan?", starterSize:"Help me een cadeau kiezen", viewOutfit:"Bekijk de selectie", noRecsSub:"Vraag me naar producten, cadeaus of prijzen en ik toon hier passende producten." }
97  };
98  function applyGenericStrings() {
99    for (var loc in GENERIC_I18N) {
100      if (!I18N[loc]) continue;
101      var o = GENERIC_I18N[loc];
102      for (var k in o) I18N[loc][k] = o[k];
103    }
104  }
105
106
107  function detectStoreCurrency() {
108    try {
109      if (window.Shopify && window.Shopify.currency && window.Shopify.currency.active) {
110        return window.Shopify.currency.active;
111      }
112      if (window.ShopifyAnalytics && window.ShopifyAnalytics.meta && window.ShopifyAnalytics.meta.currency) {
113        return window.ShopifyAnalytics.meta.currency;
114      }
115      var metaTag = document.querySelector('meta[property="og:price:currency"]');
116      if (metaTag && metaTag.getAttribute("content")) {
117        return metaTag.getAttribute("content");
118      }
119      var moneyEl = document.querySelector('[data-currency]');
120      if (moneyEl && moneyEl.getAttribute("data-currency")) {
121        return moneyEl.getAttribute("data-currency");
122      }
123      var priceEl = document.querySelector('.money[data-currency-code]');
124      if (priceEl && priceEl.getAttribute("data-currency-code")) {
125        return priceEl.getAttribute("data-currency-code");
126      }
127    } catch(e) {}
128    return "";
129  }
130
131  function reDetectCurrency() {
132    var fresh = detectStoreCurrency();
133    if (fresh && fresh.length === 3) {
134      detectedCurrency = fresh;
135    }
136  }
137
138  function generateSessionId() {
139    var chars = "abcdefghijklmnopqrstuvwxyz0123456789";
140    var id = "";
141    for (var i = 0; i < 32; i++) {
142      id += chars.charAt(Math.floor(Math.random() * chars.length));
143    }
144    return id;
145  }
146
147  var SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
148
149  function getSessionId() {
150    var idKey = "sg_session_" + shopDomain;
151    var tsKey = "sg_session_ts_" + shopDomain;
152    try {
153      var stored = localStorage.getItem(idKey);
154      var storedTs = parseInt(localStorage.getItem(tsKey) || "0", 10);
155      var age = Date.now() - storedTs;
156      // Reuse session if it exists and is less than 24 hours old
157      if (stored && stored.length >= 8 && storedTs && age < SESSION_TTL_MS) {
158        return stored;
159      }
160      // Expired or missing — clear old data and start fresh
161      if (stored) {
162        localStorage.removeItem(idKey);
163        localStorage.removeItem(tsKey);
164      }
165    } catch(e) {}
166    var newId = generateSessionId();
167    try {
168      localStorage.setItem(idKey, newId);
169      localStorage.setItem(tsKey, String(Date.now()));
170    } catch(e) {}
171    return newId;
172  }
173
174  function refreshSessionTimestamp() {
175    try {
176      localStorage.setItem("sg_session_ts_" + shopDomain, String(Date.now()));
177    } catch(e) {}
178  }
179
180  function parseUrlParams(url) {
181    var params = {};
182    var queryString = url.split("?")[1];
183    if (!queryString) return params;
184    var pairs = queryString.split("&");
185    for (var i = 0; i < pairs.length; i++) {
186      var pair = pairs[i].split("=");
187      params[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || "");
188    }
189    return params;
190  }
191
192  function hexToRgb(hex) {
193    hex = hex.replace("#", "");
194    if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
195    var r = parseInt(hex.substring(0,2), 16);
196    var g = parseInt(hex.substring(2,4), 16);
197    var b = parseInt(hex.substring(4,6), 16);
198    return r + ", " + g + ", " + b;
199  }
200
201  // Lighten (positive pct) or darken (negative) a hex colour — used to build
202  // the bell's gradient from the merchant's chosen colour.
203  function shadeHex(hex, pct) {
204    hex = hex.replace("#", "");
205    if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
206    var out = "#";
207    for (var i = 0; i < 3; i++) {
208      var v = parseInt(hex.substring(i*2, i*2+2), 16);
209      v = Math.round(pct > 0 ? v + (255 - v) * (pct/100) : v * (1 + pct/100));
210      out += ("0" + Math.max(0, Math.min(255, v)).toString(16)).slice(-2);
211    }
212    return out;
213  }
214
215  function luminance(hex) {
216    hex = hex.replace("#", "");
217    if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
218    var r = parseInt(hex.substring(0,2), 16) / 255;
219    var g = parseInt(hex.substring(2,4), 16) / 255;
220    var b = parseInt(hex.substring(4,6), 16) / 255;
221    return 0.299 * r + 0.587 * g + 0.114 * b;
222  }
223
224  function isLightColor(hex) {
225    return luminance(hex) > 0.6;
226  }
227
228  // Form surfaces (email capture, message form) follow the brand colour: a
229  // faint tint of primaryColor rather than the old hardcoded cream, so a
230  // black/grey brand gets grey forms while brass keeps its warm look.
231  function sgFormTint() {
232    var rgbStr = hexToRgb(config.primaryColor || "#C6A85B");
233    return "background:linear-gradient(135deg, rgba(" + rgbStr + ",0.08), rgba(" + rgbStr + ",0.03));border:1px solid rgba(" + rgbStr + ",0.25);";
234  }
235
236  function apiUrl(path) {
237    if (PROXY_PATH) {
238      return PROXY_PATH + path;
239    }
240    return API_BASE + "/api/widget" + path;
241  }
242
243  function audioApiUrl(path) {
244    if (PROXY_PATH) {
245      return PROXY_PATH + path;
246    }
247    return API_BASE + path;
248  }
249
250  // Once anything is added via the stylist, stamp the cart with the session id
251  // (hidden "_sg_session" attribute). Shopify carries cart attributes onto the
252  // order, where the orders/create webhook turns them into attributed revenue.
253  var cartAttributed = false;
254  function markCartAttributed() {
255    if (cartAttributed) return;
256    if (!sessionId || sessionId.length < 8) return;
257    cartAttributed = true;
258    try {
259      var xhr = new XMLHttpRequest();
260      xhr.open("POST", "/cart/update.js");
261      xhr.setRequestHeader("Content-Type", "application/json");
262      xhr.send(JSON.stringify({ attributes: { _sg_session: sessionId } }));
263    } catch (e) { cartAttributed = false; }
264  }
265
266  // ── Live merchant replies ──────────────────────────────────────────────
267  // After a handoff, poll for the store's reply and drop it into the chat as
268  // a distinct "store team" bubble. The email copy is the guaranteed channel;
269  // this is the delight when the shopper is still browsing.
270  var replyPollTimer = null;
271  function startReplyPolling() {
272    try { sessionStorage.setItem("sg-await-reply", "1"); } catch (e) {}
273    if (replyPollTimer) return;
274    replyPollTimer = setInterval(pollForReplies, 15000);
275    setTimeout(pollForReplies, 4000);
276  }
277  function stopReplyPolling() {
278    if (replyPollTimer) { clearInterval(replyPollTimer); replyPollTimer = null; }
279    try { sessionStorage.removeItem("sg-await-reply"); } catch (e) {}
280  }
281  function pollForReplies() {
282    if (!sessionId || sessionId.length < 8) return;
283    try {
284      var xhr = new XMLHttpRequest();
285      xhr.open("GET", apiUrl("/replies?shop=" + encodeURIComponent(shopDomain) + "&sessionId=" + encodeURIComponent(sessionId)));
286      xhr.onreadystatechange = function () {
287        if (xhr.readyState !== 4 || xhr.status !== 200) return;
288        var data; try { data = JSON.parse(xhr.responseText); } catch (e) { return; }
289        var replies = (data && data.replies) || [];
290        if (!replies.length) return;
291        stopReplyPolling();
292        var container = document.getElementById("sg-messages");
293        for (var i = 0; i < replies.length; i++) {
294          messages.push({ role: "assistant", content: replies[i].message, storeReply: true });
295        }
296        if (container) { renderMessages(); }
297        try { if (!isModalOpen) { var fabEl = document.getElementById("sg-fab"); if (fabEl) { var badge = fabEl.querySelector(".sg-fab-badge"); if (badge) badge.style.display = "block"; } } } catch (e) {}
298      };
299      xhr.send();
300    } catch (e) {}
301  }
302
303  function trackEvent(type, payload) {
304    try {
305      var url = apiUrl("/events");
306      var body = { type: type };
307      if (sessionId && sessionId.length >= 8) body.anonId = sessionId;
308      if (payload) body.payload = payload;
309      var xhr = new XMLHttpRequest();
310      xhr.open("POST", url);
311      xhr.setRequestHeader("Content-Type", "application/json");
312      xhr.send(JSON.stringify(body));
313    } catch(e) {}
314  }
315
316  function init() {
317    var scriptTag = null;
318
319    // Highest priority: a script tag loaded from our server (set by bootstrap v2.0)
320    var ourScripts = document.querySelectorAll("script[src*='style-guru.replit.app']");
321    if (ourScripts.length > 0) {
322      scriptTag = ourScripts[ourScripts.length - 1];
323    }
324
325    // Fallback: explicit ID tag or current script
326    if (!scriptTag) {
327      scriptTag = document.getElementById("style-guru-script") || document.currentScript;
328    }
329
330    // Last resort: any widget.js script tag
331    if (!scriptTag) {
332      var scripts = document.querySelectorAll("script[src*='widget.js']");
333      if (scripts.length > 0) {
334        scriptTag = scripts[scripts.length - 1];
335      }
336    }
337
338    if (scriptTag) {
339      var srcUrl = scriptTag.src || "";
340      var urlParams = parseUrlParams(srcUrl);
341      var explicitApi = scriptTag.getAttribute("data-api") || urlParams.api || "";
342
343      shopDomain = scriptTag.getAttribute("data-shop") || urlParams.shop || "";
344      devCustomerId = scriptTag.getAttribute("data-customer-id") || urlParams.customer || "";
345      API_BASE = explicitApi || srcUrl.replace(/\/widget\.js.*$/, "");
346
347      if (explicitApi && /^https?:\/\//.test(explicitApi)) {
348        PROXY_PATH = "";
349      } else if (srcUrl.includes("style-guru.replit.app")) {
350        PROXY_PATH = "";
351        API_BASE = "https://style-guru.replit.app";
352      } else if (window.Shopify && window.Shopify.shop) {
353        PROXY_PATH = "/apps/style-guru";
354      }
355    } else {
356      if (window.Shopify && window.Shopify.shop) {
357        PROXY_PATH = "/apps/style-guru";
358      }
359    }
360
361    if (!shopDomain) return;
362
363    if (!PROXY_PATH && !API_BASE) return;
364
365    console.log("[StyleGuru] Widget v" + WIDGET_VERSION + " loaded | shop=" + shopDomain + " | " + (PROXY_PATH ? "proxy=" + PROXY_PATH : "api=" + API_BASE));
366
367    sessionId = getSessionId();
368    detectedCurrency = detectStoreCurrency();
369
370    if (!detectedCurrency) {
371      var currencyRetries = 0;
372      var currencyInterval = setInterval(function() {
373        reDetectCurrency();
374        currencyRetries++;
375        if (detectedCurrency || currencyRetries >= 10) clearInterval(currencyInterval);
376      }, 1000);
377    }
378
379    loadSettings(function() {
380      applyContainerOverrides();
381      injectStyles();
382
383      // Page-URL override for testing activation modes without touching the
384      // merchant's saved setting (?sg-mode=top_button etc.).
385      try {
386        var sgModeOverride = new URLSearchParams(window.location.search).get("sg-mode");
387        if (sgModeOverride) config.activationMode = sgModeOverride;
388      } catch (e) {}
389      var mode = config.activationMode || "both";
390      var showBell = mode === "bell_icon" || mode === "both" || mode === "top_button_bell";
391      var useSearch = mode === "search_trigger" || mode === "both";
392      var showTopButton = mode === "top_button" || mode === "top_button_bell";
393      if (showTopButton) renderTopButton();
394
395      if (useSearch) {
396        hookSearchBox();
397        initSearchTrigger();
398      }
399      if (showBell || useSearch) {
400        createFAB();
401      }
402
403      fetchWelcomeAudio();
404      restoreConversation();
405    });
406  }
407
408  function applyContainerOverrides() {
409    var container = document.getElementById("style-guru-container");
410    if (!container) return;
411
412    var map = {
413      "primary-color": "primaryColor",
414      "font-color": "fontColor",
415      "background-color": "backgroundColor",
416      "border-color": "borderColor",
417      "font-size": "fontSize",
418      "border-radius": "borderRadius",
419      "border-width": "borderWidth",
420      "widget-height": "widgetHeight",
421      "widget-width": "widgetWidth",
422      "widget-padding": "widgetPadding",
423      "box-shadow": "boxShadow",
424      "stylist-name": "stylistName",
425      "bell-position": "bellPosition",
426      "activation-mode": "activationMode",
427      "top-button-label": "topButtonLabel",
428      "top-button-color": "topButtonColor"
429    };
430
431    for (var attr in map) {
432      var val = container.getAttribute("data-" + attr);
433      if (val && val.trim() !== "") {
434        config[map[attr]] = val.trim();
435      }
436    }
437  }
438
439  function loadSettings(callback) {
440    if (!shopDomain) { callback(); return; }
441    var xhr = new XMLHttpRequest();
442    xhr.open("GET", apiUrl("/settings?shop=" + encodeURIComponent(shopDomain)));
443    xhr.onreadystatechange = function() {
444      if (xhr.readyState === 4) {
445        if (xhr.status === 200) {
446          try {
447            var data = JSON.parse(xhr.responseText);
448            if (data.primaryColor) config.primaryColor = data.primaryColor;
449            if (data.fontColor) config.fontColor = data.fontColor;
450            if (data.fontSize) config.fontSize = data.fontSize;
451            if (data.backgroundColor) config.backgroundColor = data.backgroundColor;
452            if (data.borderRadius) config.borderRadius = data.borderRadius;
453            if (data.borderColor) config.borderColor = data.borderColor;
454            if (data.borderWidth) config.borderWidth = data.borderWidth;
455            if (data.widgetHeight) config.widgetHeight = data.widgetHeight;
456            if (data.widgetWidth) config.widgetWidth = data.widgetWidth;
457            if (data.widgetPadding) config.widgetPadding = data.widgetPadding;
458            if (data.boxShadow) config.boxShadow = data.boxShadow;
459            if (data.stylistName) config.stylistName = data.stylistName;
460            if (data.welcomeMessage) config.welcomeMessage = data.welcomeMessage;
461            if (data.interactionMode) config.interactionMode = data.interactionMode;
462            if (data.activationMode) config.activationMode = data.activationMode;
463            if (data.topButtonLabel !== undefined && data.topButtonLabel !== null) config.topButtonLabel = data.topButtonLabel;
464            if (data.topButtonColor) config.topButtonColor = data.topButtonColor;
465            if (data.bellColor) config.bellColor = data.bellColor;
466            if (data.catalogLanding) config.catalogLanding = data.catalogLanding;
467            if (data.bellReRing !== undefined) config.bellReRing = data.bellReRing;
468            if (data.bellOpenChime !== undefined) config.bellOpenChime = data.bellOpenChime;
469            if (data.sizingTeaser !== undefined) config.sizingTeaser = data.sizingTeaser;
470            if (data.bellPosition) config.bellPosition = data.bellPosition;
471            if (data.hideBranding !== undefined) config.hideBranding = data.hideBranding;
472            if (data.emailCapture !== undefined) config.emailCapture = data.emailCapture;
473            if (data.whatsappCapture !== undefined) config.whatsappCapture = data.whatsappCapture;
474            if (data.leadGate !== undefined) config.leadGate = data.leadGate;
475            if (data.prechatSatisfied !== undefined) config.prechatSatisfied = data.prechatSatisfied;
476            if (data.serverVoice !== undefined) config.serverVoice = data.serverVoice;
477            if (data.fitcoreEnabled !== undefined) config.fitcoreEnabled = data.fitcoreEnabled;
478            if (data.fitcoreEmbedUrl) config.fitcoreEmbedUrl = data.fitcoreEmbedUrl;
479            if (data.storeVertical) config.storeVertical = data.storeVertical;
480            if (data.speechEngine !== undefined) config.speechEngine = sgResolveVoiceMode(d
480ata.speechEngine);
481            if (config.storeVertical && config.storeVertical !== "fashion") applyGenericStrings();
482            if (data.enabled === false) return;
483          } catch(e) {}
484        }
485        callback();
486      }
487    };
488    xhr.send();
489  }
490
491  // Voice preview mode: server sends speechEngine "live" | "preview" | false.
492  // "preview" means voice is fully wired for this shop but must stay invisible
493  // to shoppers — it only switches on in a browser where the merchant has set
494  // the local preview flag via ?sg_voice_preview=1 (?sg_voice_preview=0 clears).
495  function sgResolveVoiceMode(v) {
496    if (v !== "preview") return v; // "live", true, false pass through
497    try {
498      var m = (window.location.search || "").match(/[?&]sg_voice_preview=(1|0)/);
499      if (m) {
500        if (m[1] === "1") localStorage.setItem("sgVoicePreview", "1");
501        else localStorage.removeItem("sgVoicePreview");
502      }
503      return localStorage.getItem("sgVoicePreview") === "1" ? "preview" : false;
504    } catch (e) { return false; }
505  }
506
507  function getThemeFont() {
508    var body = document.body;
509    if (!body) return "'Inter', system-ui, -apple-system, sans-serif";
510    var computed = window.getComputedStyle(body);
511    return computed.fontFamily || "'Inter', system-ui, -apple-system, sans-serif";
512  }
513
514  function escapeHtml(str) {
515    if (!str) return "";
516    return String(str)
517      .replace(/&/g, "&amp;")
518      .replace(/</g, "&lt;")
519      .replace(/>/g, "&gt;")
520      .replace(/"/g, "&quot;")
521      .replace(/'/g, "&#039;");
522  }
523
524  function formatMessageText(str) {
525    if (!str) return "";
526    var escaped = escapeHtml(str);
527    escaped = escaped.replace(/\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g, '<a href="$2" target="_blank" rel="noopener" style="color:' + config.primaryColor + ';text-decoration:underline;">$1</a>');
528    escaped = escaped.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
529    escaped = escaped.replace(/\n/g, '<br>');
530    return escaped;
531  }
532
533  var PRICE_SYMBOLS = { GBP:"£", USD:"$", EUR:"€", JPY:"¥", AUD:"A$", CAD:"C$",
534    CHF:"CHF ", CNY:"Â¥", SEK:"kr", NZD:"NZ$", MXN:"MX$", SGD:"S$", HKD:"HK$",
535    NOK:"kr", KRW:"₩", INR:"₹", BRL:"R$", ZAR:"R", AED:"AED " };
536
537  function formatDisplayPrice(val, currencyCode) {
538    if (val === null || val === undefined || val === "") return "";
539    var str = String(val).trim();
540    if (!str || str === "0" || str === "0.00") return "";
541    // Already has a recognised currency symbol — return as-is
542    if (/^[£$€¥₩₹₺₽฿]/.test(str) || /^[A-Z]{2,4}\s*[\d]/.test(str)) return str;
543    var num = parseFloat(str.replace(/[^0-9.]/g, ""));
544    if (isNaN(num) || num === 0) return "";
545    var code = ((currencyCode || config.currency || detectedCurrency || "GBP") + "").toUpperCase();
546    var sym = PRICE_SYMBOLS[code] || (code + " ");
547    var noDecimal = ["JPY","KRW","HUF","CLP","VND","ISK"].indexOf(code) >= 0;
548    return sym + (noDecimal ? Math.round(num) : num.toFixed(2));
549  }
550
551  function sparklesIcon() {
552    return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l1.5 5.5L19 10l-5.5 1.5L12 17l-1.5-5.5L5 10l5.5-1.5L12 3z"/><path d="M19 15l.5 2L22 17.5l-2.5.5L19 20l-.5-2L16 17.5l2.5-.5L19 15z"/></svg>';
553  }
554
555  function sendIcon(color) {
556    var c = color || "currentColor";
557    return '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" style="display:block"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" fill="' + c + '"/></svg>';
558  }
559
560  function closeIcon(color) {
561    var c = color || "#555";
562    // Use <path> not <line> — Shopify theme CSS sometimes resets <line> display/stroke
563    return '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" width="18" height="18" style="display:block;overflow:visible" fill="none" stroke="' + c + '" stroke-width="2.5" stroke-linecap="round"><path d="M18 6L6 18M6 6l12 12"/></svg>';
564  }
565
566  function micIcon() {
567    // Inline width/height (attribute + style) so no store stylesheet can
568    // collapse it; stroke follows the button state colour.
569    return '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;width:18px;height:18px;min-width:18px;min-height:18px;flex-shrink:0"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 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="22"/></svg>';
570  }
571
572  function speakerIcon(color) {
573    var c = color || "currentColor";
574    return '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" style="display:block"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z" fill="' + c + '"/></svg>';
575  }
576
577  function speakerMutedIcon(color) {
578    var c = color || "currentColor";
579    return '<svg width="16" height="16" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" style="display:block;width:16px;height:16px"><path d="M3 9v6h4l5 5V4L7 9H3z" fill="' + c + '"/><line x1="16" y1="9" x2="22" y2="15" stroke="' + c + '" stroke-width="2" stroke-linecap="round"/><line x1="22" y1="9" x2="16" y2="15" stroke="' + c + '" stroke-width="2" stroke-linecap="round"/></svg>';
580  }
581
582  function searchIcon() {
583    return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>';
584  }
585
586  function bellIcon() {
587    return '<svg viewBox="1.75 0.75 20.5 22.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 0 1-3.46 0"/></svg>';
588  }
589
590  function mailIcon(color) {
591    var c = color || "currentColor";
592    return '<svg viewBox="0 0 24 24" fill="none" stroke="' + c + '" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>';
593  }
594
595  function rulerIcon(color) {
596    var c = color || "currentColor";
597    return '<svg viewBox="0 0 24 24" fill="none" stroke="' + c + '" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.3 15.3a2.4 2.4 0 0 1 0 3.4l-2.6 2.6a2.4 2.4 0 0 1-3.4 0L2.7 8.7a2.41 2.41 0 0 1 0-3.4l2.6-2.6a2.41 2.41 0 0 1 3.4 0Z"/><path d="m14.5 12.5 2-2"/><path d="m11.5 9.5 2-2"/><path d="m8.5 6.5 2-2"/><path d="m17.5 15.5 2-2"/></svg>';
598  }
599
600  // "Add to Bag" on desktop, plain "Add" on narrow screens (CSS swaps them —
601  // the full label wraps and misaligns inside mobile product cards).
602  function cartLabel() {
603    return '<span class="sg-cart-full">' + T("addToBag") + '</span><span class="sg-cart-short">' + T("add") + '</span>';
604  }
605
606  function cartIcon(color) {
607    var c = color || "#fff";
608    return '<svg viewBox="0 0 24 24" style="display:inline-block;vertical-align:middle" fill="none" stroke="' + c + '" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6"/></svg>';
609  }
610
611  function checkIcon(color) {
612    var c = color || "#fff";
613    return '<svg viewBox="0 0 24 24" style="display:inline-block;vertical-align:middle" fill="none" stroke="' + c + '" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
614  }
615
616  function externalIcon(color) {
617    var c = color || "#555";
618    return '<svg viewBox="0 0 24 24" style="display:inline-block;vertical-align:middle" fill="none" stroke="' + c + '" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>';
619  }
620
621  function hasSpeechRecognition() {
622    return !!(window.SpeechRecognition || window.webkitSpeechRecognition);
623  }
624
625  function injectStyles() {
626    var existing = document.getElementById("style-guru-styles");
627    if (existing) existing.remove();
628    var style = document.createElement("style");
629    style.id = "style-guru-styles";
630
631    var pc = config.primaryColor;
632    var rgb = hexToRgb(pc);
633    var addedFg = isLightColor(pc) ? "#111" : "#fff";
634    var bellBg = config.bellColor || pc;
635    var bellFg = isLightColor(bellBg) ? "#111" : "#fff";
636    var bellRgb = hexToRgb(bellBg);
637    var fc = config.fontColor;
638    var bg = config.backgroundColor;
639    var bc = config.borderColor;
640    var fs = parseInt(config.fontSize) || 14;
641    var themeFont = getThemeFont();
642    var primaryOnBg = isLightColor(pc) ? "#111" : "#fff";
643    var bgDark = isLightColor(bg);
644    var surfaceBg = bgDark ? "rgba(0,0,0,0.02)" : "rgba(255,255,255,0.05)";
645    var surfaceBorder = bgDark ? "rgba(0,0,0,0.08)" : "rgba(255,255,255,0.1)";
646    var textSec = bgDark ? "rgba(0,0,0,0.55)" : "rgba(255,255,255,0.6)";
647    var textTert = bgDark ? "rgba(0,0,0,0.38)" : "rgba(255,255,255,0.4)";
648
649    style.textContent = '\
650      .sg-fab {\
651        position: fixed !important; z-index: 2147483646 !important;\
652        width: 64px; height: 64px; border-radius: 50%;\
653        background: ' + bellBg + '; color: ' + bellFg + ';\
654        border: none; cursor: pointer;\
655        display: flex; align-items: center; justify-content: center;\
656        box-shadow: 0 4px 16px rgba(0,0,0,0.18), 0 2px 4px rgba(0,0,0,0.1);\
657        transition: transform 0.2s ease, box-shadow 0.2s ease;\
658        font-family: ' + themeFont + ';\
659      }\
660      .sg-fab.sg-bell {\
661        background: linear-gradient(135deg, ' + bellBg + ' 0%, ' + shadeHex(bellBg, 14) + ' 50%, ' + shadeHex(bellBg, -16) + ' 100%);\
662        color: ' + bellFg + ';\
663        box-shadow: 0 4px 16px rgba(' + bellRgb + ',0.35), 0 2px 4px rgba(0,0,0,0.1);\
664      }\
665      .sg-fab:hover { transform: scale(1.06); box-shadow: 0 6px 24px rgba(0,0,0,0.22); }\
666      .sg-fab.sg-bell:hover {\
667        box-shadow: 0 6px 24px rgba(' + bellRgb + ',0.45), 0 2px 8px rgba(0,0,0,0.15);\
668      }\
669      .sg-fab.sg-bell:hover svg { animation: sg-bell-ring 0.6s ease; }\
670      .sg-fab.sg-fab-waiting { opacity: 0; transform: scale(0.5); pointer-events: none; }\
671      .sg-fab.sg-fab-enter { animation: sg-fab-enter 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both; }\
672      .sg-fab.sg-fab-enter svg { animation: sg-bell-ring 0.7s ease 0.45s; }\
673      @keyframes sg-fab-enter {\
674        from { opacity: 0; transform: scale(0.5) translateY(14px); }\
675        to { opacity: 1; transform: scale(1) translateY(0); }\
676      }\
677      .sg-fab.sg-fab-fade-enter { animation: sg-fab-fade-enter 0.5s ease both; }\
678      .sg-fab.sg-fab-rering svg { animation: sg-bell-ring 0.7s ease; }\
679      @keyframes sg-fab-fade-enter { from { opacity: 0; } to { opacity: 1; } }\
680      @keyframes sg-bell-ring {\
681        0% { transform: rotate(0); }\
682        15% { transform: rotate(14deg); }\
683        30% { transform: rotate(-12deg); }\
684        45% { transform: rotate(10deg); }\
685        60% { transform: rotate(-8deg); }\
686        75% { transform: rotate(4deg); }\
687        100% { transform: rotate(0); }\
688      }\
689      .sg-fab svg { width: 52px !important; height: 52px !important; max-width: none !important; max-height: none !important; display: block !important; flex-shrink: 0 !important; }\
690      .sg-fab-badge {\
691        position: absolute; top: -2px; right: -2px;\
692        width: 14px; height: 14px; border-radius: 50%;\
693        background: #22c55e; border: 2px solid #fff;\
694      }\
695      \
696      .sg-overlay {\
697        position: fixed !important; top: 0 !important; left: 0 !important;\
698        right: 0 !important; bottom: 0 !important;\
699        width: 100vw !important; height: 100vh !important; height: 100dvh !important;\
700        z-index: 2147483647 !important;\
701        background: rgba(0,0,0,0.5) !important;\
702        backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);\
703        display: none !important; align-items: center; justify-content: center;\
704        opacity: 0; transition: opacity 0.25s ease;\
705        font-family: ' + themeFont + ';\
706      }\
707      .sg-overlay.sg-visible { display: flex !important; opacity: 1; }\
708      \
709      .sg-modal {\
710        width: 94vw; max-width: 860px;\
711        height: 88vh; max-height: 720px;\
712        background: ' + bg + ';\
713        border-radius: 16px;\
714        display: flex; flex-direction: column;\
715        box-shadow: 0 24px 80px rgba(0,0,0,0.25);\
716        overflow: hidden;\
717        color: ' + fc + ';\
718        font-size: ' + fs + 'px;\
719        line-height: 1.5;\
720        transform: translateY(20px) scale(0.97);\
721        transition: transform 0.3s cubic-bezier(0.22,1,0.36,1);\
722      }\
723      .sg-overlay.sg-visible .sg-modal {\
724        transform: translateY(0) scale(1);\
725      }\
726      \
727      .sg-modal-header {\
728        padding: 16px 20px;\
729        display: flex; align-items: center; gap: 12px;\
730        border-bottom: 1px solid ' + surfaceBorder + ';\
731        flex-shrink: 0;\
732      }\
733      .sg-modal-logo {\
734        width: 48px; height: 48px; border-radius: 12px;\
735        background: rgba(' + rgb + ', 0.1);\
736        display: flex; align-items: center; justify-content: center;\
737        color: ' + pc + '; flex-shrink: 0;\
738      }\
739      .sg-modal-logo svg { width: 26px;
739 height: 26px; }\
740      .sg-modal-header-info { flex: 1; min-width: 0; }\
741      .sg-modal-header-title { font-weight: 600; font-size: ' + (fs + 2) + 'px; }\
742      .sg-modal-header-sub { font-size: ' + (fs - 2) + 'px; color: ' + textSec + '; }\
743      .sg-modal-header-actions { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }\
744      .sg-hdr-btn {\
745        width: 34px; height: 34px; border-radius: 8px;\
746        background: ' + surfaceBg + '; border: 1px solid ' + surfaceBorder + '; cursor: pointer;\
747        display: flex; align-items: center; justify-content: center;\
748        color: ' + fc + '; opacity: 1;\
749        transition: background 0.15s, border-color 0.15s, transform 0.1s;\
750      }\
751      .sg-hdr-btn:hover { background: rgba(' + (isLightColor(bg) ? "0,0,0,0.08" : "255,255,255,0.12") + '); border-color: ' + pc + '; transform: scale(1.05); }\
752      .sg-hdr-btn svg { width: 16px; height: 16px; }\
753      .sg-hdr-btn.sg-active { opacity: 1; color: ' + pc + '; }\
754      .sg-hdr-btn.sg-speaking { opacity: 1; color: ' + pc + '; animation: sg-speak-pulse 1s ease-in-out infinite; }\
755      #sg-browse-btn { display: none; position: relative; }\
756      #sg-live-btn.sg-live-active { opacity: 1; color: ' + pc + '; border-color: ' + pc + '; animation: sg-speak-pulse 1.2s ease-in-out infinite; }\
757      .sg-hdr-badge {\
758        position: absolute; top: -4px; right: -5px; min-width: 15px; height: 15px;\
759        padding: 0 4px; border-radius: 8px; background: ' + pc + '; color: #fff;\
760        font-size: 9.5px; font-weight: 700; line-height: 15px; text-align: center;\
761      }\
762      #sg-modal-close {\
763        width: 32px; height: 32px; border-radius: 50%;\
764        background: rgba(' + (isLightColor(bg) ? "0,0,0,0.10" : "255,255,255,0.15") + ');\
765        border: 1.5px solid rgba(' + (isLightColor(bg) ? "0,0,0,0.22" : "255,255,255,0.3") + ');\
766        color: ' + (isLightColor(bg) ? "#222" : "#eee") + ';\
767        display: flex; align-items: center; justify-content: center;\
768        flex-shrink: 0;\
769      }\
770      #sg-modal-close:hover {\
771        background: rgba(' + (isLightColor(bg) ? "0,0,0,0.18" : "255,255,255,0.28") + ');\
772        border-color: rgba(' + (isLightColor(bg) ? "0,0,0,0.36" : "255,255,255,0.5") + ');\
773        transform: scale(1.1);\
774      }\
775      #sg-modal-close svg { width: 16px; height: 16px; display: block; overflow: visible; }\
776      @keyframes sg-speak-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }\
777      \
778      .sg-modal-body {\
779        flex: 1; display: flex; overflow: hidden; min-height: 0;\
780      }\
781      \
782      .sg-chat-col {\
783        width: 340px; min-width: 280px;\
784        display: flex; flex-direction: column;\
785        border-right: 1px solid ' + surfaceBorder + ';\
786        flex-shrink: 0;\
787      }\
788      .sg-chat-messages {\
789        flex: 1; overflow-y: auto; padding: 16px;\
790        display: flex; flex-direction: column; gap: 10px;\
791      }\
792      .sg-chat-input-row {\
793        padding: 12px 16px; border-top: 1px solid ' + surfaceBorder + ';\
794        display: flex; gap: 8px; align-items: center; flex-shrink: 0;\
795      }\
796      .sg-chat-input {\
797        flex: 1; border: 1px solid ' + surfaceBorder + '; border-radius: 10px;\
798        padding: 10px 14px; font-size: ' + fs + 'px; font-family: inherit;\
799        color: ' + fc + '; background: transparent; outline: none;\
800        transition: border-color 0.15s;\
801      }\
802      .sg-chat-input:focus { border-color: ' + pc + '; }\
803      .sg-chat-input::placeholder { color: ' + textTert + '; }\
804      .sg-chat-send {\
805        width: 40px; height: 40px; border-radius: 10px;\
806        background: ' + pc + '; color: ' + primaryOnBg + ';\
807        border: none; cursor: pointer;\
808        display: flex; align-items: center; justify-content: center;\
809        flex-shrink: 0; transition: opacity 0.15s, transform 0.1s, box-shadow 0.15s;\
810        box-shadow: 0 2px 6px rgba(0,0,0,0.15);\
811      }\
812      .sg-chat-send:hover:not(:disabled) { opacity: 0.88; transform: scale(1.05); box-shadow: 0 3px 10px rgba(0,0,0,0.2); }\
813      .sg-chat-send:disabled { opacity: 0.45; filter: grayscale(60%); cursor: default; box-shadow: none; }\
814      .sg-chat-send svg { width: 20px; height: 20px; }\
815      .sg-powered { text-align: center; padding: 0 0 8px; }\
816      .sg-powered a { font-size: ' + (fs - 4) + 'px; color: ' + textTert + '; text-decoration: none; letter-spacing: 0.02em; }\
817      .sg-powered a:hover { color: ' + textSec + '; }\
818      .sg-powered strong { font-weight: 600; }\
819      .sg-mic-btn {\
820        width: 36px; height: 36px; border-radius: 10px;\
821        background: transparent; border: 1px solid ' + surfaceBorder + ';\
822        color: ' + fc + '; cursor: pointer;\
823        display: flex; align-items: center; justify-content: center;\
824        flex-shrink: 0; opacity: 0.6;\
825        transition: opacity 0.15s, background 0.15s, border-color 0.15s;\
826      }\
827      .sg-mic-btn:hover { opacity: 1; border-color: ' + pc + '; color: ' + pc + '; }\
828      .sg-mic-btn svg { width: 18px; height: 18px; }\
829      .sg-mic-btn.sg-listening {\
830        background: rgba(220,38,38,0.1); border-color: #dc2626; color: #dc2626; opacity: 1;\
831        animation: sg-pulse 1.5s ease-in-out infinite;\
832      }\
833      @keyframes sg-pulse { 0%,100%{box-shadow:0 0 0 0 rgba(220,38,38,0.3)} 50%{box-shadow:0 0 0 6px rgba(220,38,38,0)} }\
834      \
835      .sg-msg { max-width: 90%; display: flex; flex-direction: column; gap: 4px; }\
836      .sg-fb-row { display: flex; gap: 2px; align-items: center; margin-top: 2px; padding-left: 6px; }\
837      .sg-fb-btn { background: none; border: none; cursor: pointer; opacity: 0.35; padding: 3px; line-height: 1; transition: opacity 0.15s, transform 0.15s; }\
838      .sg-fb-btn:hover { opacity: 0.9; transform: scale(1.15); }\
839      .sg-fb-thanks { font-size: 11px; opacity: 0.55; padding-left: 6px; margin-top: 2px; }\
840      .sg-msg-user { align-self: flex-end; }\
841      .sg-msg-assistant { align-self: flex-start; }\
842      .sg-msg-bubble {\
843        padding: 10px 14px; border-radius: 12px;\
844        font-size: ' + fs + 'px; line-height: 1.55;\
845        word-wrap: break-word; overflow-wrap: break-word;\
846      }\
847      .sg-msg-user .sg-msg-bubble {\
848        background: ' + pc + '; color: ' + primaryOnBg + ';\
849        border-bottom-right-radius: 4px;\
850      }\
851      .sg-msg-assistant .sg-msg-bubble {\
852        background: ' + surfaceBg + '; color: ' + fc + ';\
853        border: 1px solid ' + surfaceBorder + ';\
854        border-bottom-left-radius: 4px;\
855      }\
856      .sg-typing { display: flex; gap: 4px; align-items: center; padding: 4px 0; }\
857      .sg-typing-dot {\
858        width: 5px; height: 5px; border-radius: 50%;\
859        background: ' + textTert + ';\
860        animation: sg-bounce 1.4s ease-in-out infinite;\
861      }\
862      .sg-typing-dot:nth-child(2) { animation-delay: 0.2s; }\
863      .sg-typing-dot:nth-child(3) { animation-delay: 0.4s; }\
864      .sg-typing-label { font-size: 12px; color: ' + textSec + '; margin-left: 4px; }\
865      @keyframes sg-bounce { 0%,80%,100%{transform:translateY(0)} 40%{transform:translateY(-5px)} }\
866      \
867      .sg-welcome-section {\
868        display: flex; flex-direction: column; align-items: center;\
869        text-align: center; padding: 32px 20px 16px;\
870      }\
871      .sg-welcome-title { font-size: ' + (fs + 6) + 'px; font-weight: 700; margin-bottom: 4px; letter-spacing: 0.01em; }\
872      .sg-welcome-sub { font-size: ' + (fs - 1) + 'px; color: ' + textSec + '; margin-bottom: 16px; }\
873      .sg-suggestions { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }\
874      .sg-suggestion {\
875        padding: 7px 14px; border: 1px solid ' + surfaceBorder + ';\
876        border-radius: 20px; font-size: ' + (fs - 1) + 'px;\
877        color: ' + fc + '; cursor: pointer;\
878        background: transparent; font-family: inherit;\
879        transition: border-color 0.15s, background 0.15s, color 0.15s;\
880      }\
881      .sg-suggestion:hover { border-color: ' + pc + '; background: rgba(' + rgb + ', 0.06); color: ' + pc + '; }\
882      \
883      .sg-products-col {\
884        flex: 1; display: flex; flex-direction: column;\
885        overflow: hidden; min-width: 0; min-height: 0;\
886        position: relative;\
887      }\
888      .sg-products-header {\
889        padding: 14px 20px; border-bottom: 1px solid ' + surfaceBorder + ';\
890        font-weight: 600; font-size: ' + (fs + 1) + 'px;\
891        display: flex; align-items: center; gap: 8px; flex-shrink: 0;\
892        color: ' + fc + ';\
893      }\
894      .sg-products-header-count {\
895        font-weight: 400; font-size: ' + (fs - 1) + 'px; color: ' + textSec + ';\
896      }\
897      .sg-filter-bar {\
898        padding: 10px 14px 8px; border-bottom: 1px solid ' + surfaceBorder + ';\
899        display: flex; flex-direction: column; gap: 7px; flex-shrink: 0;\
900        background: ' + (isLightColor(bg) ? "rgba(0,0,0,0.018)" : "rgba(255,255,255,0.025)") + ';\
901      }\
902      .sg-filter-top-row {\
903        display: flex; align-items: center; gap: 8px;\
904      }\
905      .sg-filter-label {\
906        font-size: ' + (fs - 2) + 'px; font-weight: 600;\
907        text-transform: uppercase; letter-spacing: 0.07em;\
908        color: ' + textSec + '; white-space: nowrap; flex-shrink: 0;\
909      }\
910      .sg-filter-search-wrap {\
911        position: relative; flex: 1;\
912      }\
913      .sg-filter-search-icon {\
914        position: absolute; left: 9px; top: 50%; transform: translateY(-50%);\
915        pointer-events: none; opacity: 0.45;\
916      }\
917      .sg-filter-search {\
918        width: 100%; height: 30px;\
919        border: 1px solid ' + surfaceBorder + '; border-radius: 15px;\
920        background: ' + bg + '; color: ' + fc + ';\
921        padding: 0 10px 0 28px; font-size: ' + (fs - 1) + 'px; font-family: inherit;\
922        outline: none; box-sizing: border-box; transition: border-color 0.15s;\
923      }\
924      .sg-filter-search:focus { border-color: ' + pc + '; }\
925      .sg-filter-search::placeholder { color: ' + textSec + '; }\
926      .sg-filter-clear-btn {\
927        height: 26px; padding: 0 9px; border-radius: 13px;\
928        border: 1px solid ' + surfaceBorder + '; background: transparent;\
929        color: ' + textSec + '; font-size: ' + (fs - 2) + 'px; font-family: inherit;\
930        cursor: pointer; white-space: nowrap; flex-shrink: 0;\
931        display: none; transition: all 0.15s;\
932      }\
933      .sg-filter-clear-btn:hover { border-color: ' + pc + '; color: ' + pc + '; }\
934      .sg-filter-clear-btn.sg-visible { display: block; }\
935      .sg-filter-chips {\
936        display: flex; align-items: center; gap: 5px;\
937        overflow-x: auto; padding-bottom: 2px;\
938        scrollbar-width: none;\
939      }\
940      .sg-filter-chips::-webkit-scrollbar { display: none; }\
941      .sg-filter-select {\
942        height: 26px !important; min-height: 26px !important; max-height: 26px !important;\
943        line-height: 24px !important; vertical-align: middle !important;\
944        width: auto !important; max-width: 140px !important; margin: 0 !important;\
945        border: 1px solid ' + surfaceBorder + ' !important; border-radius: 13px !important;\
946        background-color: ' + bg + ' !important; color: ' + fc + ' !important;\
947        padding: 0 22px 0 10px !important; font-size: ' + (fs - 2) + 'px !important; font-family: inherit !important;\
948        cursor: pointer; outline: none; flex-shrink: 0 !important; flex-grow: 0 !important;\
949        appearance: none !important; -webkit-appearance: none !important;\
950        background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27 width%3D%2710%27 height%3D%276%27 viewBox%3D%270 0 10 6%27%3E%3Cpath d%3D%27M1 1l4 4 4-4%27 stroke%3D%27%23999%27 stroke-width%3D%271.5%27 fill%3D%27none%27 stroke-linecap%3D%27round%27%2F%3E%3C%2Fsvg%3E") !important;\
951        background-repeat: no-repeat !important; background-position: right 6px center !important;\
952        transition: border-color 0.15s, color 0.15s; white-space: nowrap;\
953        box-sizing: border-box !important;\
954      }\
955      .sg-filter-select:focus { border-color: ' + pc + '; }\
956      .sg-filter-select.sg-filter-active {\
957        border-color: ' + pc + '; background-color: rgba(' + rgb + ',0.08);\
958        color: ' + pc + '; font-weight: 600;\
959      }\
960      .sg-filter-toggle {\
961        height: 26px; padding: 0 9px; border-radius: 13px;\
962        border: 1px solid ' + surfaceBorder + '; background: ' + bg + ';\
963        color: ' + textSec + '; font-size: ' + (fs - 2) + 'px; font-family: inherit;\
964        cursor: pointer; white-space: nowrap; flex-shrink: 0;\
965        transition: all 0.15s; display: flex; align-items: center; gap: 4px;\
966      }\
967      .sg-filter-toggle.sg-filter-active {\
968        border-color: ' + pc + '; background: rgba(' + rgb + ',0.08);\
969        color: ' + pc + '; font-weight: 600;\
970      }\
971      .sg-filter-loading {\
972        text-align: center; padding: 40px 16px;\
973        color: ' + textSec + '; font-size: ' + (fs - 1) + 'px;\
974        display: flex; flex-direction: column; align-items: center; gap: 12px;\
975      }\
976      .sg-filter-divider {\
977        height: 1px; background: ' + surfaceBorder + '; margin: 0;\
978      }\
979      .sg-products-scroll {\
980        flex: 1; overflow-y: auto; min-height: 0;\
981        display: flex; flex-direction: column;\
982        scrollbar-width: thin;\
983        scrollbar-color: rgba(' + rgb + ',0.35) transparent;\
984      }\
985      .sg-products-scroll::-webkit-scrollbar { width: 6px; }\
986      .sg-products-scroll::-webkit-scrollbar-track { background: transparent; }\
987      .sg-products-scroll::-webkit-scrollbar-thumb {\
988        background: rgba(' + rgb + ',0.35); border-radius: 6px;\
989      }\
990      .sg-products-scroll::-webkit-scrollbar-thumb:hover {\
991        background: rgba(' + rgb + ',0.55);\
992      }\
993      .sg-products-grid {\
994        padding: 16px 20px; flex-shrink: 0;\
995        display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));\
996        grid-auto-rows: max-content;\
997        gap: 16px; align-content: start;\
998      }\
999      .sg-products-col-inner {\
1000        flex: 1; display: flex; flex-direction: column; min-height: 0; position: relative;\
1001      }\
1002      .sg-products-fade-top, .sg-products-fade-bottom {\
1003        position: absolute; left: 0; right: 8px; height: 32px;\
1004        pointer-events: none; z-index: 1; opacity: 0;\
1005        transition: opacity 0.2s;\
1006      }\
1007      .sg-products-fade-top {\
1008        top: 0;\
1009        background: linear-gradient(to bottom, ' + bg + ', transparent);\
1010      }\
1011      .sg-products-fade-bottom {\
1012        bottom: 0;\
1013        background: linear-gradient(to top, ' + bg + ', transparent);\
1014      }\
1015      .sg-products-fade-top.sg-fade-visible,\
1016      .sg-products-fade-bottom.sg-fade-visible { opacity: 1; }\
1017      .sg-product-card {\
1018        border: 1px solid ' + surfaceBorder + '; border-radius: 12px;\
1019        overflow: hidden; background: ' + bg + ';\
1020        transition: border-color 0.2s, box-shadow 0.2s;\
1021        display: flex; flex-direction: column;\
1022      }\
1023      .sg-product-card:hover {\
1024        border-color: rgba(' + rgb + ', 0.3);\
1025        box-shadow: 0 4px 16px rgba(0,0,0,0.06);\
1026      }\
1027      .sg-product-card > a {\
1028        display: block; cursor: zoom-in;\
1029      }\
1030      .sg-product-img {\
1031        width: 100%; aspect-ratio: 1/1; object-fit: cover;\
1032        background: ' + surfaceBg + ';\
1033        display: block;\
1034      }\
1035      .sg-img-lightbox {\
1036        display: none; position: absolute; inset: 0; z-index: 10;\
1037        background: rgba(0,0,0,0.93); flex-direction: column;\
1038        border-radius: 16px;\
1039      }\
1040      .sg-img-lightbox.sg-lb-open { display: flex; }\
1041      .sg-lb-topbar {\
1042        flex-shrink: 0; display: flex; align-items: center;\
1043        padding: 10px 14px; gap: 10px;\
1044        border-bottom: 1px solid rgba(255,255,255,0.1);\
1045      }\
1046      .sg-lb-close {\
1047        display: flex; align-items: center; gap: 6px;\
1048        background: rgba(255,255,255,0.12); border: none;\
1049        color: rgba(255,255,255,0.9); cursor: pointer;\
1050        padding: 6px 12px; border-radius: 20px;\
1051        font-size: ' + (fs - 1) + 'px; font-family: inherit;\
1052        transition: background 0.15s; flex-shrink: 0;\
1053      }\
1054      .sg-lb-close:hover { background: rgba(255,255,255,0.22); }\
1055      .sg-lb-topname {\
1056        flex: 1; color: rgba(255,255,255,0.8);\
1057        font-size: ' + (fs - 1) + 'px; font-weight: 500;\
1058        overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\
1059      }\
1060      .sg-lb-counter {\
1061        flex-shrink: 0; color: rgba(255,255,255,0.5);\
1062        font-size: ' + (fs - 2) + 'px;\
1063      }\
1064      .sg-lb-imgarea {\
1065        flex: 1; display: flex; align-items: center; justify-content: center;\
1066        position: relative; overflow: hidden; min-height: 0;\
1067      }\
1068      .sg-img-lightbox img {\
1069        max-width: 90%; max-height: 100%; border-radius: 8px;\
1070        object-fit: contain;\
1071      }\
1072      .sg-lb-arrow {\
1073        position: absolute; top: 50%; transform: translateY(-50%);\
1074        width: 40px; height: 40px; border-radius: 50%;\
1075        background: rgba(255,255,255,0.16); border: none;\
1076        color: #fff; cursor: pointer; font-size: 22px;\
1077        display: flex; align-items: center; justify-content: center;\
1078        transition: background 0.15s, opacity 0.15s;\
1079      }\
1080      .sg-lb-arrow:hover { background: rgba(255,255,255,0.30); }\
1081      .sg-lb-arrow:disabled { opacity: 0.18; cursor: default; }\
1082      .sg-lb-prev { left: 10px; }\
1083      .sg-lb-next { right: 10px; }\
1084      .sg-product-img-placeholder {\
1085        width: 100%; aspect-ratio: 1/1;\
1086        background: ' + surfaceBg + ';\
1087        display: flex; align-items: center; justify-content: center;\
1088        color: ' + textTert + ';\
1089      }\
1090      .sg-product-img-placeholder svg { width: 32px; height: 32px; opacity: 0.4; }\
1091      .sg-product-info { padding: 12px; display: flex; flex-direction: column; gap: 6px; flex: 1; }\
1092      .sg-product-name {\
1093        font-weight: 600; font-size: ' + fs + 'px;\
1094        display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;\
1095      }\
1096      .sg-product-name a { color: ' + fc + '; text-decoration: none; }\
1097      .sg-product-name a:hover { color: ' + pc + '; }\
1098      .sg-product-price { font-size: ' + (fs + 1) + 'px; font-weight: 700; color: ' + fc + '; }\
1099      .sg-product-variant {\
1100        font-size: ' + (fs - 2) + 'px; color: ' + textSec + ';\
1101        display: flex; align-items: center; gap: 4px;\
1102      }\
1103      .sg-product-variant-dot { width: 6px; height: 6px; border-radius: 50%; background: #22c55e; flex-shrink: 0; }\
1104      .sg-variant-picker {\
1105        padding: 0 12px 8px; display: flex; flex-direction: column; gap: 6px;\
1106      }\
1107      .sg-variant-select-group {\
1108        display: flex; flex-direction: column; gap: 2px;\
1109      }\
1110      .sg-variant-select-label {\
1111        font-size: ' + (fs - 3) + 'px; font-weight: 600; color: ' + textSec + ';\
1112        text-transform: uppercase; letter-spacing: 0.5px;\
1113      }\
1114      .sg-variant-select {\
1115        width: 100%; padding: 6px 28px 6px 10px; border-radius: 8px;\
1116        border: 1px solid ' + surfaceBorder + ';\
1117        background: ' + bg + '; color: ' + fc + ';\
1118        font-size: ' + (fs - 1) + 'px; font-family: inherit;\
1119        cursor: pointer; outline: none;\
1120        -webkit-appearance: none; -moz-appearance: none; appearance: none;\
1121        background-image: url("data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' width=\'12\' height=\'12\' viewBox=\'0 0 24 24\' fill=\'none\' stroke=\'%23666\' stroke-width=\'2\'%3E%3Cpolyline points=\'6 9 12 15 18 9\'/%3E%3C/svg%3E");\
1122        background-repeat: no-repeat; background-position: right 8px center;\
1123        transition: border-color 0.15s;\
1124      }\
1125      .sg-variant-select:hover { border-color: ' + pc + '; }\
1126      .sg-variant-select:focus { border-color: ' + pc + '; box-shadow: 0 0 0 2px rgba(' + rgb + ', 0.15); }\
1127      .sg-needs-size { border-color: #dc2626 !important; animation: sg-size-pulse 1.2s ease-in-out 2; }\
1128      @keyframes sg-size-pulse { 0%,100% { box-shadow: 0 0 0 0 rgba(220,38,38,0.35); } 50% { box-shadow: 0 0 0 4px rgba(220,38,38,0.18); } }\
1129      .sg-size-msg { font-size: 11px; color: #dc2626; margin-top: 4px; line-height: 1.4; }\
1130      .sg-size-msg a { color: inherit; text-decoration: underline; cursor: pointer; font-weight: 600; }\
1131      .sg-variant-static-value {\
1132        font-size: ' + (fs - 1) + 'px; color: ' + fc + '; padding: 2px 0;\
1133      }\
1134      .sg-advisory-note {\
1135        font-size: ' + (fs - 4) + 'px; color: ' + textSec + '; font-style: italic; margin-top: 2px;\
1136      }\
1137      .sg-advisory-select { border-style: dashed; }\
1138      .sg-product-actions { padding: 0 12px 12px; display: flex; gap: 6px; }\
1139      .sg-btn-cart {\
1140        flex: 1; padding: 8px 12px; border-radius: 8px;\
1141        background: ' + pc + '; color: ' + primaryOnBg + ';\
1142        border: none; cursor: pointer;\
1143        font-size: ' + (fs - 1) + 'px; font-weight: 600; font-family: inherit;\
1144        display: flex; align-items: center; justify-content: center; gap: 6px;\
1145        transition: opacity 0.15s;\
1146      }\
1147      .sg-btn-cart:hover { opacity: 0.9; }\
1148      .sg-btn-cart:disabled { opacity: 0.5; cursor: default; }\
1149      .sg-btn-cart svg { width: 14px; height: 14px; }\
1150      .sg-btn-cart.sg-added { background: ' + pc + '; color: ' + addedFg + '; }\
1151      .sg-btn-cart.sg-sold-out { background: #9ca3af; color: #fff; cursor: not-allowed; opacity: 0.7; }\
1152      .sg-btn-view {\
1153        padding: 8px 12px; border-radius: 8px;\
1154        background: transparent; border: 1px solid ' + surfaceBorder + ';\
1155        color: ' + fc + '; cursor: pointer;\
1156        display: flex; align-items: center; justify-content: center; gap: 4px;\
1157        flex-shrink: 0; text-decoration: none;\
1158        font-size: ' + (fs - 1) + 'px; font-weight: 500;\
1159        transition: border-color 0.15s, color 0.15s;\
1160        white-space: nowrap;\
1161      }\
1162      .sg-btn-view:hover { border-color: ' + pc + '; color: ' + pc + '; }\
1163      .sg-btn-view svg { width: 12px; height: 12px; }\
1164      \
1165      .sg-empty-products {\
1166        display: flex; flex-direction: column; align-items: center;\
1167        justify-content: center; padding: 48px 24px; text-align: center;\
1168        color: ' + textSec + '; height: 100%;\
1169      }\
1170      .sg-empty-products svg { width: 48px; height: 48px; margin-bottom: 16px; opacity: 0.3; color: ' + pc + '; }\
1171      .sg-empty-products h4 { font-size: ' + (fs + 1) + 'px; font-weight: 600; color: ' + fc + '; margin-bottom: 4px; }\
1172      .sg-empty-products p { font-size: ' + (fs - 1) + 'px; max-width: 240px; }\
1173      \
1174      .sg-products-grid.sg-has-bundles {\
1175        display: flex; flex-direction: column; gap: 12px;\
1176      }\
1177      .sg-bundle {\
1178        border: 1px solid ' + surfaceBorder + '; border-radius: 14px;\
1179        overflow: hidden; background: ' + bg + ';\
1180        margin-bottom: 4px;\
1181      }\
1182      .sg-bundle-header {\
1183        padding: 12px 16px; display: flex; align-items: center; justify-content: space-between;\
1184        gap: 8px; flex-wrap: wrap; border-bottom: 1px solid ' + surfaceBorder + ';\
1185        background: rgba(' + rgb + ', 0.04);\
1186      }\
1187      .sg-bundle-title {\
1188        font-weight: 700; font-size: ' + fs + 'px; color: ' + fc + ';\
1189        display: flex; align-items: center; gap: 6px;\
1190      }\
1191      .sg-bundle-title svg { width: 16px; height: 16px; color: ' + pc + '; }\
1192      .sg-bundle-meta {\
1193        display: flex; align-items: center; gap: 10px;\
1194      }\
1195      .sg-bundle-price {\
1196        font-weight: 700; font-size: ' + (fs + 1) + 'px; color: ' + pc + ';\
1197      }\
1198      .sg-bundle-count {\
1199        font-size: ' + (fs - 2) + 'px; color: ' + textSec + ';\
1200        background: ' + surfaceBg + '; padding: 2px 8px; border-radius: 10px;\
1201      }\
1202      .sg-bundle-items {\
1203        display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));\
1204        grid-auto-rows: max-content;\
1205        gap: 12px; padding: 12px 16px;\
1206      }\
1207      .sg-bundle-footer {\
1208        padding: 10px 16px; border-top: 1px solid ' + surfaceBorder + ';\
1209        display: flex; align-items: center; justify-content: center; gap: 8px;\
1210      }\
1211      .sg-btn-add-all {\
1212        width: 100%; padding: 10px 16px; border-radius: 10px;\
1213        background: ' + pc + '; color: ' + primaryOnBg + ';\
1214        border: none; cursor: pointer;\
1215        font-size: ' + fs + 'px; font-weight: 700; font-family: inherit;\
1216        display: flex; align-items: center; justify-content: center; gap: 8px;\
1217        transition: opacity 0.15s;\
1218      }\
1219      .sg-btn-add-all:hover { opacity: 0.9; }\
1220      .sg-btn-add-all:disabled { opacity: 0.5; cursor: default; }\
1221      .sg-btn-add-all svg { width: 16px; height: 16px; }\
1222      .sg-btn-add-all.sg-added { background: ' + pc + '; color: ' + addedFg + '; }\
1223      .sg-bundle-toast {\
1224        padding: 8px 12px; margin: 0 16px 10px; border-radius: 8px;\
1225        font-size: ' + (fs - 1) + 'px; display: flex; align-items: center; gap: 6px;\
1226        animation: sg-toast-in 0.3s ease;\
1227      }\
1228      .sg-bundle-toast.sg-toast-success {\
1229        background: rgba(' + rgb + ', 0.1); color: ' + pc + '; border: 1px solid rgba(' + rgb + ', 0.2);\
1230      }\
1231      .sg-bundle-toast.sg-toast-warn {\
1232        background: rgba(234,179,8,0.1); color: #a16207; border: 1px solid rgba(234,179,8,0.2);\
1233      }\
1234      .sg-bundle-toast.sg-toast-error {\
1235        background: rgba(239,68,68,0.1); color: #dc2626; border: 1px solid rgba(239,68,68,0.2);\
1236      }\
1237      .sg-bundle-toast svg { width: 14px; height: 14px; flex-shrink: 0; }\
1238      @keyframes sg-toast-in { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } }\
1239      .sg-suit-summary {\
1240        margin: 0 16px 10px; padding: 8px 12px;\
1241        background: rgba(' + rgb + ', 0.06); border: 1px dashed rgba(' + rgb + ', 0.35);\
1242        border-radius: 8px; font-size: ' + (fs - 2) + 'px; color: ' + pc + ';\
1243        display: flex; flex-wrap: wrap; gap: 6px; align-items: center;\
1244        animation: sg-toast-in 0.3s ease;\
1245      }\
1246      .sg-suit-summary-label {\
1247        font-weight: 600; margin-right: 4px; flex-shrink: 0;\
1248      }\
1249      .sg-suit-summary-tag {\
1250        background: rgba(' + rgb + ', 0.12); border-radius: 4px;\
1251        padding: 1px 7px; font-size: ' + (fs - 3) + 'px; color: ' + pc + ';\
1252        white-space: nowrap;\
1253      }\
1254      .sg-suit-summary-advisory {\
1255        font-size: ' + (fs - 3) + 'px; color: #888; font-style: italic; flex-basis: 100%;\
1256        margin-top: 2px;\
1257      }\
1258      \
1259      /* ── Complete Look zone ─────────────────────────────────────────────────── */\
1260      .sg-complete-look-zone {\
1261        margin: 0 0 0 0; border-bottom: 1px solid ' + surfaceBorder + ';\
1262        background: linear-gradient(135deg, rgba(' + rgb + ',0.06) 0%, rgba(' + rgb + ',0.02) 100%);\
1263        animation: sg-toast-in 0.3s ease;\
1264        flex-shrink: 0;\
1265      }\
1266      .sg-cl-header {\
1267        display: flex; align-items: center; justify-content: space-between;\
1268        padding: 10px 14px 6px; gap: 8px;\
1269      }\
1270      .sg-cl-title {\
1271        font-weight: 700; font-size: ' + fs + 'px; color: ' + fc + ';\
1272        display: flex; align-items: center; gap: 6px; flex: 1; min-width: 0;\
1273      }\
1274      .sg-cl-title svg { width: 15px; height: 15px; color: ' + pc + '; flex-shrink: 0; }\
1275      .sg-cl-title-text { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\
1276      .sg-cl-dismiss {\
1277        background: none; border: none; cursor: pointer; color: ' + textSec + ';\
1278        font-size: 18px; line-height: 1; padding: 0 2px; opacity: 0.6; flex-shrink: 0;\
1279        transition: opacity 0.15s;\
1280      }\
1281      .sg-cl-dismiss:hover { opacity: 1; }\
1282      .sg-cl-strip {\
1283        display: flex; gap: 8px; overflow-x: auto; padding: 0 14px 8px;\
1284        scrollbar-width: none;\
1285      }\
1286      .sg-cl-strip::-webkit-scrollbar { display: none; }\
1287      .sg-cl-item {\
1288        flex-shrink: 0; width: 72px; display: flex; flex-direction: column; gap: 4px;\
1289        cursor: default;\
1290      }\
1291      .sg-cl-thumb-wrap {\
1292        position: relative; width: 72px; height: 72px; border-radius: 10px;\
1293        overflow: hidden; border: 1px solid ' + surfaceBorder + ';\
1294        background: ' + surfaceBg + ';\
1295      }\
1296      .sg-cl-thumb {\
1297        width: 100%; height: 100%; object-fit: cover; display: block;\
1298      }\
1299      .sg-cl-thumb-placeholder {\
1300        width: 100%; height: 100%; background: rgba(' + rgb + ',0.08);\
1301        display: flex; align-items: center; justify-content: center;\
1302      }\
1303      .sg-cl-swap-overlay {\
1304        position: absolute; inset: 0; background: rgba(0,0,0,0.45);\
1305        display: flex; align-items: center; justify-content: center;\
1306        opacity: 0; transition: opacity 0.18s;\
1307      }\
1308      .sg-cl-thumb-wrap:hover .sg-cl-swap-overlay { opacity: 1; }\
1309      .sg-cl-swap-btn {\
1310        background: rgba(255,255,255,0.92); border: none; cursor: pointer;\
1311        font-size: 10px; font-weight: 700; font-family: inherit;\
1312        padding: 3px 7px; border-radius: 6px; color: #222;\
1313        letter-spacing: 0.03em; transition: background 0.12s;\
1314      }\
1315      .sg-cl-swap-btn:hover { background: #fff; }\
1316      .sg-cl-item-name {\
1317        font-size: ' + (fs - 3) + 'px; color: ' + textSec + ';\
1318        text-align: center; white-space: nowrap; overflow: hidden;\
1319        text-overflow: ellipsis; line-height: 1.2;\
1320      }\
1321      .sg-cl-footer {\
1322        display: flex; align-items: center; gap: 8px;\
1323        padding: 0 14px 10px; justify-content: space-between;\
1324      }\
1325      .sg-cl-total {\
1326        font-weight: 700; font-size: ' + (fs + 1) + 'px; color: ' + pc + ';\
1327      }\
1328      .sg-cl-add-btn {\
1329        flex: 1; max-width: 220px; padding: 8px 14px; border-radius: 10px;\
1330        background: ' + pc + '; color: ' + primaryOnBg + ';\
1331        border: none; cursor: pointer; font-size: ' + (fs - 1) + 'px;\
1332        font-weight: 700; font-family: inherit;\
1333        display: flex; align-items: center; justify-content: center; gap: 7px;\
1334        transition: opacity 0.15s;\
1335      }\
1336      .sg-cl-add-btn:hover { opacity: 0.88; }\
1337      .sg-cl-add-btn:disabled { opacity: 0.5; cursor: default; }\
1338      .sg-cl-add-btn.sg-added { background: ' + pc + '; color: ' + addedFg + '; }\
1339      .sg-cl-add-btn svg { width: 14px; height: 14px; }\
1340      .sg-cl-toast {\
1341        margin: 0 14px 8px; padding: 6px 10px; border-radius: 8px;\
1342        font-size: ' + (fs - 2) + 'px; display: flex; align-items: center; gap: 6px;\
1343        animation: sg-toast-in 0.3s ease;\
1344      }\
1345      .sg-cl-toast.sg-toast-success { background: rgba(' + rgb + ', 0.1); color: ' + pc + '; border: 1px solid rgba(' + rgb + ', 0.2); }\
1346      .sg-cl-toast.sg-toast-warn    { background: rgba(234,179,8,0.1);  color: #a16207; border: 1px solid rgba(234,179,8,0.2); }\
1347      .sg-cl-toast.sg-toast-error   { background: rgba(239,68,68,0.1);  color: #dc2626; border: 1px solid rgba(239,68,68,0.2); }\
1348      /* Mobile stacking — complete look zone below chat col on small screens */\
1349      @media (max-width: 640px) {\
1350        .sg-complete-look-zone { border-bottom: none; border-top: 1px solid ' + surfaceBorder + '; }\
1351      }\
1352      .sg-item-role {\
1353        font-size: ' + (fs - 3) + 'px; text-transform: uppercase; letter-spacing: 0.05em;\
1354        font-weight: 600; padding: 2px 6px; border-radius: 4px;\
1355        background: rgba(' + rgb + ', 0.08); color: ' + pc + ';\
1356      }\
1357      .sg-inline-items {\
1358        display: flex; flex-direction: column; gap: 8px; margin-top: 10px;\
1359      }\
1360      .sg-inline-item {\
1361        display: flex; align-items: center; gap: 8px;\
1362        padding: 8px; border-radius: 8px;\
1363        border: 1px solid ' + surfaceBorder + ';\
1364        background: ' + bg + ';\
1365      }\
1366      .sg-inline-item.sg-inline-slim {\
1367        cursor: pointer; transition: border-color 0.15s, background 0.15s;\
1368      }\
1369      .sg-inline-item.sg-inline-slim:hover {\
1370        border-color: ' + pc + '; background: rgba(' + rgb + ', 0.05);\
1371      }\
1372      .sg-inline-chevron {\
1373        color: ' + textSec + '; font-size: 18px; line-height: 1;\
1374        flex-shrink: 0; padding-right: 2px;\
1375      }\
1376      .sg-inline-item.sg-inline-slim:hover .sg-inline-chevron { color: ' + pc + '; }\
1377      .sg-inline-soldout {\
1378        font-size: ' + (fs - 3) + 'px; font-weight: 600; color: #b45309;\
1379        background: rgba(234,179,8,0.12); padding: 1px 6px; border-radius: 6px;\
1380        margin-left: 4px; text-transform: uppercase; letter-spacing: 0.04em;\
1381      }\
1382      @keyframes sg-pane-flash-kf {\
1383        0% { box-shadow: 0 0 0 3px rgba(' + rgb + ', 0.65); }\
1384        100% { box-shadow: 0 0 0 3px rgba(' + rgb + ', 0); }\
1385      }\
1386      .sg-product-card.sg-pane-flash { animation: sg-pane-flash-kf 1.6s ease 2; }\
1387      .sg-inline-item-img {\
1388        width: 44px; height: 44px; border-radius: 6px; object-fit: cover; flex-shrink: 0;\
1389        background: ' + surfaceBg + ';\
1390      }\
1391      .sg-inline-item-info {\
1392        flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px;\
1393      }\
1394      .sg-inline-item-name {\
1395        font-size: ' + (fs - 1) + 'px; font-weight: 600; color: ' + fc + ';\
1396        white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\
1397      }\
1398      .sg-inline-item-name a { color: ' + fc + '; text-decoration: none; }\
1399      .sg-inline-item-name a:hover { color: ' + pc + '; }\
1400      .sg-inline-item-price {\
1401        font-size: ' + (fs - 2) + 'px; font-weight: 600; color: ' + textSec + ';\
1402      }\
1403      .sg-inline-variant-select {\
1404        padding: 2px 4px; border-radius: 4px;\
1405        border: 1px solid ' + surfaceBorder + ';\
1406        background: ' + bg + '; color: ' + fc + ';\
1407        font-size: ' + (fs - 3) + 'px; font-family: inherit;\
1408        cursor: pointer; margin-top: 2px;\
1409        max-width: 100%;\
1410      }\
1411      .sg-inline-add-btn {\
1412        padding: 5px 10px; border-radius: 6px;\
1413        background: ' + pc + '; color: ' + primaryOnBg + ';\
1414        border: none; cursor: pointer;\
1415        font-size: ' + (fs - 2) + 'px; font-weight: 600; font-family: inherit;\
1416        display: flex; align-items: center; gap: 4px;\
1417        white-space: nowrap; flex-shrink: 0;\
1418        transition: opacity 0.15s;\
1419      }\
1420      .sg-inline-add-btn:hover { opacity: 0.85; }\
1421      .sg-inline-add-btn:disabled { opacity: 0.5; cursor: default; }\
1422      .sg-inline-add-btn svg { width: 12px; height: 12px; }\
1423      .sg-inline-add-btn.sg-added { background: ' + pc + '; color: ' + addedFg + '; }\
1424      .sg-inline-add-btn.sg-sold-out { background: #9ca3af; color: #fff; cursor: not-allowed; opacity: 0.7; }\
1425      \
1426      .sg-tab-bar {\
1427        display: none; flex-direction: row; flex-shrink: 0;\
1428        border-bottom: 1px solid ' + surfaceBorder + ';\
1429        background: ' + bg + ';\
1430      }\
1431      .sg-tab-btn {\
1432        flex: 1; padding: 10px 0; font-size: ' + fs + 'px; font-weight: 600;\
1433        border: none; background: transparent; color: ' + textSec + ';\
1434        cursor: pointer; font-family: inherit;\
1435        border-bottom: 2px solid transparent; margin-bottom: -1px;\
1436        transition: color 0.15s, border-color 0.15s;\
1437      }\
1438      .sg-tab-btn.sg-active { color: ' + pc + '; border-bottom-color: ' + pc + '; }\
1439      .sg-tab-badge {\
1440        display: inline-block; margin-left: 5px;\
1441        font-size: ' + (fs - 3) + 'px; background: rgba(' + rgb + ', 0.12);\
1442        color: ' + pc + '; padding: 1px 6px; border-radius: 10px; font-weight: 700;\
1443      }\
1444      @media (max-width: 640px) {\
1445        .sg-modal { width: 100vw; height: 100%; height: 100dvh; max-height: 100dvh; border-radius: 0; }\
1446        .sg-modal-body { flex-direction: row; }\
1447        .sg-chat-col { width: 100%; min-width: 0; border-right: none; flex: 1; }\
1448        .sg-products-col { flex: 1; min-width: 0; }\
1449        .sg-tab-hidden { display: none !important; }\
1450        #sg-browse-btn { display: flex !important; }\
1451        .sg-modal-header-actions { overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; max-width: 62vw; }\
1452        .sg-modal-header-actions::-webkit-scrollbar { display: none; }\
1453        .sg-hdr-btn { flex-shrink: 0; }\
1454        .sg-modal-header-info { min-width: 0; }\
1455        .sg-modal-header-title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }\
1456        .sg-products-grid { grid-template-columns: repeat(2, 1fr); gap: 10px; padding: 12px; }\
1457        .sg-product-img { aspect-ratio: 3/4; }\
1458        .sg-product-name { font-size: ' + (fs - 1) + 'px; }\
1459        .sg-fab { width: 56px; height: 56px; }\
1460        .sg-fab svg { width: 44px !important; height: 44px !important; }\
1461        .sg-chat-input-row { padding: 10px 14px calc(14px + env(safe-area-inset-bottom, 0px)); gap: 10px; }\
1462        .sg-chat-input {\
1463          padding: 14px 16px; font-size: 16px;\
1464          border-radius: 14px; border-width: 1.5px;\
1465          box-shadow: 0 1px 4px rgba(0,0,0,0.08);\
1466          min-height: 50px;\
1467        }\
1468        .sg-chat-send { width: 50px; height: 50px; border-radius: 14px; flex-shrink: 0; }\
1469        .sg-chat-send svg { width: 22px; height: 22px; }\
1470        .sg-mic-btn { width: 50px; height: 50px; border-radius: 14px; }\
1471      }\
1472      \
1473      /* Theme hardening — store themes style bare svg/input/button elements\
1474         (zero-width icons, square 50px inputs). Pin the widget chrome. */\
1475      .sg-modal-logo svg { width: 48px !important; height: 48px !important; display: block !important; }\
1476      .sg-hdr-btn svg, #sg-modal-close svg { width: 16px !important; height: 16px !important; display: block !important; max-width: none !important; }\
1477      .sg-chat-send svg { width: 20px !important; height: 20px !important; display: block !important; max-width: none !important; }\
1478      .sg-mic-btn svg { width: 18px !important; height: 18px !important; display: block !important; max-width: none !important; stroke: currentColor !important; }\
1479      .sg-mic-btn svg path, .sg-mic-btn svg line { stroke: currentColor !important; fill: none !important; stroke-width: 2px !important; opacity: 1 !important; }\
1480      .sg-mic-btn { width: 36px !important; height: 36px !important; min-height: 0 !important; padding: 0 !important; margin: 0 !important; display: flex !important; align-items: center !important; justify-content: center !important; }\
1481      .sg-empty-products svg { width: 48px !important; height: 48px !important; }\
1482      .sg-chat-input {\
1483        height: auto !important; min-height: 0 !important; margin: 0 !important;\
1484        padding: 10px 14px !important; border-radius: 10px !important;\
1485        border: 1px solid ' + surfaceBorder + ' !important;\
1486        font-size: ' + fs + 'px !important; line-height: 1.4 !important;\
1487        box-shadow: none !important; background: transparent !important;\
1488        text-transform: none !important; letter-spacing: normal !important;\
1489      }\
1490      .sg-chat-input:focus { border-color: ' + pc + ' !important; outline: none !important; }\
1491      .sg-chat-send {\
1492        width: 40px !important; height: 40px !important; min-height: 0 !important;\
1493        margin: 0 !important; padding: 0 !important; border: none !important;\
1494        border-radius: 10px !important; background: ' + pc + ' !important;\
1495      }\
1496      .sg-hdr-btn, #sg-modal-close { min-height: 0 !important; margin: 0 !important; padding: 0 !important; }\
1497      .sg-suggestion {\
1498        min-height: 0 !important; margin: 0 !important; padding: 7px 14px !important;\
1499        border-radius: 20px !important; line-height: 1.4 !important;\
1500        font-size: ' + (fs - 1) + 'px !important;\
1501        text-transform: none !important; letter-spacing: normal !important;\
1502      }\
1503      .sg-tab-btn { min-height: 0 !important; text-transform: none !important; letter-spacing: normal !important; margin: 0 !important; }\
1504      .sg-filter-search {\
1505        height: 30px !important; min-height: 0 !important; margin: 0 !important;\
1506        padding: 0 10px 0 28px !important;\
1507        font-size: ' + (fs - 1) + 'px !important; line-height: 28px !important;\
1508        border-radius: 15px !important; box-shadow: none !important;\
1509        text-transform: none !important; letter-spacing: normal !important;\
1510      }\
1511      .sg-btn-cart, .sg-btn-view {\
1512        min-height: 0 !important; margin: 0 !important;\
1513        line-height: 1.2 !important; white-space: nowrap !important;\
1514        text-transform: none !important; letter-spacing: normal !important;\
1515        font-size: ' + (fs - 1) + 'px !important;\
1516      }\
1517      .sg-btn-cart { display: flex !important; align-items: center !important; justify-content: center !important; gap: 6px !important; padding: 8px 10px !important; }\
1518      .sg-btn-cart svg { width: 14px !important; height: 14px !important; flex-shrink: 0 !important; display: block !important; }\
1519      .sg-cart-short { display: none; }\
1520      @media (max-width: 640px) {\
1521        .sg-chat-input { padding: 14px 16px !important; font-size: 16px !important; border-radius: 14px !important; min-height: 50px !important; border-width: 1.5px !important; }\
1522        .sg-chat-send { width: 50px !important; height: 50px !important; border-radius: 14px !important; }\
1523        .sg-chat-send svg { width: 22px !important; height: 22px !important; }\
1524        .sg-mic-btn { width: 50px !important; height: 50px !important; }\
1525        .sg-cart-full { display: none; }\
1526        .sg-cart-short { display: inline; }\
1527      }\
1528    ';
1529    // config.borderRadius (0-40, default 24) scales every corner in the sheet;
1530    // 0 = fully square. The setting was parsed but never applied until 8.39.0
1531    // (first asked for by a merchant wanting square corners).
1532    var baseR = parseInt(config.borderRadius, 10);
1533    if (!isNaN(baseR) && baseR !== 24) {
1534      var rk = baseR / 24;
1535      style.textContent = style.textContent.replace(/border-radius:\s
1535*([0-9.]+)px/g, function(_, px) {
1536        return "border-radius: " + Math.round(parseFloat(px) * rk) + "px";
1537      });
1538      if (baseR === 0) {
1539        // Circular buttons go square too; tiny status dots stay dots.
1540        style.textContent += ' .sg-fab, #sg-modal-close, .sg-hdr-btn, .sg-mic-btn, .sg-chat-send, .sg-lb-arrow, #sg-capture-prompt input, #sg-capture-prompt button { border-radius: 0 !important; }';
1541      }
1542    }
1543    document.head.appendChild(style);
1544  }
1545
1546  var searchSelectors = [
1547    'form[action="/search"] input[type="search"]',
1548    'form[action="/search"] input[type="text"]',
1549    'form[action="/search"] input[name="q"]',
1550    'input[name="q"][type="search"]',
1551    'input[name="q"]',
1552    '.search-modal__input',
1553    '#Search-In-Modal',
1554    '.search__input',
1555    'predictive-search input',
1556    'search-modal input',
1557    '.header__search input',
1558    '.site-header__search input',
1559    '#HeaderSearchInput',
1560    '[data-search-input]',
1561    '.search-bar input',
1562    '.search-form input',
1563    '#search-input',
1564    '.header input[type="search"]',
1565    '.header input[name="q"]',
1566  ];
1567  var hookedInputs = [];
1568
1569  function hookSingleInput(input) {
1570    for (var k = 0; k < hookedInputs.length; k++) {
1571      if (hookedInputs[k] === input) return;
1572    }
1573    hookedInputs.push(input);
1574
1575    var searchForm = input.closest("form");
1576    if (searchForm) {
1577      searchForm.addEventListener("submit", function(e) {
1578        var val = input.value.trim();
1579        if (!val) return;
1580        e.preventDefault();
1581        e.stopPropagation();
1582        openModal(val);
1583      }, true);
1584    }
1585    input.addEventListener("keydown", function(e) {
1586      if (e.key === "Enter") {
1587        var val = input.value.trim();
1588        if (val) {
1589          e.preventDefault();
1590          e.stopPropagation();
1591          openModal(val);
1592        }
1593      }
1594    }, true);
1595  }
1596
1597  function findAndHookSearchInputs() {
1598    for (var i = 0; i < searchSelectors.length; i++) {
1599      var found = document.querySelectorAll(searchSelectors[i]);
1600      for (var j = 0; j < found.length; j++) {
1601        hookSingleInput(found[j]);
1602      }
1603    }
1604  }
1605
1606  function hookSearchBox() {
1607    findAndHookSearchInputs();
1608
1609    var retries = 0;
1610    var retryInterval = setInterval(function() {
1611      findAndHookSearchInputs();
1612      retries++;
1613      if (retries >= 10) clearInterval(retryInterval);
1614    }, 1000);
1615
1616    if (typeof MutationObserver !== "undefined") {
1617      var observer = new MutationObserver(function() {
1618        findAndHookSearchInputs();
1619      });
1620      observer.observe(document.body, { childList: true, subtree: true });
1621    }
1622  }
1623
1624  function hasAutoTriggered() {
1625    try {
1626      var key = "sg_auto_triggered_" + shopDomain;
1627      return sessionStorage.getItem(key) === "1";
1628    } catch(e) { return false; }
1629  }
1630
1631  function markAutoTriggered() {
1632    try {
1633      var key = "sg_auto_triggered_" + shopDomain;
1634      sessionStorage.setItem(key, "1");
1635    } catch(e) {}
1636    autoTriggeredThisSession = true;
1637  }
1638
1639  function isCartOrCheckoutPage() {
1640    var path = (window.location.pathname || "").toLowerCase();
1641    return path === "/cart" || path.startsWith("/cart/") ||
1642           path === "/checkout" || path.startsWith("/checkout/") ||
1643           path.startsWith("/a/checkout") || path.startsWith("/orders/");
1644  }
1645
1646  function autoOpenOnce() {
1647    if (autoTriggeredThisSession || hasAutoTriggered() || isModalOpen) return;
1648    if (isCartOrCheckoutPage()) return; // never auto-open on cart or checkout
1649    markAutoTriggered();
1650    trackEvent("assistant_auto_triggered", { trigger: detectTriggerReason() });
1651    openModal("");
1652  }
1653
1654  function detectTriggerReason() {
1655    var path = window.location.pathname || "";
1656    if (/\/products\//.test(path)) return "product_page";
1657    if (/\/collections\//.test(path)) return "collection_dwell";
1658    return "search_activity";
1659  }
1660
1661  function isProductPage() {
1662    var path = window.location.pathname || "";
1663    return /\/products\//.test(path);
1664  }
1665
1666  function isCategoryPage() {
1667    var path = (window.location.pathname || "").toLowerCase();
1668    if (!/\/collections\//.test(path)) return false;
1669    var categoryKeywords = [
1670      "suit", "suits", "shirt", "shirts", "blazer", "blazers",
1671      "formal"
1672    ];
1673    for (var i = 0; i < categoryKeywords.length; i++) {
1674      if (path.indexOf(categoryKeywords[i]) !== -1) return true;
1675    }
1676    return false;
1677  }
1678
1679  function initSearchTrigger() {
1680    if (isProductPage()) {
1681      setTimeout(function() { autoOpenOnce(); }, 2000);
1682      return;
1683    }
1684
1685    if (isCategoryPage()) {
1686      var dwellTimer = setTimeout(function() { autoOpenOnce(); }, 20000);
1687      var cancelDwell = function() {
1688        clearTimeout(dwellTimer);
1689        window.removeEventListener("beforeunload", cancelDwell);
1690      };
1691      window.addEventListener("beforeunload", cancelDwell);
1692    }
1693  }
1694
1695  function createFAB() {
1696    if (document.getElementById("sg-fab")) return;
1697    var useBell = config.activationMode === "bell_icon" || config.activationMode === "both";
1698    var fab = document.createElement("button");
1699    fab.id = "sg-fab";
1700    fab.className = "sg-fab" + (useBell ? " sg-bell" : "");
1701    fab.title = "Ask " + config.stylistName;
1702    fab.setAttribute("data-testid", "button-style-guru-fab");
1703    fab.innerHTML = (useBell ? bellIcon() : sparklesIcon()) + '<span class="sg-fab-badge"></span>';
1704
1705    var bPos = config.bellPosition || "bottom_left";
1706    fab.style.position = "fixed";
1707    fab.style.top = "auto";
1708    fab.style.bottom = "auto";
1709    fab.style.left = "auto";
1710    fab.style.right = "auto";
1711    fab.style.transform = "";
1712    if (bPos === "top_right")        { fab.style.top = "24px";   fab.style.right = "24px"; }
1713    else if (bPos === "top_left")    { fab.style.top = "24px";   fab.style.left  = "24px"; }
1714    else if (bPos === "bottom_left") { fab.style.bottom = "90px"; fab.style.left = "24px"; }
1715    else if (bPos === "bottom_center"){ fab.style.bottom = "90px"; fab.style.left = "50%"; fab.style.transform = "translateX(-50%)"; }
1716    else                             { fab.style.bottom = "90px"; fab.style.right = "24px"; }
1717
1718    // Soft two-tone door chime on open — a click is a user gesture, so audio
1719    // is permitted. Synthesised (no asset); once per session.
1720    function playOpenChime() {
1721      if (config.bellOpenChime !== true) return;
1722      try {
1723        if (sessionStorage.getItem("sg-chime-done") === "1") return;
1724        sessionStorage.setItem("sg-chime-done", "1");
1725      } catch (e) {}
1726      try {
1727        var AC = window.AudioContext || window.webkitAudioContext;
1728        if (!AC) return;
1729        var ctx = new AC();
1730        [[830, 0], [1245, 0.09]].forEach(function (t) {
1731          var osc = ctx.createOscillator();
1732          var gain = ctx.createGain();
1733          osc.type = "sine";
1734          osc.frequency.value = t[0];
1735          gain.gain.setValueAtTime(0.0001, ctx.currentTime + t[1]);
1736          gain.gain.exponentialRampToValueAtTime(0.12, ctx.currentTime + t[1] + 0.02);
1737          gain.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime + t[1] + 0.7);
1738          osc.connect(gain); gain.connect(ctx.destination);
1739          osc.start(ctx.currentTime + t[1]);
1740          osc.stop(ctx.currentTime + t[1] + 0.8);
1741        });
1742        setTimeout(function () { try { ctx.close(); } catch (e) {} }, 1500);
1743      } catch (e) {}
1744    }
1745
1746    // Silent visual re-ring: replay the bell-ring animation every ~40s until
1747    // the customer clicks, max 3 times, once per session. Motion draws the
1748    // eye without the autoplay-audio fight (browsers block sound pre-gesture).
1749    var reringTimer = null;
1750    function scheduleReRing() {
1751      if (config.bellReRing === false) return;
1752      try { if (sessionStorage.getItem("sg-rering-done") === "1") return; } catch (e) {}
1753      var count = 0;
1754      reringTimer = setInterval(function () {
1755        var fabEl = document.getElementById("sg-fab");
1756        if (!fabEl || fabEl.className.indexOf("sg-fab-waiting") !== -1) return;
1757        count++;
1758        fabEl.className += " sg-fab-rering";
1759        setTimeout(function () { fabEl.className = fabEl.className.replace(" sg-fab-rering", ""); }, 800);
1760        if (count >= 3) stopReRing();
1761      }, 40000);
1762    }
1763    function stopReRing() {
1764      if (reringTimer) { clearInterval(reringTimer); reringTimer = null; }
1765      try { sessionStorage.setItem("sg-rering-done", "1"); } catch (e) {}
1766    }
1767
1768    // ── Sizing-hesitation teaser ─────────────────────────────────────────
1769    // Product page, ~30s dwell, no add-to-cart, widget never opened: a small
1770    // dismissible bubble by the bell offers sizing help. Once per session.
1771    function scheduleSizingTeaser(fabEl, bPosition) {
1772      if (config.sizingTeaser === false) return;
1773      if (!isProductPage()) return;
1774      try { if (sessionStorage.getItem("sg-teaser-done") === "1") return; } catch (e) {}
1775      var cancelled = false;
1776      function cancel() { cancelled = true; }
1777      // An add-to-cart means they weren't hesitating — stand down.
1778      document.addEventListener("click", function (e) {
1779        var t = e.target && e.target.closest && e.target.closest('form[action*="/cart/add"] [type="submit"], form[action*="/cart/add"] button, [name="add"]');
1780        if (t) cancel();
1781      }, true);
1782      setTimeout(function () {
1783        if (cancelled || isModalOpen) return;
1784        try { if (sessionStorage.getItem("sg-teaser-done") === "1") return; } catch (e) {}
1785        if (!document.getElementById("sg-fab")) return;
1786        try { sessionStorage.setItem("sg-teaser-done", "1"); } catch (e) {}
1787        var pc = config.primaryColor || "#C6A85B";
1788        var bubble = document.createElement("div");
1789        bubble.id = "sg-sizing-teaser";
1790        var vertical = (bPosition === "top_right" || bPosition === "top_left") ? "top: 96px;" : "bottom: 160px;";
1791        var horiz = (bPosition === "top_left" || bPosition === "bottom_left") ? "left: 20px;" : (bPosition === "bottom_center" ? "left: 50%; transform: translateX(-50%);" : "right: 20px;");
1792        bubble.style.cssText = "position:fixed;" + vertical + horiz + "z-index:2147483645;max-width:240px;" +
1793          "background:#fff;color:#1A2332;border:1px solid " + pc + ";border-radius:14px;" +
1794          "box-shadow:0 6px 24px rgba(0,0,0,0.16);padding:12px 34px 12px 14px;" +
1795          "font:500 13px/1.4 " + (config.fontFamily || "inherit") + ";cursor:pointer;" +
1796          "opacity:0;transition:opacity 0.4s ease, translate 0.4s ease;";
1797        bubble.textContent = T("teaserSizing");
1798        var x = document.createElement("button");
1799        x.setAttribute("aria-label", "Dismiss");
1800        x.innerHTML = "&#10005;";
1801        x.style.cssText = "position:absolute;top:6px;right:8px;border:none;background:none;color:#9aa0ab;font-size:11px;cursor:pointer;padding:2px;";
1802        x.onclick = function (e) { e.stopPropagation(); bubble.remove(); trackEvent("teaser_dismissed", {}); };
1803        bubble.appendChild(x);
1804        bubble.onclick = function () {
1805          bubble.remove();
1806          trackEvent("teaser_clicked", {});
1807          openModal(T("starterSize"));
1808        };
1809        document.body.appendChild(bubble);
1810        requestAnimationFrame(function () { bubble.style.opacity = "1"; });
1811        trackEvent("teaser_shown", {});
1812        // quietly retire if ignored — a lingering nag is worse than none
1813        setTimeout(function () { if (bubble.parentNode) { bubble.style.opacity = "0"; setTimeout(function(){ bubble.remove(); }, 500); } }, 25000);
1814      }, 30000);
1815    }
1816
1817    fab.onclick = function() {
1818      try { var tz = document.getElementById("sg-sizing-teaser"); if (tz) tz.remove(); } catch (e) {}
1819      stopReRing();
1820      playOpenChime();
1821      // openModal() handles the voice greeting via onUserGesture() — playing
1822      // it here as well doubles the audio (echo).
1823      openModal("");
1824    };
1825    scheduleReRing();
1826    scheduleSizingTeaser(fab, bPos);
1827
1828    // Delayed entrance: on the first page of a session the bell holds back
1829    // (~8s) so cookie/newsletter popups get their moment, then arrives with a
1830    // spring + ring that draws the eye. Later pages show it immediately.
1831    var seenKey = "sg-bell-seen";
1832    var alreadySeen = false;
1833    try { alreadySeen = sessionStorage.getItem(seenKey) === "1"; } catch (e) {}
1834    if (!alreadySeen) {
1835      fab.className += " sg-fab-waiting";
1836      setTimeout(function () {
1837        fab.className = fab.className.replace(" sg-fab-waiting", "");
1838        // bottom_center positions with an inline translateX that the scale
1839        // entrance would override — fade there instead.
1840        fab.className += (bPos === "bottom_center") ? " sg-fab-fade-enter" : " sg-fab-enter";
1841        try { sessionStorage.setItem(seenKey, "1"); } catch (e) {}
1842        trackEvent("bell_shown", { delayed: true });
1843      }, 8000);
1844    } else {
1845      trackEvent("bell_shown", { delayed: false });
1846    }
1847    document.body.appendChild(fab);
1848  }
1849
1850  function createModal() {
1851    if (document.getElementById("sg-overlay")) return;
1852
1853    var fc = config.fontColor || "#333";
1854    var pc = config.primaryColor || "#C6A85B";
1855    var sendBtnColor = isLightColor(pc) ? "#111" : "#fff";
1856    var hdrIconColor = isLightColor(config.backgroundColor || "#fff") ? "#555" : "#ccc";
1857
1858    var isVoiceMode = config.interactionMode === "voice_stylist";
1859    audioEnabled = isVoiceMode;
1860
1861    var micHtml = (isVoiceMode && hasSpeechRecognition())
1862      ? '<button class="sg-mic-btn" id="sg-modal-mic" title="Speak" data-testid="button-mic">' + micIcon() + '</button>'
1863      : '';
1864
1865    var speakerHtml = isVoiceMode
1866      ? '<button class="sg-hdr-btn' + (audioEnabled ? ' sg-active' : '') + '" id="sg-speaker-toggle" title="' + (audioEnabled ? 'Audio on' : 'Audio off') + '" data-testid="button-speaker-toggle">' + speakerIcon(hdrIconColor) + '</button>'
1867      : '';
1868
1869    // Live voice (ElevenLabs Speech Engine pilot): real spoken conversation —
1870    // mic, turn-taking and voice handled by the engine, our agent as the brain.
1871    var liveVoiceHtml = (config.speechEngine && PROXY_PATH)
1872      ? '<button class="sg-hdr-btn" id="sg-live-btn" title="Talk to the stylist" data-testid="button-live-voice">
1872<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><line x1="2" y1="6" x2="2" y2="10" stroke="' + hdrIconColor + '" stroke-width="1.6" stroke-linecap="round"/><line x1="5" y1="4" x2="5" y2="12" stroke="' + hdrIconColor + '" stroke-width="1.6" stroke-linecap="round"/><line x1="8" y1="2" x2="8" y2="14" stroke="' + hdrIconColor + '" stroke-width="1.6" stroke-linecap="round"/><line x1="11" y1="4" x2="11" y2="12" stroke="' + hdrIconColor + '" stroke-width="1.6" stroke-linecap="round"/><line x1="14" y1="6" x2="14" y2="10" stroke="' + hdrIconColor + '" stroke-width="1.6" stroke-linecap="round"/></svg></button>'
1873      : '';
1874
1875    var sizeFinderHtml = config.fitcoreEnabled
1876      ? '<button class="sg-hdr-btn" id="sg-size-finder-btn" title="Find my size with FitCore" data-testid="button-size-finder">' + rulerIcon(hdrIconColor) + '</button>'
1877      : '';
1878
1879    // Mobile-only header toggle between the chat pane and the browse/filter
1880    // pane — same treatment as the FitCore size-finder link. The icon flips
1881    // to a chat bubble while the products pane is showing.
1882    var browseGridSvg = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="1.5" y="1.5" width="5" height="5" rx="1" stroke="' + hdrIconColor + '" stroke-width="1.5"/><rect x="9.5" y="1.5" width="5" height="5" rx="1" stroke="' + hdrIconColor + '" stroke-width="1.5"/><rect x="1.5" y="9.5" width="5" height="5" rx="1" stroke="' + hdrIconColor + '" stroke-width="1.5"/><rect x="9.5" y="9.5" width="5" height="5" rx="1" stroke="' + hdrIconColor + '" stroke-width="1.5"/></svg>';
1883    var browseChatSvg = '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M14 10.5a1.5 1.5 0 0 1-1.5 1.5H5l-3 3V3.5A1.5 1.5 0 0 1 3.5 2h9A1.5 1.5 0 0 1 14 3.5v7z" stroke="' + hdrIconColor + '" stroke-width="1.5" stroke-linejoin="round"/></svg>';
1884    var browseBtnHtml = '<button class="sg-hdr-btn" id="sg-browse-btn" title="Browse products" data-testid="button-browse"><span id="sg-browse-ic-grid" style="display:flex">' + browseGridSvg + '</span><span id="sg-browse-ic-chat" style="display:none">' + browseChatSvg + '</span><span class="sg-hdr-badge" id="sg-browse-count" style="display:none"></span></button>';
1885
1886    var overlay = document.createElement("div");
1887    overlay.id = "sg-overlay";
1888    overlay.className = "sg-overlay";
1889    overlay.innerHTML = '\
1890      <div class="sg-modal" id="sg-modal" data-testid="modal-stylist">\
1891        <div class="sg-modal-header">\
1892          <div class="sg-modal-logo">' + sparklesIcon() + '</div>\
1893          <div class="sg-modal-header-info">\
1894            <div class="sg-modal-header-title">' + escapeHtml(config.stylistName) + '</div>\
1895            <div class="sg-modal-header-sub">' + (config.storeVertical && config.storeVertical !== "fashion" ? "Your personal AI shopping assistant" : "Your personal AI style assistant") + '</div>\
1896          </div>\
1897          <div class="sg-modal-header-actions">\
1898            ' + liveVoiceHtml + '\
1899            ' + speakerHtml + '\
1900            ' + sizeFinderHtml + '\
1901            ' + browseBtnHtml + '\
1902            <button class="sg-hdr-btn" id="sg-message-us" title="Send us a message" data-testid="button-message-us">' + mailIcon(hdrIconColor) + '</button>\
1903            <button class="sg-hdr-btn" id="sg-modal-close" title="Close" data-testid="button-close-modal">' + closeIcon(hdrIconColor) + '</button>\
1904          </div>\
1905        </div>\
1906        <div class="sg-tab-bar" id="sg-tab-bar">\
1907          <button class="sg-tab-btn sg-active" id="sg-tab-chat" data-testid="tab-chat">' + T("chat") + '</button>\
1908          <button class="sg-tab-btn" id="sg-tab-outfits" data-testid="tab-outfits">' + T("outfits") + '<span class="sg-tab-badge" id="sg-tab-count" style="display:none"></span></button>\
1909        </div>\
1910        <div class="sg-modal-body">\
1911          <div class="sg-chat-col" id="sg-chat-col">\
1912            <div class="sg-chat-messages" id="sg-messages" data-testid="chat-messages"></div>\
1913            <div class="sg-chat-input-row">\
1914              <input type="text" class="sg-chat-input" id="sg-chat-input" placeholder="' + (isVoiceMode && hasSpeechRecognition() ? T('micPlaceholder') : T('askPlaceholder')) + '" data-testid="input-chat" />\
1915              ' + micHtml + '\
1916              <button class="sg-chat-send" id="sg-chat-send" disabled data-testid="button-send">' + sendIcon(sendBtnColor) + '</button>\
1917            </div>\
1918            ' + (config.hideBranding ? '' : '<div class="sg-powered"><a href="https://www.styleguru.io" target="_blank" rel="noopener" data-testid="link-powered-by">Powered by <strong>Style Guru</strong></a></div>') + '\
1919          </div>\
1920          <div class="sg-products-col" id="sg-products-col">\
1921            <div class="sg-products-header">\
1922              <span id="sg-products-header-label">' + T("browse") + '</span>\
1923              <span class="sg-products-header-count" id="sg-product-count"></span>\
1924            </div>\
1925            <div class="sg-filter-bar" id="sg-filter-bar">\
1926              <div class="sg-filter-top-row">\
1927                <span class="sg-filter-label">Filter by</span>\
1928                <div class="sg-filter-search-wrap">\
1929                  <svg class="sg-filter-search-icon" width="13" height="13" viewBox="0 0 13 13" fill="none"><circle cx="5.5" cy="5.5" r="4" stroke="currentColor" stroke-width="1.5"/><line x1="8.5" y1="8.5" x2="12" y2="12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>\
1930                  <input class="sg-filter-search" id="sg-filter-search" type="text" placeholder="' + T("searchProducts") + '" autocomplete="off" />\
1931                </div>\
1932                <button class="sg-filter-clear-btn" id="sg-filter-clear" type="button">Clear</button>\
1933              </div>\
1934              <div class="sg-filter-chips" id="sg-filter-chips">\
1935                <select class="sg-filter-select" id="sg-filter-category">\
1936                  <option value="">' + T("category") + '</option>\
1937                </select>\
1938                <select class="sg-filter-select" id="sg-filter-colour">\
1939                  <option value="">' + T("colour") + '</option>\
1940                  <option value="black">Black</option>\
1941                  <option value="white">White</option>\
1942                  <option value="navy">Navy</option>\
1943                  <option value="blue">Blue</option>\
1944                  <option value="charcoal">Charcoal</option>\
1945                  <option value="grey">Grey</option>\
1946                  <option value="brown">Brown</option>\
1947                  <option value="tan">Tan</option>\
1948                  <option value="beige">Beige</option>\
1949                  <option value="cream">Cream</option>\
1950                  <option value="burgundy">Burgundy</option>\
1951                  <option value="green">Green</option>\
1952                  <option value="pink">Pink</option>\
1953                  <option value="red">Red</option>\
1954                </select>\
1955                <select class="sg-filter-select" id="sg-filter-fabric" style="display:none">\
1956                  <option value="">Fabric</option>\
1957                </select>\
1958                <select class="sg-filter-select" id="sg-filter-size">\
1959                  <option value="">' + T("size") + '</option>\
1960                </select>\
1961                <select class="sg-filter-select" id="sg-filter-price">\
1962                  <option value="">' + T("price") + '</option>\
1963                  <option value="0-100">Under £100</option>\
1964                  <option value="100-250">£100 – £250</option>\
1965                  <option value="250-500">£250 – £500</option>\
1966                  <option value="500-1000">£500 – £1,000</option>\
1967                  <option value="1000-99999">£1,000+</option>\
1968                </select>\
1969                <select class="sg-filter-select" id="sg-filter-sort">\
1970                  <option value="">Sort</option>\
1971                  <option value="price_asc">Price: Low to High</option>\
1972                  <option value="price_desc">Price: High to Low</option>\
1973                  <option value="name_asc">Name: A–Z</option>\
1974                  <option value="new">Newest First</option>\
1975                </select>\
1976                <button class="sg-filter-toggle" id="sg-filter-instock" type="button" data-active="false">In stock only</button>\
1977              </div>\
1978            </div>\
1979            <div class="sg-products-col-inner">\
1980              <div class="sg-products-fade-top" id="sg-fade-top"></div>\
1981              <div class="sg-products-scroll" id="sg-products-scroll">\
1982                <div class="sg-complete-look-zone" id="sg-complete-look-zone" style="display:none"></div>\
1983                <div class="sg-products-grid" id="sg-products-grid" data-testid="products-grid"></div>\
1984              </div>\
1985              <div class="sg-products-fade-bottom" id="sg-fade-bottom"></div>\
1986            </div>\
1987            <div class="sg-img-lightbox" id="sg-img-lightbox" role="dialog" aria-modal="true">\
1988              <div class="sg-lb-topbar">\
1989                <button class="sg-lb-close" id="sg-lb-close" aria-label="Back to products">&#8592; Back</button>\
1990                <span class="sg-lb-topname" id="sg-lb-name"></span>\
1991                <span class="sg-lb-counter" id="sg-lb-counter"></span>\
1992              </div>\
1993              <div class="sg-lb-imgarea">\
1994                <button class="sg-lb-arrow sg-lb-prev" id="sg-lb-prev" aria-label="Previous image">&#8592;</button>\
1995                <img id="sg-lb-img" src="" alt="" />\
1996                <button class="sg-lb-arrow sg-lb-next" id="sg-lb-next" aria-label="Next image">&#8594;</button>\
1997              </div>\
1998            </div>\
1999          </div>\
2000        </div>\
2001      </div>\
2002    ';
2003
2004    document.body.appendChild(overlay);
2005
2006    overlay.addEventListener("click", function(e) {
2007      if (e.target === overlay) closeModal();
2008    });
2009
2010    document.getElementById("sg-modal-close").onclick = closeModal;
2011    var messageUsBtn = document.getElementById("sg-message-us");
2012    if (messageUsBtn) messageUsBtn.onclick = function() { showHandoffForm(null, ""); };
2013    var sizeFinderBtn = document.getElementById("sg-size-finder-btn");
2014    if (sizeFinderBtn) sizeFinderBtn.onclick = function() { switchTab("chat"); openSizeFinderUI(); };
2015    var liveBtn = document.getElementById("sg-live-btn");
2016    if (liveBtn) liveBtn.onclick = function() { sgLiveConv ? sgStopLiveVoice() : sgStartLiveVoice(); };
2017    var browseHdrBtn = document.getElementById("sg-browse-btn");
2018    if (browseHdrBtn) browseHdrBtn.onclick = function() { switchTab(activeTab === "products" ? "chat" : "products"); };
2019
2020    // Image lightbox with left/right navigation
2021    var lightbox = document.getElementById("sg-img-lightbox");
2022    var lbImg = document.getElementById("sg-lb-img");
2023    var lbName = document.getElementById("sg-lb-name");
2024    var lbClose = document.getElementById("sg-lb-close");
2025    var lbPrev = document.getElementById("sg-lb-prev");
2026    var lbNext = document.getElementById("sg-lb-next");
2027    var lbCounter = document.getElementById("sg-lb-counter");
2028
2029    var lbImages = []; // [{src, name}] — all images currently in the panel
2030    var lbIndex = 0;   // which one is showing
2031
2032    function getLightboxImages() {
2033      var triggers = document.querySelectorAll("#sg-products-col .sg-img-zoom-trigger[data-img]");
2034      var imgs = [];
2035      for (var i = 0; i < triggers.length; i++) {
2036        var src = triggers[i].getAttribute("data-img");
2037        var name = triggers[i].getAttribute("data-name") || "";
2038        if (src) imgs.push({ src: src, name: name });
2039      }
2040      return imgs;
2041    }
2042
2043    function renderLightboxAt(idx) {
2044      if (!lbImages.length) return;
2045      idx = Math.max(0, Math.min(idx, lbImages.length - 1));
2046      lbIndex = idx;
2047      lbImg.src = lbImages[idx].src;
2048      lbImg.alt = lbImages[idx].name;
2049      lbName.textContent = lbImages[idx].name;
2050      if (lbImages.length > 1) {
2051        lbCounter.textContent = (idx + 1) + " / " + lbImages.length;
2052        lbCounter.style.display = "";
2053        lbPrev.style.display = "";
2054        lbNext.style.display = "";
2055        lbPrev.disabled = idx === 0;
2056        lbNext.disabled = idx === lbImages.length - 1;
2057      } else {
2058        lbCounter.style.display = "none";
2059        lbPrev.style.display = "none";
2060        lbNext.style.display = "none";
2061      }
2062    }
2063
2064    function openLightbox(src, name) {
2065      lbImages = getLightboxImages();
2066      // Find the index of the clicked image
2067      var foundIdx = 0;
2068      for (var i = 0; i < lbImages.length; i++) {
2069        if (lbImages[i].src === src) { foundIdx = i; break; }
2070      }
2071      lightbox.classList.add("sg-lb-open");
2072      renderLightboxAt(foundIdx);
2073    }
2074
2075    function closeLightbox() {
2076      lightbox.classList.remove("sg-lb-open");
2077      lbImg.src = "";
2078    }
2079
2080    lbClose.onclick = function(e) { e.stopPropagation(); closeLightbox(); };
2081    lightbox.addEventListener("click", function(e) { if (e.target === lightbox) closeLightbox(); });
2082    lbPrev.onclick = function(e) { e.stopPropagation(); renderLightboxAt(lbIndex - 1); };
2083    lbNext.onclick = function(e) { e.stopPropagation(); renderLightboxAt(lbIndex + 1); };
2084
2085    // Delegate click on image zoom triggers inside products col
2086    document.getElementById("sg-products-col").addEventListener("click", function(e) {
2087      var trigger = e.target.closest(".sg-img-zoom-trigger");
2088      if (!trigger) return;
2089      var imgSrc = trigger.getAttribute("data-img");
2090      var imgName = trigger.getAttribute("data-name");
2091      if (imgSrc) {
2092        e.preventDefault();
2093        openLightbox(imgSrc, imgName);
2094      }
2095    });
2096
2097    var chatInput = document.getElementById("sg-chat-input");
2098    var chatSend = document.getElementById("sg-chat-send");
2099
2100    chatInput.addEventListener("input", function() {
2101      chatSend.disabled = !chatInput.value.trim() || isLoading;
2102    });
2103
2104    chatInput.addEventListener("keydown", function(e) {
2105      if (e.key === "Enter" && !e.shiftKey) {
2106        e.preventDefault();
2107        sendMessage();
2108      }
2109    });
2110
2111    document.addEventListener("keydown", function(e) {
2112      if (!lightbox.classList.contains("sg-lb-open")) return;
2113      if (e.key === "Escape") { closeLightbox(); }
2114      else if (e.key === "ArrowLeft") { e.preventDefault(); renderLightboxAt(lbIndex - 1); }
2115      else if (e.key === "ArrowRight") { e.preventDefault(); renderLightboxAt(lbIndex + 1); }
2116    });
2117
2118    chatSend.onclick = sendMessage;
2119
2120    // Scroll-fade indicators for the products pane (outfit zone + grid scroll together)
2121    var sgGrid = document.getElementById("sg-products-grid");
2122    var sgScroll = document.getElementById("sg-products-scroll");
2123    var sgFadeTop = document.getElementById("sg-fade-top");
2124    var sgFadeBottom = document.getElementById("sg-fade-bottom");
2125
2126    updateScrollFade = function() {
2127      if (!sgScroll || !sgFadeTop || !sgFadeBottom) return;
2128      var atTop = sgScroll.scrollTop < 8;
2129      var atBottom = sgScroll.scrollTop + sgScroll.clientHeight >= sgScroll.scrollHeight - 8;
2130      sgFadeTop.classList.toggle("sg-fade-visible", !atTop);
2131      sgFadeBottom.classList.toggle("sg-fade-visible", !atBottom && sgScroll.scrollHeight > sgScroll.clientHeight);
2132    };
2133
2134    if (sgScroll) {
2135      sgScroll.addEventListener("scroll", updateScrollFade, { passive: true });
2136    }
2137
2138    // ─── Deterministic filter pane ─────────────────────────────────────────────
2139    var filterState = { query: "", category: "", colour: "", size: "", priceRange: "", sort: "", inStockOnly: false, onSale: false, fabric: "" };
2140    // Merchant-configured landing view: Browse opens pre-filtered (category/
2141    // tags/sort set in the dashboard). The filter UI reflects it, and Clear
2142    // filters returns the shopper to the full catalogue.
2143    var landingTags = [];
2144    // When the AI applies a grid filter that matches nothing, relax it one
2145    // constraint at a time (query -> category -> size) rather than showing an
2146    // empty grid under a message that promised products.
2147    var aiFilterRelax = [];
2148    if (config.catalogLanding && typeof config.catalogLanding === "object") {
2149      if (config.catalogLanding.category) filterState.category = String(config.catalogLanding.category);
2150      if (config.catalogLanding.sort) filterState.sort = String(config.catalogLanding.sort);
2151      if (config.catalogLanding.tags && config.catalogLanding.tags.length) landingTags = config.catalogLanding.tags;
2152    }
2153    var filterDebounceTimer = null;
2154    var filterFacetsLoaded = false;
2155    var filterRequestSeq = 0;
2156    var currentCompleteLook = null;
2157
2158    var filterSearch = document.getElementById("sg-filter-search");
2159    var filterCategory = document.getElementById("sg-filter-category");
2160    var filterColour = document.getElementById("sg-filter-colour");
2161    var filterFabric = document.getElementById("sg-filter-fabric");
2162    var filterSize = document.getElementById("sg-filter-size");
2163    var filterPrice = document.getElementById("sg-filter-price");
2164    var filterSort = document.getElementById("sg-filter-sort");
2165    var filterInstock = document.getElementById("sg-filter-instock");
2166    var filterClear = document.getElementById("sg-filter-clear");
2167    var headerLabel = document.getElementById("sg-products-header-label");
2168
2169    // Force inline styles on select elements to guarantee they override any theme CSS
2170    var filterSelects = [filterCategory, filterColour, filterSize, filterPrice, filterS
2170ort];
2171    var selectFc = config.fontColor || "#333333";
2172    var selectBg = config.backgroundColor || "#ffffff";
2173    var selectBorder = isLightColor(selectBg) ? "rgba(0,0,0,0.15)" : "rgba(255,255,255,0.2)";
2174    for (var fsi = 0; fsi < filterSelects.length; fsi++) {
2175      var fsel = filterSelects[fsi];
2176      if (!fsel) continue;
2177      fsel.style.setProperty("color", selectFc, "important");
2178      fsel.style.setProperty("background-color", selectBg, "important");
2179      fsel.style.setProperty("border-color", selectBorder, "important");
2180      fsel.style.setProperty("width", "auto", "important");
2181      fsel.style.setProperty("max-width", "140px", "important");
2182      fsel.style.setProperty("height", "26px", "important");
2183      fsel.style.setProperty("min-height", "26px", "important");
2184      fsel.style.setProperty("max-height", "26px", "important");
2185      fsel.style.setProperty("line-height", "24px", "important");
2186      fsel.style.setProperty("padding", "0 22px 0 10px", "important");
2187      fsel.style.setProperty("margin", "0", "important");
2188      fsel.style.setProperty("box-sizing", "border-box", "important");
2189      fsel.style.setProperty("appearance", "none", "important");
2190      fsel.style.setProperty("-webkit-appearance", "none", "important");
2191      fsel.style.setProperty("border-radius", "13px", "important");
2192      fsel.style.setProperty("padding", "0 22px 0 10px", "important");
2193      fsel.style.setProperty("font-size", (parseInt(config.fontSize || "14") - 2) + "px", "important");
2194      fsel.style.setProperty("flex-shrink", "0", "important");
2195      fsel.style.setProperty("flex-grow", "0", "important");
2196      fsel.style.setProperty("cursor", "pointer", "important");
2197      fsel.style.setProperty("outline", "none", "important");
2198      fsel.style.setProperty("background-image", "url(\"data:image/svg+xml;charset=utf-8,%3Csvg xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27 width%3D%2710%27 height%3D%276%27 viewBox%3D%270 0 10 6%27%3E%3Cpath d%3D%27M1 1l4 4 4-4%27 stroke%3D%27%23777%27 stroke-width%3D%271.5%27 fill%3D%27none%27 stroke-linecap%3D%27round%27%2F%3E%3C%2Fsvg%3E\")", "important");
2199      fsel.style.setProperty("background-repeat", "no-repeat", "important");
2200      fsel.style.setProperty("background-position", "right 6px center", "important");
2201      fsel.style.setProperty("box-sizing", "border-box", "important");
2202    }
2203
2204    function isFilterActive() {
2205      return !!(filterState.query || filterState.category || filterState.colour || filterState.size || filterState.priceRange || filterState.inStockOnly || filterState.onSale || filterState.fabric);
2206    }
2207
2208    function updateFilterClearBtn() {
2209      if (filterClear) filterClear.classList.toggle("sg-visible", isFilterActive());
2210    }
2211
2212    function updateFilterActiveStyles() {
2213      var pc = config.primaryColor || "#C6A85B";
2214      var rgb = hexToRgb(pc);
2215      var selectBg = config.backgroundColor || "#ffffff";
2216      var selectFc = config.fontColor || "#333333";
2217      [[filterCategory, !!filterState.category],
2218       [filterColour,   !!filterState.colour],
2219       [filterSize,     !!filterState.size],
2220       [filterPrice,    !!filterState.priceRange],
2221       [filterSort,     !!filterState.sort]].forEach(function(pair) {
2222        var el = pair[0]; var active = pair[1];
2223        if (!el) return;
2224        el.classList.toggle("sg-filter-active", active);
2225        if (active) {
2226          el.style.setProperty("color", pc, "important");
2227          el.style.setProperty("border-color", pc, "important");
2228          el.style.setProperty("background-color", "rgba(" + rgb + ",0.08)", "important");
2229          el.style.setProperty("font-weight", "600", "important");
2230        } else {
2231          el.style.setProperty("color", selectFc, "important");
2232          el.style.setProperty("border-color", isLightColor(selectBg) ? "rgba(0,0,0,0.15)" : "rgba(255,255,255,0.2)", "important");
2233          el.style.setProperty("background-color", selectBg, "important");
2234          el.style.setProperty("font-weight", "400", "important");
2235        }
2236      });
2237      if (filterInstock) {
2238        filterInstock.classList.toggle("sg-filter-active", filterState.inStockOnly);
2239        filterInstock.setAttribute("data-active", filterState.inStockOnly ? "true" : "false");
2240      }
2241      updateFilterClearBtn();
2242    }
2243
2244    function loadFilteredProducts() {
2245      var catalogUrl = PROXY_PATH ? PROXY_PATH + "/catalog" : (API_BASE || "") + "/apps/style-guru/catalog";
2246      var body = { shop: shopDomain };
2247      if (filterState.query) body.query = filterState.query;
2248      if (filterState.category) body.category = filterState.category;
2249      if (filterState.colour) body.colour = filterState.colour;
2250      if (filterState.size) body.size = filterState.size;
2251      if (filterState.priceRange) body.priceRange = filterState.priceRange;
2252      if (filterState.sort) body.sort = filterState.sort;
2253      if (filterState.inStockOnly) body.inStockOnly = true;
2254      if (filterState.onSale) body.onSale = true;
2255      if (filterState.fabric) body.fabric = filterState.fabric;
2256      if (landingTags.length) body.tags = landingTags;
2257
2258      var spinColor = config.primaryColor || "#C6A85B";
2259      if (sgGrid) {
2260        sgGrid.innerHTML = '<div class="sg-filter-loading"><div style="width:24px;height:24px;border:3px solid ' + spinColor + ';border-top-color:transparent;border-radius:50%;animation:sg-spin 0.7s linear infinite"></div><span>Filtering...</span></div>';
2261      }
2262      if (headerLabel) headerLabel.textContent = isFilterActive() ? T("results") : T("browse");
2263
2264      filterRequestSeq++;
2265      var thisSeq = filterRequestSeq;
2266
2267      fetch(catalogUrl, {
2268        method: "POST",
2269        headers: { "Content-Type": "application/json" },
2270        body: JSON.stringify(body)
2271      })
2272        .then(function(r) {
2273          if (thisSeq !== filterRequestSeq) return null; // stale request — ignore
2274          if (!r.ok) { throw new Error("HTTP " + r.status); }
2275          return r.json();
2276        })
2277        .then(function(data) {
2278          if (!data || thisSeq !== filterRequestSeq) return; // stale or aborted
2279          var products = (data.products || []).map(function(p) {
2280            return {
2281              title: p.name,
2282              items: [{
2283                name: p.name,
2284                url: p.url || ("/products/" + (p.handle || "")),
2285                price: p.price || "",
2286                image: p.image || "",
2287                productId: String(p.id || ""),
2288                // Only carry a variant when there is exactly one — for sized
2289                // products the customer must pick; defaulting to the first
2290                // size put wrong sizes in carts.
2291                variantId: (p.variants && p.variants.length === 1 ? String(p.variants[0].id) : ""),
2292                variantTitle: (p.variants && p.variants.length === 1 ? p.variants[0].title : ""),
2293                role: "hero",
2294                options: (p.options || []).map(function(o) { return typeof o === "object" ? (o.name || "") : (o || ""); }),
2295                variants: p.variants || [],
2296                tags: p.tags || [],
2297                option1: p.variants && p.variants[0] ? p.variants[0].option1 : "",
2298                option2: p.variants && p.variants[0] ? p.variants[0].option2 : ""
2299              }]
2300            };
2301          });
2302          // Populate category facets once (from the initial all-products load)
2303          if (!filterFacetsLoaded && data.facets) {
2304            populateFacets(data.facets);
2305            filterFacetsLoaded = true;
2306          }
2307          // Always refresh size facets so they stay relevant to the selected category
2308          if (data.facets && filterFacetsLoaded) {
2309            populateSizeFacets(data.facets.sizes || []);
2310            populateEnrichmentFacets(data.facets);
2311          }
2312          if (products.length === 0 && aiFilterRelax.length) {
2313            var relaxKey = aiFilterRelax.shift();
2314            filterState[relaxKey] = "";
2315            if (relaxKey === "query" && filterSearch) filterSearch.value = "";
2316            if (relaxKey === "category" && filterCategory) filterCategory.value = "";
2317            if (relaxKey === "size" && filterSize) filterSize.value = "";
2318            updateFilterActiveStyles();
2319            loadFilteredProducts();
2320            return;
2321          }
2322          if (pendingChatPreview) {
2323            pendingChatPreview = false;
2324            if (products.length > 0) {
2325              for (var pcm = messages.length - 1; pcm >= 0; pcm--) {
2326                if (messages[pcm].role === "assistant") {
2327                  if (!messages[pcm].outfits || messages[pcm].outfits.length === 0) {
2328                    messages[pcm].outfits = products.slice(0, 4);
2329                    renderMessages();
2330                  }
2331                  break;
2332                }
2333              }
2334            }
2335          }
2336          aiFilterRelax = []; // results (or a manual empty) — ladder is spent
2337          if (products.length === 0) {
2338            var countEl2 = document.getElementById("sg-product-count");
2339            if (countEl2) countEl2.textContent = "";
2340            if (sgGrid) sgGrid.innerHTML = '<div class="sg-empty-products">' + searchIcon() + '<h4>' + T("noProducts") + '</h4><p>' + T("adjustFilters") + '</p></div>';
2341            updateScrollFade();
2342          } else {
2343            try {
2344              renderProducts(products, { source: "browse" });
2345            } catch(renderErr) {
2346              console.error("[StyleGuru] renderProducts error:", renderErr);
2347              if (sgGrid) sgGrid.innerHTML = '<div class="sg-empty-products">' + searchIcon() + '<h4>Could not display products</h4></div>';
2348            }
2349          }
2350        })
2351        .catch(function(err) {
2352          if (thisSeq !== filterRequestSeq) return; // stale — ignore
2353          console.error("[StyleGuru] Catalog fetch error:", err);
2354          if (sgGrid) sgGrid.innerHTML = '<div class="sg-empty-products">' + searchIcon() + '<h4>Could not load products</h4><p>Check your connection and try again.</p></div>';
2355        });
2356    }
2357
2358    function populateSizeFacets(sizes) {
2359      if (!filterSize) return;
2360      sizes = sizes || [];
2361      // A remembered size that doesn't exist in the current category (collar vs
2362      // shoe vs chest sizing) silently filters everything to zero and renders
2363      // the select blank. Drop it and refetch once.
2364      if (filterState.size && sizes.indexOf(filterState.size) === -1) {
2365        filterState.size = "";
2366        filterSize.value = "";
2367        updateFilterActiveStyles();
2368        loadFilteredProducts();
2369        return;
2370      }
2371      if (sizes.length === 0) {
2372        filterSize.innerHTML = '<option value="">' + T("size") + '</option>';
2373        return;
2374      }
2375      var numericSizes = sizes.filter(function(s) { return /\d/.test(s); })
2376        .sort(function(a, b) {
2377          var aNum = parseFloat(a); var bNum = parseFloat(b);
2378          return (!isNaN(aNum) && !isNaN(bNum)) ? aNum - bNum : a.localeCompare(b);
2379        });
2380      var wordSizes = sizes.filter(function(s) { return !/\d/.test(s); })
2381        .sort(function(a, b) {
2382          var order = ["XS","S","M","L","XL","XXL","2XL","XXXL","3XL"];
2383          var ai = order.indexOf(a.toUpperCase()); var bi = order.indexOf(b.toUpperCase());
2384          if (ai !== -1 && bi !== -1) return ai - bi;
2385          return a.localeCompare(b);
2386        });
2387      var sizeHtml = '<option value="">' + T("size") + '</option>';
2388      if (wordSizes.length > 0) {
2389        sizeHtml += '<optgroup label="Size">';
2390        wordSizes.forEach(function(s) { sizeHtml += '<option value="' + escapeHtml(s) + '">' + escapeHtml(s) + '</option>'; });
2391        sizeHtml += '</optgroup>';
2392      }
2393      if (numericSizes.length > 0) {
2394        sizeHtml += '<optgroup label="Collar / Numeric">';
2395        numericSizes.forEach(function(s) { sizeHtml += '<option value="' + escapeHtml(s) + '">' + escapeHtml(s) + '</option>'; });
2396        sizeHtml += '</optgroup>';
2397      }
2398      filterSize.innerHTML = sizeHtml;
2399      // Re-apply inline styles after innerHTML replacement (they get wiped)
2400      var selectFc2 = config.fontColor || "#333333";
2401      var selectBg2 = config.backgroundColor || "#ffffff";
2402      filterSize.style.setProperty("color", filterState.size ? (config.primaryColor || "#C6A85B") : selectFc2, "important");
2403      filterSize.style.setProperty("background-color", selectBg2, "important");
2404      if (filterState.size) filterState.size = syncSelectValue(filterSize, filterState.size);
2405    }
2406
2407    function populateFacets(facets) {
2408      if (filterCategory && facets.categories && facets.categories.length > 0) {
2409        var catHtml = '<option value="">' + T("category") + '</option>';
2410        facets.categories.forEach(function(cat) {
2411          catHtml += '<option value="' + escapeHtml(cat) + '">' + escapeHtml(cat) + '</option>';
2412        });
2413        filterCategory.innerHTML = catHtml;
2414        // Re-apply inline styles after innerHTML replacement
2415        var selectFc3 = config.fontColor || "#333333";
2416        var selectBg3 = config.backgroundColor || "#ffffff";
2417        filterCategory.style.setProperty("color", filterState.category ? (config.primaryColor || "#C6A85B") : selectFc3, "important");
2418        filterCategory.style.setProperty("background-color", selectBg3, "important");
2419        if (filterState.category) filterState.category = syncSelectValue(filterCategory, filterState.category);
2420      }
2421      populateSizeFacets(facets.sizes || []);
2422      populateEnrichmentFacets(facets);
2423    }
2424
2425    // Canonical colour/fabric dropdowns from vision enrichment. Colours only
2426    // replace the built-in list when the store is enriched; the fabric select
2427    // stays hidden entirely until fabrics exist (graceful absence).
2428    function populateEnrichmentFacets(facets) {
2429      var pretty = function(v) {
2430        return v.split("-").map(function(w){ return w.charAt(0).toUpperCase() + w.slice(1); }).join(" ");
2431      };
2432      if (filterColour && facets.colours && facets.colours.length >= 3) {
2433        var ch = '<option value="">' + T("colour") + '</option>';
2434        facets.colours.forEach(function(c) { ch += '<option value="' + escapeHtml(c) + '">' + escapeHtml(pretty(c)) + '</option>'; });
2435        filterColour.innerHTML = ch;
2436        if (filterState.colour) filterState.colour = syncSelectValue(filterColour, filterState.colour);
2437      }
2438      if (filterFabric && facets.fabrics && facets.fabrics.length >= 2) {
2439        var fh = '<option value="">Fabric</option>';
2440        facets.fabrics.forEach(function(f) { fh += '<option value="' + escapeHtml(f) + '">' + escapeHtml(pretty(f)) + '</option>'; });
2441        filterFabric.innerHTML = fh;
2442        filterFabric.style.display = "";
2443        if (filterState.fabric) filterState.fabric = syncSelectValue(filterFabric, filterState.fabric);
2444      }
2445    }
2446
2447    // ── Complete Look Zone ────────────────────────────────────────────────────
2448    function showClToast(zoneEl, message, type) {
2449      var existing = zoneEl.querySelector(".sg-cl-toast");
2450      if (existing) existing.remove();
2451      var t = document.createElement("div");
2452      t.className = "sg-cl-toast sg-toast-" + (type || "success");
2453      t.textContent = message;
2454      zoneEl.appendChild(t);
2455      setTimeout(function() { if (t.parentNode) t.remove(); }, 4000);
2456    }
2457
2458    hideCompleteLook = function() {
2459      currentCompleteLook = null;
2460      var zone = document.getElementById("sg-complete-look-zone");
2461      if (zone) { zone.style.display = "none"; zone.innerHTML = ""; }
2462    }
2463
2464    showCompleteLook = function(outfit) {
2465      if (!outfit || !outfit.items || outfit.items.length === 0) return;
2466      currentCompleteLook = outfit;
2467      var zone = document.getElementById("sg-complete-look-zone");
2468      if (!zone) return;
2469
2470      var pc = config.primaryColor || "#C6A85B";
2471      var btnIconColor = isLightColor(pc) ? "#111" : "#fff";
2472      var items = outfit.items || [];
2473      var title = outfit.title || "Complete Look";
2474      var bundlePrice = outfit.bundlePrice || "";
2475
2476      var thumbsHtml = "";
2477      for (var ti = 0; ti < items.length; ti++) {
2478        var item = items[ti];
2479        var imgSrc = item.image || "";
2480        var itemName = item.name || "Item";
2481        var itemCat = item.category || "";
2482        var variantId = item.variantId || "";
2483        var soldOut = item.soldOut ? "true" : "false";
2484        var shortName = itemName.split(" ").slice(0, 3).join(" ");
2485        var thumbContent = imgSrc
2486          ? '<img src="' + escapeHtml(imgSrc) + '" class="sg-cl-thumb" alt="' + escapeHtml(itemName) + '" loading="lazy" />'
2487          : '<div class="sg-cl-thumb-placeholder"></div>';
2488        var swapOverlay = itemCat
2489          ? '<div class="sg-cl-swap-overlay"><button class="sg-cl-swap-btn" data-cat="' + escapeHtml(itemCat) + '" type="button">Swap</button></div>'
2490          : '';
2491        var removeBtn = outfit.removable
2492          ? '<button class="sg-cl-remove-btn" data-key="' + escapeHtml(sgLookKey(item)) + '" type="button" title="Remove from look" style="position:absolute;top:-6px;right:-6px;width:18px;height:18px;border-radius:50%;border:none;background:rgba(0,0,0,0.55);color:#fff;font-size:12px;line-height:18px;padding:0;cursor:pointer;z-index:2">×</button>'
2493          : '';
2494        thumbsHtml += '<div class="sg-cl-item" data-variant-id="' + escapeHtml(variantId) + '" data-sold-out="' + soldOut + '">'
2495          + '<div class="sg-cl-thumb-wrap" style="position:relative">' + thumbContent + swapOverlay + removeBtn + '</div>'
2496          + '<div class="sg-cl-item-name">' + escapeHtml(shortName) + '</div>'
2497          + '</div>';
2498      }
2499
2500      var availCount = 0;
2501      for (var ai = 0; ai < items.length; ai++) {
2502        if (!items[ai].soldOut && (items[ai].variantId || items[ai].productId)) availCount++;
2503      }
2504      var addBtnHtml = availCount >= 2
2505        ? '<button class="sg-cl-add-btn" type="button">' + cartIcon(btnIconColor) + ' Add Complete Look</button>'
2506        : '';
2507
2508      zone.innerHTML = '<div class="sg-cl-header">'
2509        + '<div class="sg-cl-title">' + bundleIcon(pc) + '<span class="sg-cl-title-text">' + escapeHtml(title) + '</span></div>'
2510        + '<button class="sg-cl-dismiss" type="button" title="Dismiss">\u00d7</button>'
2511        + '</div>'
2512        + '<div class="sg-cl-strip">' + thumbsHtml + '</div>'
2513        + '<div class="sg-cl-footer">'
2514        + (bundlePrice ? '<span class="sg-cl-total">' + escapeHtml(bundlePrice) + ' total</span>' : '<span></span>')
2515        + addBtnHtml
2516        + '</div>';
2517
2518      zone.style.display = "block";
2519
2520      var dismissBtn = zone.querySelector(".sg-cl-dismiss");
2521      if (dismissBtn) dismissBtn.addEventListener("click", function() {
2522        // Dismissing the look starts a fresh one.
2523        sessionLook = [];
2524        hideCompleteLook();
2525      });
2526
2527      var removeBtns = zone.querySelectorAll(".sg-cl-remove-btn");
2528      for (var ri = 0; ri < removeBtns.length; ri++) {
2529        (function(rBtn) {
2530          rBtn.addEventListener("click", function(ev) {
2531            ev.stopPropagation();
2532            removeFromSessionLook(rBtn.getAttribute("data-key"));
2533          });
2534        })(removeBtns[ri]);
2535      }
2536
2537      var swapBtns = zone.querySelectorAll(".sg-cl-swap-btn");
2538      for (var si = 0; si < swapBtns.length; si++) {
2539        (function(swapBtn) {
2540          swapBtn.addEventListener("click", function() {
2541            var cat = swapBtn.getAttribute("data-cat");
2542            if (cat && filterCategory) {
2543              filterState.category = cat;
2544              filterCategory.value = cat;
2545              updateFilterActiveStyles();
2546              loadFilteredProducts();
2547              // Update header label to show we're browsing for a swap
2548              var lbl = document.getElementById("sg-products-header-label");
2549              if (lbl) lbl.textContent = "Browse to swap";
2550            }
2551          });
2552        })(swapBtns[si]);
2553      }
2554
2555      var addBtn = zone.querySelector(".sg-cl-add-btn");
2556      if (addBtn) {
2557        addBtn.addEventListener("click", function() { addZoneOutfitToCart(addBtn, outfit, zone); });
2558      }
2559
2560      // New outfit: bring the pane back to the top so it's in view,
2561      // with the product grid scrollable beneath it.
2562      var scrollWrap = document.getElementById("sg-products-scroll");
2563      if (scrollWrap) scrollWrap.scrollTop = 0;
2564    }
2565
2566    function addZoneOutfitToCart(btn, outfit, zoneEl) {
2567      var items = outfit.items || [];
2568      var lines = [];
2569      var unsized = [];
2570      for (var i = 0; i < items.length; i++) {
2571        var vid = items[i].variantId;
2572        if (vid && !items[i].soldOut) lines.push({ variantId: vid, quantity: 1, name: items[i].name || "" });
2573        else if (!vid && !items[i].soldOut) unsized.push(items[i].name || "an item");
2574      }
2575      if (lines.length === 0) {
2576        showClToast(zoneEl, "Please select sizes using the product cards below before adding.", "warn");
2577        return;
2578      }
2579      // Never silently drop pieces: sized items go in, but say what's missing.
2580      if (unsized.length > 0) {
2581        showClToast(zoneEl, "Adding " + lines.length + " — still need a size for: " + unsized.join(", "), "warn");
2582      }
2583
2584      var pc = config.primaryColor || "#C6A85B";
2585      var btnIconColor = isLightColor(pc) ? "#111" : "#fff";
2586      trackEvent("add_bundle_clicked", { itemCount: lines.length, source: "complete_look_zone" });
2587      btn.disabled = true;
2588      btn.innerHTML = '<span style="width:14px;
2588height:14px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;display:inline-block;animation:sg-spin 0.6s linear infinite"></span> Adding ' + lines.length + ' items\u2026';
2589
2590      var styleEl = document.getElementById("sg-spin-style");
2591      if (!styleEl) {
2592        styleEl = document.createElement("style");
2593        styleEl.id = "sg-spin-style";
2594        styleEl.textContent = '@keyframes sg-spin { to { transform: rotate(360deg); } }';
2595        document.head.appendChild(styleEl);
2596      }
2597
2598      var bundleUrl = apiUrl("/cart/add-bundle");
2599      var xhr = new XMLHttpRequest();
2600      xhr.open("POST", bundleUrl);
2601      xhr.setRequestHeader("Content-Type", "application/json");
2602      xhr.onreadystatechange = function() {
2603        if (xhr.readyState !== 4) return;
2604        if (xhr.status === 429) {
2605          btn.disabled = false;
2606          btn.innerHTML = cartIcon(btnIconColor) + ' Add Complete Look';
2607          showClToast(zoneEl, "Please wait a moment before trying again.", "warn");
2608          return;
2609        }
2610        try {
2611          var data = JSON.parse(xhr.responseText);
2612          if (!data.ok) {
2613            btn.disabled = false;
2614            btn.innerHTML = cartIcon(btnIconColor) + ' Add Complete Look';
2615            showClToast(zoneEl, data.error || "Failed to add to cart.", "error");
2616            return;
2617          }
2618          var addedCount = data.added ? data.added.length : 0;
2619          if (addedCount > 0) {
2620            var cartItems = data.added.map(function(a) {
2621              return { id: parseInt(a.variantId.replace(/\D/g, ""), 10), quantity: a.quantity };
2622            });
2623            var cartXhr = new XMLHttpRequest();
2624            cartXhr.open("POST", "/cart/add.js");
2625            cartXhr.setRequestHeader("Content-Type", "application/json");
2626            cartXhr.onreadystatechange = function() {
2627              if (cartXhr.readyState === 4) {
2628                if (cartXhr.status === 200 || cartXhr.status === 201) {
2629                  btn.classList.add("sg-added");
2630                  btn.innerHTML = checkIcon(btnIconColor) + ' Added to Bag!';
2631                  markCartAttributed();
2632                  trackEvent("add_bundle_success", { itemCount: addedCount, source: "complete_look_zone", items: (lines || []).slice(0, 10).map(function(l){ return { name: l.name || "", variantId: String(l.variantId || "") }; }) });
2633                  if (data.removedUnavailable && data.removedUnavailable.length > 0) {
2634                    showClToast(zoneEl, addedCount + " items added. " + data.removedUnavailable.length + " item(s) are currently unavailable.", "warn");
2635                  } else {
2636                    showClToast(zoneEl, "Complete look added to your bag!", "success");
2637                  }
2638                } else {
2639                  btn.disabled = false;
2640                  btn.innerHTML = cartIcon(btnIconColor) + ' Add Complete Look';
2641                  showClToast(zoneEl, "Could not add to cart. Please try again.", "error");
2642                }
2643              }
2644            };
2645            cartXhr.send(JSON.stringify({ items: cartItems }));
2646          } else {
2647            btn.disabled = false;
2648            btn.innerHTML = cartIcon(btnIconColor) + ' Add Complete Look';
2649            showClToast(zoneEl, data.removedUnavailable && data.removedUnavailable.length > 0
2650              ? "These items are currently unavailable. Please check back soon."
2651              : "Nothing to add — some items may be sold out.", "warn");
2652          }
2653        } catch(e) {
2654          btn.disabled = false;
2655          btn.innerHTML = cartIcon(btnIconColor) + ' Add Complete Look';
2656          showClToast(zoneEl, "Something went wrong. Please try again.", "error");
2657        }
2658      };
2659      xhr.send(JSON.stringify({ lines: lines }));
2660    }
2661
2662    // Setting select.value to a value with no matching <option> renders the
2663    // pill gold-highlighted but BLANK (facets not loaded yet, or a case
2664    // mismatch like "suits" vs "Suits"). Match case-insensitively and adopt
2665    // the option's actual value; if nothing matches, append a temporary
2666    // title-cased option so the pill always shows its text.
2667    function syncSelectValue(sel, value) {
2668      if (!sel || !value) return value;
2669      var wanted = String(value).toLowerCase();
2670      for (var oi = 0; oi < sel.options.length; oi++) {
2671        if (sel.options[oi].value && sel.options[oi].value.toLowerCase() === wanted) {
2672          sel.value = sel.options[oi].value;
2673          return sel.options[oi].value;
2674        }
2675      }
2676      var opt = document.createElement("option");
2677      opt.value = value;
2678      opt.textContent = String(value).replace(/\b[a-z]/g, function(c) { return c.toUpperCase(); });
2679      opt.setAttribute("data-sg-temp", "1");
2680      sel.appendChild(opt);
2681      sel.value = value;
2682      return value;
2683    }
2684
2685    applyAIFilters = function(cmd, opts) {
2686      pendingChatPreview = !!(opts && opts.chatPreview);
2687      // cmd: { category?, size?, colour?, query?, inStockOnly? }
2688      // A new grid command is a new intent: start from a clean slate so stale
2689      // constraints from earlier turns (a colour, an old query) can't leak in
2690      // and silently narrow what the customer just asked to see.
2691      filterState = { query: "", category: "", colour: "", size: "", priceRange: "", sort: "", inStockOnly: false, onSale: false, fabric: "" };
2692      if (filterSearch) filterSearch.value = "";
2693      if (filterCategory) filterCategory.value = "";
2694      if (filterColour) filterColour.value = "";
2695      if (filterFabric) filterFabric.value = "";
2696      if (filterSize) filterSize.value = "";
2697      var changed = true;
2698      if (cmd.category !== undefined) { filterState.category = cmd.category; changed = true; }
2699      if (cmd.colour !== undefined) { filterState.colour = cmd.colour; changed = true; }
2700      if (cmd.size !== undefined) { filterState.size = cmd.size; changed = true; }
2701      if (cmd.query !== undefined) { filterState.query = cmd.query; changed = true; }
2702      if (cmd.inStockOnly !== undefined) { filterState.inStockOnly = !!cmd.inStockOnly; changed = true; }
2703      if (cmd.onSale !== undefined) { filterState.onSale = !!cmd.onSale; changed = true; }
2704      if (cmd.fabric !== undefined) { filterState.fabric = cmd.fabric; changed = true; }
2705      // Sync DOM — adopt the matched option's actual value so the state and
2706      // the pill can never disagree.
2707      if (filterCategory && filterState.category) filterState.category = syncSelectValue(filterCategory, filterState.category);
2708      if (filterColour && filterState.colour) filterState.colour = syncSelectValue(filterColour, filterState.colour);
2709      if (filterSize && filterState.size) filterState.size = syncSelectValue(filterSize, filterState.size);
2710      if (filterSearch && filterState.query) filterSearch.value = filterState.query;
2711      updateFilterActiveStyles();
2712      // Relax the AI's guesses before the customer's own words: category is
2713      // the agent's mapping, size is inferred — the query is what they SAID.
2714      aiFilterRelax = [];
2715      if (cmd.category) aiFilterRelax.push("category");
2716      if (cmd.size) aiFilterRelax.push("size");
2717      if (cmd.query) aiFilterRelax.push("query");
2718      if (changed) loadFilteredProducts();
2719    };
2720
2721    function clearFilters() {
2722      filterState = { query: "", category: "", colour: "", size: "", priceRange: "", sort: "", inStockOnly: false, onSale: false, fabric: "" };
2723      landingTags = [];
2724      aiFilterRelax = [];
2725      if (filterSearch) filterSearch.value = "";
2726      if (filterCategory) filterCategory.value = "";
2727      if (filterColour) filterColour.value = "";
2728      if (filterFabric) filterFabric.value = "";
2729      if (filterSize) filterSize.value = "";
2730      if (filterPrice) filterPrice.value = "";
2731      if (filterSort) filterSort.value = "";
2732      updateFilterActiveStyles();
2733      if (headerLabel) headerLabel.textContent = T("browse");
2734      loadFilteredProducts();
2735    }
2736
2737    // Proactive filter: detect product type from user message and pre-populate pane immediately
2738    detectAndApplyProactiveFilter = function(userText) {
2739      var text = (userText || "").toLowerCase();
2740      var catMap = [
2741        [["shirt","shirts","dress shirt","formal shirt","casual shirt"], "shirts"],
2742        [["shoe","shoes","brogue","brogues","oxford","oxfords","loafer","loafers","derby","monk strap","footwear"], "shoes"],
2743        [["tie","ties","cravat","bow tie","bow ties","necktie"], "ties"],
2744        [["trouser","trousers","pant","pants","chino","chinos"], "trousers"],
2745        [["suit","suits"], "suits"],
2746        [["knit","knitwear","jumper","sweater","cardigan","pullover"], "knitwear"],
2747        [["belt","cufflink","cufflinks","pocket square","accessory","accessories","sock","socks"], "accessories"]
2748      ];
2749      // Garment types with no store category of their own (blazers, coats,
2750      // polos...) go in the QUERY — mapping them to a guessed category shows
2751      // the customer a grid of something they didn't ask for.
2752      var offCategoryNouns = ["blazer", "sport coat", "overcoat", "waistcoat", "polo", "gilet", "jacket", "coat"];
2753      var detectedNounQuery = "";
2754      for (var ni = 0; ni < offCategoryNouns.length; ni++) {
2755        if (text.indexOf(offCategoryNouns[ni]) !== -1) { detectedNounQuery = offCategoryNouns[ni]; break; }
2756      }
2757      var detectedCategory = "";
2758      for (var ci = 0; ci < catMap.length; ci++) {
2759        var words = catMap[ci][0];
2760        for (var wi = 0; wi < words.length; wi++) {
2761          if (text.indexOf(words[wi]) !== -1) { detectedCategory = catMap[ci][1]; break; }
2762        }
2763        if (detectedCategory) break;
2764      }
2765      var colours = ["white","black","navy","blue","grey","gray","brown","beige","tan","burgundy","red","green","charcoal","cream","pink","khaki"];
2766      var detectedColour = "";
2767      for (var ci2 = 0; ci2 < colours.length; ci2++) {
2768        if (text.indexOf(colours[ci2]) !== -1) { detectedColour = colours[ci2] === "gray" ? "grey" : colours[ci2]; break; }
2769      }
2770      // Size: collar sizes like 15.5, jacket sizes like 40R/42S, waist sizes like 32/34, shoe sizes like 8/9/10
2771      var sizeMatch = text.match(/\b(\d{1,2}(?:\.\d)?(?:r|s|l|xl|xxl)?)\b/);
2772      var detectedSize = sizeMatch ? sizeMatch[1] : "";
2773      if (detectedNounQuery || detectedCategory || detectedColour) {
2774        var cmd = {};
2775        if (detectedNounQuery) { cmd.query = detectedNounQuery; }
2776        else if (detectedCategory) { cmd.category = detectedCategory; }
2777        if (detectedColour) cmd.colour = detectedColour;
2778        if (detectedSize) cmd.size = detectedSize;
2779        applyAIFilters(cmd);
2780        proactiveFilterApplied = true;
2781      }
2782    };
2783
2784    // Wire filter controls
2785    if (filterSearch) {
2786      filterSearch.addEventListener("input", function() {
2787        filterState.query = filterSearch.value.trim();
2788        updateFilterClearBtn();
2789        clearTimeout(filterDebounceTimer);
2790        filterDebounceTimer = setTimeout(loadFilteredProducts, 350);
2791      });
2792    }
2793    if (filterCategory) {
2794      filterCategory.addEventListener("change", function() {
2795        filterState.category = filterCategory.value;
2796        // Sizes are category-specific (collar vs shoe vs chest) — a stale size
2797        // from the previous category silently filters everything to zero.
2798        filterState.size = "";
2799        if (filterSize) filterSize.value = "";
2800        updateFilterActiveStyles();
2801        loadFilteredProducts();
2802      });
2803    }
2804    if (filterColour) {
2805      filterColour.addEventListener("change", function() {
2806        filterState.colour = filterColour.value;
2807        updateFilterActiveStyles();
2808        loadFilteredProducts();
2809      });
2810    }
2811    if (filterFabric) {
2812      filterFabric.addEventListener("change", function() {
2813        filterState.fabric = filterFabric.value;
2814        updateFilterActiveStyles();
2815        loadFilteredProducts();
2816      });
2817    }
2818    if (filterSize) {
2819      filterSize.addEventListener("change", function() {
2820        filterState.size = filterSize.value;
2821        updateFilterActiveStyles();
2822        loadFilteredProducts();
2823      });
2824    }
2825    if (filterPrice) {
2826      filterPrice.addEventListener("change", function() {
2827        filterState.priceRange = filterPrice.value;
2828        updateFilterActiveStyles();
2829        loadFilteredProducts();
2830      });
2831    }
2832    if (filterSort) {
2833      filterSort.addEventListener("change", function() {
2834        filterState.sort = filterSort.value;
2835        updateFilterActiveStyles();
2836        loadFilteredProducts();
2837      });
2838    }
2839    if (filterInstock) {
2840      filterInstock.addEventListener("click", function() {
2841        filterState.inStockOnly = !filterState.inStockOnly;
2842        updateFilterActiveStyles();
2843        loadFilteredProducts();
2844      });
2845    }
2846    if (filterClear) {
2847      filterClear.addEventListener("click", clearFilters);
2848    }
2849
2850    // Load products + facets on open
2851    loadFilteredProducts();
2852
2853    var micBtn = document.getElementById("sg-modal-mic");
2854    if (micBtn) {
2855      micBtn.onclick = (config.speechEngine && PROXY_PATH)
2856        ? function() { sgLiveConv ? sgStopLiveVoice() : sgStartLiveVoice(); }
2857        : toggleMic;
2858    }
2859
2860    var speakerBtn = document.getElementById("sg-speaker-toggle");
2861    if (speakerBtn) { speakerBtn.onclick = toggleSpeaker; updateSpeakerButton(); }
2862
2863    var tabChat = document.getElementById("sg-tab-chat");
2864    var tabOutfits = document.getElementById("sg-tab-outfits");
2865    if (tabChat) tabChat.onclick = function() { switchTab("chat"); };
2866    if (tabOutfits) tabOutfits.onclick = function() { switchTab("products"); };
2867
2868    renderMessages();
2869    renderProducts([]);
2870  }
2871
2872  var activeTab = "chat";
2873  window.addEventListener("resize", function () {
2874    if (isModalOpen) switchTab(activeTab || "chat");
2875  });
2876  try { if (sessionStorage.getItem("sg-await-reply") === "1") { setTimeout(function () { startReplyPolling(); }, 3000); } } catch (e) {}
2877
2878  // On phones the product grid lives behind the Outfits tab — when the agent
2879  // presents products, drop a tappable chip into the chat so the customer can
2880  // actually reach them (real shoppers repeated queries because replies said
2881  // "here are the ties" while the results sat on the hidden tab).
2882  function showViewProductsChip(label) {
2883    if (window.innerWidth > 640 || activeTab !== "chat") return;
2884    var container = document.getElementById("sg-messages");
2885    if (!container) return;
2886    var old = document.getElementById("sg-view-products-chip");
2887    if (old) old.remove();
2888    var div = document.createElement("div");
2889    div.id = "sg-view-products-chip";
2890    div.className = "sg-msg sg-msg-assistant";
2891    var pc = config.primaryColor || "#C6A85B";
2892    div.innerHTML = '<button style="display:inline-flex;align-items:center;gap:8px;padding:10px 18px;border:none;border-radius:22px;background:' + pc + ';color:#fff;font-size:14px;font-weight:600;font-family:inherit;cursor:pointer;box-shadow:0 2px 8px rgba(0,0,0,0.18);line-height:1.2;">' + escapeHtml(label) + ' →</button>';
2893    div.firstChild.onclick = function() { switchTab("products"); };
2894    container.appendChild(div);
2895    container.scrollTop = container.scrollHeight;
2896  }
2897
2898  // After an error reply, offer a one-tap resend of the message that failed.
2899  // Matches the welcome-chip styling; removes itself once used.
2900  function showRetryChip() {
2901    if (!lastSentText) return;
2902    var container = document.getElementById("sg-messages");
2903    if (!container) return;
2904    var old = document.getElementById("sg-retry-chip");
2905    if (old) old.remove();
2906    var div = document.createElement("div");
2907    div.id = "sg-retry-chip";
2908    div.className = "sg-msg sg-msg-assistant";
2909    var btn = document.createElement("button");
2910    btn.className = "sg-suggestion";
2911    btn.textContent = T("tryAgain");
2912    btn.onclick = function() {
2913      if (isLoading) return;
2914      trackEvent("retry_used");
2915      div.remove();
2916      sendToAPI(lastSentText);
2917    };
2918    div.appendChild(btn);
2919    container.appendChild(div);
2920    container.scrollTop = container.scrollHeight;
2921  }
2922
2923  function switchTab(tab) {
2924    activeTab = tab;
2925    var chatCol = document.getElementById("sg-chat-col");
2926    var productsCol = document.getElementById("sg-products-col");
2927    var tabChatBtn = document.getElementById("sg-tab-chat");
2928    var tabOutfitsBtn = document.getElementById("sg-tab-outfits");
2929    var isMobile = window.innerWidth <= 640;
2930    if (!chatCol || !productsCol) return;
2931    if (!isMobile) {
2932      chatCol.classList.remove("sg-tab-hidden");
2933      productsCol.classList.remove("sg-tab-hidden");
2934      return;
2935    }
2936    var browseBtn = document.getElementById("sg-browse-btn");
2937    var browseIcGrid = document.getElementById("sg-browse-ic-grid");
2938    var browseIcChat = document.getElementById("sg-browse-ic-chat");
2939    if (tab === "chat") {
2940      chatCol.classList.remove("sg-tab-hidden");
2941      productsCol.classList.add("sg-tab-hidden");
2942      if (tabChatBtn) tabChatBtn.classList.add("sg-active");
2943      if (tabOutfitsBtn) tabOutfitsBtn.classList.remove("sg-active");
2944      if (browseBtn) { browseBtn.classList.remove("sg-active"); browseBtn.title = "Browse products"; }
2945      if (browseIcGrid) browseIcGrid.style.display = "flex";
2946      if (browseIcChat) browseIcChat.style.display = "none";
2947    } else {
2948      chatCol.classList.add("sg-tab-hidden");
2949      productsCol.classList.remove("sg-tab-hidden");
2950      if (tabChatBtn) tabChatBtn.classList.remove("sg-active");
2951      if (tabOutfitsBtn) tabOutfitsBtn.classList.add("sg-active");
2952      if (browseBtn) { browseBtn.classList.add("sg-active"); browseBtn.title = "Back to chat"; }
2953      if (browseIcGrid) browseIcGrid.style.display = "none";
2954      if (browseIcChat) browseIcChat.style.display = "flex";
2955    }
2956  }
2957
2958  function openModal(initialQuery) {
2959    createModal();
2960    isModalOpen = true;
2961    trackEvent("widget_opened");
2962    sgPrefetchVoice(); // warm the live-voice token+SDK so the mic starts instantly
2963    setTimeout(showVoiceInvite, 900);
2964    var overlay = document.getElementById("sg-overlay");
2965    // Re-append so the overlay is the last element in <body>: third-party
2966    // floating badges (reviews, chat bubbles) often use the same max z-index,
2967    // and later DOM order wins the tie.
2968    if (overlay.parentNode === document.body && document.body.lastElementChild !== overlay) {
2969      document.body.appendChild(overlay);
2970    }
2971    overlay.style.display = "flex";
2972    requestAnimationFrame(function() {
2973      requestAnimationFrame(function() {
2974        overlay.classList.add("sg-visible");
2975      });
2976    });
2977    document.body.style.overflow = "hidden";
2978    // Hide the bell while the widget is open — it otherwise floats on top of
2979    // the modal on small screens.
2980    var fabEl = document.getElementById("sg-fab");
2981    if (fabEl) fabEl.style.display = "none";
2982    var topBtn = document.getElementById("sg-top-button");
2983    if (topBtn) topBtn.style.display = "none";
2984
2985    // Mobile shows ONE column at a time — chat first; the tab bar moves
2986    // between them. Without this both columns open side-by-side, squashed.
2987    switchTab(activeTab || "chat");
2988
2989    onUserGesture();
2990
2991    var chatInput = document.getElementById("sg-chat-input");
2992    if (initialQuery) {
2993      chatInput.value = initialQuery;
2994      sendMessage();
2995    } else {
2996      setTimeout(function() { chatInput.focus(); }, 200);
2997    }
2998  }
2999
3000  function closeModal() {
3001    isModalOpen = false;
3002    sgStopLiveVoice(); // closing the widget ends the call — no disembodied voice
3003    try { if (window.speechSynthesis) window.speechSynthesis.cancel(); } catch (e) {}
3004    var overlay = document.getElementById("sg-overlay");
3005    if (!overlay) return;
3006    overlay.classList.remove("sg-visible");
3007    document.body.style.overflow = "";
3008    setTimeout(function() {
3009      overlay.style.display = "none";
3010    }, 300);
3011    var fabEl = document.getElementById("sg-fab");
3012    if (fabEl) fabEl.style.display = "";
3013    var topBtn2 = document.getElementById("sg-top-button");
3014    if (topBtn2) topBtn2.style.display = "";
3015    if (currentAudio) {
3016      try {
3017        if (currentAudio.pause) currentAudio.pause();
3018        else currentAudio.stop();
3019      } catch(e) {}
3020      currentAudio = null;
3021      setSpeakingState(false);
3022    }
3023  }
3024
3025  function restoreConversation() {
3026    if (!sessionId || !shopDomain) return;
3027    var xhr = new XMLHttpRequest();
3028    xhr.open("GET", apiUrl("/history?shop=" + encodeURIComponent(shopDomain) + "&sessionId=" + encodeURIComponent(sessionId)));
3029    xhr.onreadystatechange = function() {
3030      if (xhr.readyState === 4 && xhr.status === 200) {
3031        try {
3032          var data = JSON.parse(xhr.responseText);
3033          if (data.messages && data.messages.length > 0) {
3034            // An existing conversation counts as already-gated — returning
3035            // shoppers are never re-asked for their details mid-journey.
3036            sgMarkLeadDone();
3037            messages = [];
3038            for (var i = 0; i < data.messages.length; i++) {
3039              var m = data.messages[i];
3040              messages.push({
3041                role: m.role,
3042                content: m.content,
3043                outfits: m.outfits || undefined
3044              });
3045              if (m.outfits && m.outfits.length > 0) {
3046                lastOutfits = m.outfits;
3047                mergeIntoSessionLook(m.outfits[0]);
3048              }
3049            }
3050            renderMessages();
3051            if (sessionLook.length > 0) {
3052              renderSessionLook();
3053              showDiscussedProducts([{ items: sessionLook }]);
3054            }
3055          }
3056        } catch(e) {}
3057      }
3058    };
3059    xhr.send();
3060  }
3061
3062  function sendMessage() {
3063    var chatInput = document.getElementById("sg-chat-input");
3064    var text = chatInput.value.trim();
3065    if (!text || isLoading) return;
3066
3067    // Barge-in: a new question interrupts the answer still being spoken.
3068    // Runs before onUserGesture so the first-gesture welcome audio isn't
3069    // silenced the instant it starts.
3070    if (stopSpeaking()) trackEvent("voice_bargein", { source: "send" });
3071
3072    onUserGesture();
3073    // Remember that this shopper has actually used the widget — later visits
3074    // greet them as returning instead of repeating the first-time welcome.
3075    try { localStorage.setItem("sg_used_" + shopDomain, "1"); } catch (e) {}
3076    chatInput.value = "";
3077    document.getElementById("sg-chat-send").disabled = true;
3078
3079    switchTab("chat"); // ensure chat is visible while waiting for response
3080    trackEvent("message_sent");
3081    refreshSessionTimestamp(); // keep 24-hour window alive while actively chatting
3082    messages.push({ role: "user", content: text });
3083    renderMessages();
3084    if (audioEnabled) unlockSpeech(); // Safari: unlock during the send gesture
3085    // Immediately filter right pane based on what user typed (before AI responds)
3086    proactiveFilterApplied = false;
3087    detectAndApplyProactiveFilter(text);
3088    sendToAPI(text);
3089  }
3090
3091  var audioEnabled = false;
3092  var welcomeAudioUrl = null;
3093  var welcomeAudioPlayed = false;
3094  var welcomeAudioFetched = false;
3095  var sgAudioEl = null;
3096  // Second half of a split reply, preloading while the first half plays.
3097  var sgQueuedUrl = null;
3098
3099  function getSgAudioEl() {
3100    if (!sgAudioEl) {
3101      sgAudioEl = document.createElement("audio");
3102      sgAudioEl.id = "sg-audio-player";
3103      sgAudioEl.preload = "auto";
3104      sgAudioEl.style.display = "none";
3105      document.body.appendChild(sgAudioEl);
3106      sgAudioEl.addEventListener("ended", function() {
3107        if (sgQueuedUrl && audioEnabled) {
3108          var next = sgQueuedUrl;
3109          sgQueuedUrl = null;
3110          sgPlayUrl(next);
3111          return;
3112        }
3113        setSpeakingState(false);
3114        currentAudio = null;
3115      });
3116      sgAudioEl.addEventListener("error", function() {
3117        sgQueuedUrl = null;
3118        setSpeakingState(false);
3119        currentAudio = null;
3120      });
3121    }
3122    return sgAudioEl;
3123  }
3124
3125  function sgPlayUrl(url) {
3126    var el = getSgAudioEl();
3127    el.volume = 1.0;       // explicit — no state carry-over between clips
3128    el.playbackRate = 1.0;
3129    el.src = url;
3130    el.load();
3131    setSpeakingState(true);
3132    currentAudio = el;
3133    trackEvent("voice_played");
3134    var p = el.play();
3135    if (p && p.catch) p.catch(function() {
3136      setSpeakingState(false);
3137      currentAudio = null;
3138    });
3139  }
3140
3141  function fetchWelcomeAudio() {
3142    if (!shopDomain || welcomeAudioFetched) return;
3143    if (config.interactionMode !== "voice_stylist" && config.interactionMode !== "concierge") return;
3144    welcomeAudioFetched = true;
3145    var xhr = new XMLHttpRequest();
3146    xhr.open("GET", apiUrl("/welcome-audio?shop=" + encodeURIComponent(shopDomain)));
3147    xhr.onreadystatechange = function() {
3148      if (xhr.readyState === 4 && xhr.status === 200) {
3149        try {
3150          var data = JSON.parse(xhr.responseText);
3151          if (data.audioUrl) {
3152            welcomeAudioUrl = PROXY_PATH ? data.audioUrl : (API_BASE + data.audioUrl);
3153            var el = getSgAudioEl();
3154            el.src = welcomeAudioUrl;
3155            el.load();
3156          }
3157        } catch(e) {}
3158      }
3159    };
3160    xhr.send();
3161  }
3162
3163  function onUserGesture() {
3164    if (welcomeAudioPlayed || !audioEnabled) return;
3165    welcomeAudioPlayed = true;
3166    if (welcomeAudioUrl) {
3167      sgPlayUrl(welcomeAudioUrl);
3168    }
3169  }
3170
3171  // Toggle-on confirmation in the stylist's server voice — fetched fresh each
3172  // time (TTS clips expire server-side); browser speech only as a fallback.
3173  var SG_SILENT_WAV = "data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAQB8AAEAfAAABAAgAZGF0YQAAAAA=";
3174  function playConfirmAudio() {
3175    var xhr = new XMLHttpRequest();
3176    xhr.open("GET", apiUrl("/welcome-audio?kind=confirm&shop=" + encodeURIComponent(shopDomain)));
3177    xhr.onreadystatechange = function() {
3178      if (xhr.readyState !== 4 || !audioEnabled) return;
3179      var url = null;
3180      if (xhr.status === 200) {
3181        try { url = JSON.parse(xhr.responseText).audioUrl; } catch (e) {}
3182      }
3183      if (url) {
3184        sgPlayUrl(PROXY_PATH ? url : (API_BASE + url));
3185      } else {
3186        speakFallback("Voice replies are on.");
3187      }
3188    };
3189    xhr.send();
3190  }
3191
3192  function updateSpeakerButton() {
3193    var btn = document.getElementById("sg-speaker-toggle");
3194    if (!btn) return;
3195    btn.classList.toggle("sg-active", audioEnabled);
3196    btn.title = audioEnabled ? "Voice replies: on" : "Voice replies: off";
3197    // unmistakable state: gold-filled when on, slashed grey when off
3198    btn.innerHTML = (audioEnabled ? speakerIcon("currentColor") : speakerMutedIcon("currentColor"));
3199    btn.style.background = audioEnabled ? (config.primaryColor || "#C6A85B") : "";
3200    btn.style.color = audioEnabled ? "#fff" : "";
3201  }
3202
3203  function toggleSpeaker() {
3204    audioEnabled = !audioEnabled;
3205    updateSpeakerButton();
3206    if (!audioEnabled) {
3207      sgQueuedUrl = null;
3208      try { if (window.speechSynthesis) window.speechSynthesis.cancel(); } catch (e) {}
3209      if (currentAudio) {
3210        try {
3211          if (currentAudio.pause) currentAudio.pause();
3212          else currentAudio.stop();
3213        } catch(e) {}
3214        currentAudio = null;
3215      }
3216      setSpeakingState(false);
3217    } else {
3218      // Unlock the shared audio element inside this click gesture (Safari),
3219      // then confirm in the same server voice used for replies.
3220      var el = getSgAudioEl();
3221      try { el.src = SG_SILENT_WAV; var p = el.play(); if (p && p.catch) p.catch(function() {}); } catch (e) {}
3222      playConfirmAudio();
3223    }
3224  }
3225
3226  function toggleMic() {
3227    if (isListening) {
3228      stopListening();
3229    } else {
3230      startListening();
3231    }
3232  }
3233
3234  function startListening() {
3235    var SR = window.SpeechRecognition || window.webkitSpeechRecognition;
3236    if (!SR) return;
3237    // Barge-in: the customer talking always silences the stylist.
3238    if (stopSpeaking()) trackEvent("voice_bargein", { source: "mic" });
3239    recognition = new SR();
3240    recognition.continuous = false;
3241    recognition.interimResults = true;
3242    // Recognise in the storefront's language — a US model mishears British
3243    // accents (and non-English shoppers entirely), which reads as lag.
3244    var SR_LANGS = { en: "en-GB", fr: "fr-FR", de: "de-DE", es: "es-ES", it: "it-IT", nl: "nl-NL" };
3245    recognition.lang = SR_LANGS[sgLocale] || (navigator.language || "en-GB");
3246
3247    recognition.onstart = function() {
3248      isListening = true;
3249      var btn = document.getElementById("sg-modal-mic");
3250      if (btn) btn.classList.add("sg-listening");
3251    };
3252    // Live transcript: interim results echo into the input so the customer
3253    // sees they're being heard; the final result settles for 600ms (cleared
3254    // if more results arrive) and then auto-sends. Browsers without interim
3255    // support just deliver the one final result — same behaviour as before.
3256    var settleTimer = null;
3257    recognition.onresult = function(event) {
3258      var finalText = "";
3259      var interimText = "";
3260      for (var ri = 0; ri < event.results.length; ri++) {
3261        if (event.results[ri].isFinal) finalText += event.results[ri][0].transcript;
3262        else interimText += event.results[ri][0].transcript;
3263      }
3264      var chatInput = document.getElementById("sg-chat-input");
3265      if (!chatInput) return;
3266      chatInput.value = (finalText + interimText).trim();
3267      var sendBtn = document.getElementById("sg-chat-send");
3268      if (sendBtn) sendBtn.disabled = !chatInput.value;
3269      if (settleTimer) { clearTimeout(settleTimer); settleTimer = null; }
3270      if (finalText.trim()) {
3271        settleTimer = setTimeout(function() {
3272          var input2 = document.getElementById("sg-chat-input");
3273          // Release the mic BEFORE sending: an open recognition session
3274          // ducks ALL playback at OS level on mobile, so a lingering mic
3275          // makes the stylist's reply play quiet.
3276          stopListening();
3277          if (input2 && input2.value.trim()) sendMessage();
3278        }, 350);
3279      }
3280    };
3281    recognition.onerror = function() { stopListening(); };
3282    recognition.onend = function() { stopListening(); };
3283    // Show the listening state on the tap itself — onstart can lag a beat
3284    // and a silent button reads as broken.
3285    isListening = true;
3286    var micBtnNow = document.getElementById("sg-modal-mic");
3287    if (micBtnNow) micBtnNow.classList.add("sg-listening");
3288    try {
3289      recognition.start();
3290    } catch (e) {
3291      // A just-stopped session can still be tearing down (rapid re-tap) —
3292      // one delayed retry covers it; a second failure gives up cleanly.
3293      var rec = recognition;
3294      setTimeout(function () {
3295        if (recognition !== rec) return; // user already toggled again
3296        try { rec.start(); } catch (e2) { stopListening(); }
3297      }, 250);
3298    }
3299  }
3300
3301  function stopListening() {
3302    isListening = false;
3303    if (recognition) {
3304      try { recognition.stop(); } catch(e) {}
3305      recognition = null;
3306    }
3307    var btn = document.getElementById("sg-modal-mic");
3308    if (btn) btn.classList.remove("sg-listening");
3309  }
3310
3311  function setSpeakingState(active) {
3312    isSpeaking = active;
3313    var btn = document.getElementById("sg-speaker-toggle");
3314    if (btn) btn.classList.toggle("sg-speaking", active);
3315  }
3316
3317  // Barge-in: silence the stylist immediately — server audio, the queued tail
3318  // clip, and any browser speech. Returns true if something was actually
3319  // playing so callers can track real interruptions only.
3320  function stopSpeaking() {
3321    var wasPlaying = false;
3322    if (currentAudio) {
3323      wasPlaying = true;
3324      try {
3325        if (currentAudio.pause) currentAudio.pause();
3326        else currentAudio.stop();
3327      } catch(e) {}
3328      currentAudio = null;
3329    }
3330    sgQueuedUrl = null;
3331    try {
3332      if (window.speechSynthesis) {
3333        if (window.speechSynthesis.speaking) wasPlaying = true;
3334        window.speechSynthesis.cancel();
3335      }
3336    } catch(e) {}
3337    if (isSpeaking) wasPlaying = true;
3338    setSpeakingState(false);
3339    return wasPlaying;
3340  }
3341
3342  // Early-audio protocol: the server may announce the first-sentence clip
3343  // mid-stream (type:"audio") so the voice starts while text is still arriving;
3344  // the done event then carries the tail clip to queue behind it.
3345  var sgEarlyAudioUrl = null;
3346
3347  function queueFollowUpAudio(audioUrl2) {
3348    if (!audioEnabled || !audioUrl2) return;
3349    var resolved = PROXY_PATH ? audioUrl2 : (API_BASE + audioUrl2);
3350    if (isSpeaking && currentAudio) {
3351      sgQueuedUrl = resolved;
3352    } else {
3353      sgPlayUrl(resolved);
3354    }
3355  }
3356
3357  function playAudio(audioUrl, audioUrl2) {
3358    if (!audioEnabled || !audioUrl) return;
3359    if (currentAudio) {
3360      try {
3361        if (currentAudio.pause) currentAudio.pause();
3362        else currentAudio.stop();
3363      } catch(e) {}
3364      currentAudio = null;
3365    }
3366    sgQueuedUrl = audioUrl2 ? (PROXY_PATH ? audioUrl2 : (API_BASE + audioUrl2)) : null;
3367    sgPlayUrl(PROXY_PATH ? audioUrl : (API_BASE + audioUrl));
3368  }
3369
3370  // Browser-native speech when the server has no TTS configured — voice mode
3371  // works out of the box; server audio (when present) always takes priority.
3372  var sgVoice = null;
3373  function pickVoice() {
3374    if (sgVoice) return sgVoice;
3375    try {
3376      var voices = window.speechSynthesis.getVoices() || [];
3377      sgVoice =
3378        // Chrome's cloud voices are far more natural than local defaults
3379        voices.find(function(v) { return /Google UK English Female/i.test(v.name); }) ||
3380        voices.find(function(v) { return /Google UK English/i.test(v.name); }) ||
3381        // Safari/macOS premium British voices
3382        voices.find(function(v) { return /en-GB/i.test(v.lang) && /Serena|Daniel|Kate|Stephanie|Sonia/i.test(v.name); }) ||
3383        voices.find(function(v) { return /Google US English/i.test(v.name); }) ||
3384        voices.find(function(v) { return /en-GB/i.test(v.lang); }) ||
3385        voices.find(function(v) { return /^en/i.test(v.lang); }) ||
3386        null;
3387    } catch (e) {}
3388    return sgVoice;
3389  }
3390  if (window.speechSynthesis && window.speechSynthesis.onvoiceschanged !== undefined) {
3391    window.speechSynthesis.onvoiceschanged = function() { sgVoice = null; pickVoice(); };
3392  }
3393
3394  // Safari refuses speech that doesn't originate from a user gesture. Speaking
3395  // a silent zero-length utterance DURING a gesture unlocks the engine for the
3396  // network-callback speech that follows.
3397  var speechUnlocked = false;
3398  function unlockSpeech() {
3399    if (speechUnlocked || !window.speechSynthesis) return;
3400    try {
3401      var u = new SpeechSynthesisUtterance(" ");
3402      u.volume = 0;
3403      window.speechSynthesis.speak(u);
3404      window.speechSynthesis.resume();
3405      speechUnlocked = true;
3406    } catch (e) {}
3407  }
3408
3409  // Module-level reference: Chrome garbage-collects unreferenced utterances
3410  // mid-speech, which silences them without any error.
3411  var sgUtterance = null;
3412
3413  function speakFallback(text) {
3414    // When the server synthesises voice, a missing audioUrl means 
3414silence —
3415    // never the robotic browser voice. The fallback only exists for
3416    // out-of-the-box installs with no TTS key on the server.
3417    if (config.serverVoice) return;
3418    if (!audioEnabled || !text || !window.speechSynthesis) return;
3419    try {
3420      window.speechSynthesis.cancel();
3421      // strip markdown-ish noise so it reads naturally
3422      var clean = String(text).replace(/[*_#`]/g, "").replace(/\s+/g, " ").trim().slice(0, 1200);
3423      if (!clean) return;
3424      var u = new SpeechSynthesisUtterance(clean);
3425      var v = pickVoice();
3426      if (v) u.voice = v;
3427      u.rate = 1.02;
3428      u.pitch = 1.0;
3429      u.onstart = function() { setSpeakingState(true); };
3430      u.onend = function() { setSpeakingState(false); if (sgUtterance === u) sgUtterance = null; };
3431      u.onerror = function() { setSpeakingState(false); if (sgUtterance === u) sgUtterance = null; };
3432      sgUtterance = u; // keep alive against Chrome's GC
3433      // cancel() followed immediately by speak() races in Chrome — brief defer
3434      setTimeout(function() {
3435        try {
3436          window.speechSynthesis.speak(u);
3437          window.speechSynthesis.resume();
3438        } catch (e3) {}
3439      }, 60);
3440    } catch (e) {}
3441  }
3442
3443  var lastOutfits = [];
3444  var captureShown = false;
3445
3446  function hasCaptureShown() {
3447    try { return sessionStorage.getItem("sg_capture_" + shopDomain) === "1"; } catch(e) { return false; }
3448  }
3449  function markCaptureShown() {
3450    captureShown = true;
3451    try { sessionStorage.setItem("sg_capture_" + shopDomain, "1"); } catch(e) {}
3452  }
3453
3454  var PURCHASE_SIGNALS = ["add to cart", "buy", "order", "purchase", "checkout", "i'll take", "i want", "can i get", "how do i order", "price", "stock", "available", "in stock"];
3455  var SIZING_SIGNALS = ["size", "sizing", "chest", "waist", "collar", "fit", "too big", "too small", "measurements", "inside leg", "trouser", "jacket size"];
3456  var SAVE_SIGNALS = ["save", "send me", "email me", "remember", "keep this", "love this", "perfect outfit", "this is the one", "i like this"];
3457  var ORDER_SIGNALS = ["order", "delivery", "shipping", "return", "refund", "exchange", "track", "where is my", "late", "not arrived"];
3458
3459  function detectIntent(userText, aiReply) {
3460    // Only scan user text — the AI frequently asks about sizes/prices which would false-trigger
3461    var uText = (userText || "").toLowerCase();
3462    var score = { purchase: 0, sizing: 0, save: 0, order: 0 };
3463    for (var i = 0; i < PURCHASE_SIGNALS.length; i++) if (uText.indexOf(PURCHASE_SIGNALS[i]) !== -1) score.purchase++;
3464    for (var i = 0; i < SIZING_SIGNALS.length; i++) if (uText.indexOf(SIZING_SIGNALS[i]) !== -1) score.sizing++;
3465    for (var i = 0; i < SAVE_SIGNALS.length; i++) if (uText.indexOf(SAVE_SIGNALS[i]) !== -1) score.save++;
3466    for (var i = 0; i < ORDER_SIGNALS.length; i++) if (uText.indexOf(ORDER_SIGNALS[i]) !== -1) score.order++;
3467    if (score.save >= 2) return "save";
3468    if (score.purchase >= 3) return "outfit";
3469    if (score.sizing >= 3) return "sizing";
3470    if (score.order >= 2) return "order";
3471    return null;
3472  }
3473
3474  var CAPTURE_PROMPTS = {
3475    outfit: "Love what you see? Drop your email and we'll send the full outfit look-book straight to your inbox — no account needed.",
3476    sizing: "Want us to save your measurements for next time? Leave your email and we'll keep your size profile ready.",
3477    save: "We can email you this complete outfit so you've got it saved. Just drop your email below.",
3478    order: "If you'd like an update on your order or a callback from our team, leave your email and we'll be in touch.",
3479  };
3480
3481  // ── Lead gate (value-moment capture) ───────────────────────────────────────
3482  // Chat is free; the FIRST outfit/look reveal is locked behind name + email.
3483  // Once per device (localStorage), skipped for logged-in customers
3484  // (prechatSatisfied from the signed settings call) and for restored
3485  // conversations. Server only sends leadGate:true on entitled plans.
3486  function sgLeadDone() {
3487    if (config.prechatSatisfied) return true;
3488    try {
3489      // 'sgfc:lead-captured' is the SHARED cross-app flag (contract with the
3490      // FitCore widget, 2026-09-25): either app's capture silences both gates.
3491      return localStorage.getItem("sg_lead_" + shopDomain) === "1"
3492        || localStorage.getItem("sgfc:lead-captured") === "1";
3493    } catch (e) { return true; }
3494  }
3495  /** Internal skip only (e.g. a restored conversation) — does NOT claim a
3496   *  capture happened, so FitCore's gate is unaffected. */
3497  function sgMarkLeadDone() {
3498    try { localStorage.setItem("sg_lead_" + shopDomain, "1"); } catch (e) {}
3499  }
3500  /** A REAL capture (our form, or FitCore's) — sets the shared flag too. */
3501  function sgMarkLeadCaptured() {
3502    sgMarkLeadDone();
3503    try { localStorage.setItem("sgfc:lead-captured", "1"); } catch (e) {}
3504  }
3505  // Belt and braces per the FitCore contract: their viewer broadcasts
3506  // {type:'fitcore:lead', captured:true} on capture — catch it live so an
3507  // in-page FitCore capture unlocks any pending chat reveal immediately.
3508  try {
3509    window.addEventListener("message", function (ev) {
3510      if (ev.data && ev.data.type === "fitcore:lead" && ev.data.captured) {
3511        sgMarkLeadCaptured();
3512        var gateEl = document.getElementById("sg-lead-gate");
3513        if (gateEl) {
3514          gateEl.remove();
3515          var reveal = sgGateReveal;
3516          sgGateReveal = null;
3517          if (reveal) reveal();
3518        }
3519      }
3520    });
3521  } catch (e) {}
3522  var sgGateReveal = null; // the stashed reveal, run on unlock
3523
3524  function sgShowLeadGate(revealFn, outfitSummary) {
3525    sgGateReveal = revealFn;
3526    var container = document.getElementById("sg-messages");
3527    if (!container) { revealFn(); return; }
3528    if (document.getElementById("sg-lead-gate")) return; // already showing — reveal updated above
3529
3530    trackEvent("lead_gate_shown");
3531    var inputStyle = "width:100%;padding:7px 10px;border:1px solid #ccc;border-radius:8px;font-size:13px;background:#fff;color:#333;box-sizing:border-box;";
3532    var div = document.createElement("div");
3533    div.className = "sg-msg sg-msg-assistant";
3534    div.id = "sg-lead-gate";
3535    div.innerHTML =
3536      '<div class="sg-msg-bubble" style="' + sgFormTint() + '">' +
3537        '<p style="margin:0 0 10px;font-size:13px;line-height:1.5;font-weight:600;">' + escapeHtml(T("leadGateTitle") || "Your look is ready — who shall I show it to?") + '</p>' +
3538        '<p style="margin:0 0 10px;font-size:12px;line-height:1.5;">' + escapeHtml(T("leadGateBody") || "Leave your name and email and I’ll reveal it, plus keep your looks and sizes saved for next time.") + '</p>' +
3539        '<div style="margin-bottom:8px"><input type="text" id="sg-lead-name" placeholder="' + escapeHtml(T("leadGateName") || "Your name") + '" style="' + inputStyle + '" /></div>' +
3540        '<div style="margin-bottom:8px"><input type="email" id="sg-lead-email" placeholder="' + escapeHtml(T("leadGateEmail") || "Your email address") + '" style="' + inputStyle + '" /></div>' +
3541        '<button id="sg-lead-submit" style="width:100%;padding:9px 12px;background:' + (config.primaryColor || "#C6A85B") + ';color:' + (config.addedFg || "#fff") + ';border:none;border-radius:8px;font-size:13px;
3541cursor:pointer;font-weight:600;">' + escapeHtml(T("leadGateCta") || "Show my look") + '</button>' +
3542      '</div>';
3543    container.appendChild(div);
3544    container.scrollTop = container.scrollHeight;
3545
3546    document.getElementById("sg-lead-submit").onclick = function () {
3547      var name = (document.getElementById("sg-lead-name").value || "").trim();
3548      var email = (document.getElementById("sg-lead-email").value || "").trim();
3549      if (!name) { document.getElementById("sg-lead-name").style.borderColor = "#e44"; return; }
3550      if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { document.getElementById("sg-lead-email").style.borderColor = "#e44"; return; }
3551      var btn = document.getElementById("sg-lead-submit");
3552      btn.textContent = "…";
3553      btn.disabled = true;
3554      var payload = JSON.stringify({ name: name, email: email, sessionId: sessionId, captureType: "gate", outfitSummary: outfitSummary || null });
3555      var post = function () {
3556        var xhr = new XMLHttpRequest();
3557        xhr.open("POST", apiUrl("/capture"));
3558        xhr.setRequestHeader("Content-Type", "application/json");
3559        xhr.onreadystatechange = function () {
3560          if (xhr.readyState !== 4) return;
3561          // One background retry on failure — but the reveal NEVER waits on
3562          // (or breaks because of) the network: the lead form was completed.
3563          if (xhr.status < 200 || xhr.status >= 300) setTimeout(function () {
3564            var r = new XMLHttpRequest();
3565            r.open("POST", apiUrl("/capture"));
3566            r.setRequestHeader("Content-Type", "application/json");
3567            r.send(payload);
3568          }, 4000);
3569        };
3570        xhr.send(payload);
3571      };
3572      try { post(); } catch (e) {}
3573      trackEvent("lead_gate_submitted");
3574      sgMarkLeadCaptured();
3575      div.remove();
3576      var reveal = sgGateReveal;
3577      sgGateReveal = null;
3578      if (reveal) reveal();
3579    };
3580  }
3581
3582  function showCapturePrompt(intentType, outfitSummary) {
3583    if (!config.emailCapture) return;
3584    if (sgLeadDone() && config.leadGate) return; // identity already captured by the gate
3585    if (captureShown || hasCaptureShown()) return;
3586    markCaptureShown();
3587
3588    var container = document.getElementById("sg-messages");
3589    if (!container) return;
3590
3591    var prompt = CAPTURE_PROMPTS[intentType] || CAPTURE_PROMPTS.outfit;
3592    var showPhone = config.whatsappCapture;
3593
3594    var div = document.createElement("div");
3595    div.className = "sg-msg sg-msg-assistant";
3596    div.id = "sg-capture-prompt";
3597
3598    var phoneField = showPhone
3599      ? '<div style="margin-top:8px"><input type="tel" id="sg-capture-phone" placeholder="WhatsApp number (optional)" style="width:100%;padding:7px 10px;border:1px solid #ccc;border-radius:8px;font-size:13px;background:#fff;color:#333;box-sizing:border-box;" /></div>'
3600      : "";
3601
3602    div.innerHTML =
3603      '<div class="sg-msg-bubble" style="' + sgFormTint() + '">' +
3604        '<p style="margin:0 0 10px;font-size:13px;line-height:1.5;">' + escapeHtml(prompt) + '</p>' +
3605        '<div style="margin-bottom:8px"><input type="email" id="sg-capture-email" placeholder="Your email address" style="width:100%;padding:7px 10px;border:1px solid #ccc;border-radius:8px;font-size:13px;background:#fff;color:#333;box-sizing:border-box;" /></div>' +
3606        phoneField +
3607        '<div style="display:flex;gap:8px;margin-top:10px">' +
3608          '<button id="sg-capture-submit" style="flex:1;padding:8px 12px;background:' + (config.primaryColor || "#C6A85B") + ';color:#fff;border:none;border-radius:8px;font-size:13px;cursor:pointer;font-weight:600;">Send me the details</button>' +
3609          '<button id="sg-capture-dismiss" style="padding:8px 12px;background:transparent;color:#999;border:1px solid #ddd;border-radius:8px;font-size:12px;cursor:pointer;">Not now</button>' +
3610        '</div>' +
3611      '</div>';
3612
3613    container.appendChild(div);
3614    container.scrollTop = container.scrollHeight;
3615
3616    document.getElementById("sg-capture-dismiss").onclick = function() {
3617      div.remove();
3618    };
3619
3620    document.getElementById("sg-capture-submit").onclick = function() {
3621      var email = (document.getElementById("sg-capture-email").value || "").trim();
3622      if (!email || email.indexOf("@") === -1) {
3623        document.getElementById("sg-capture-email").style.borderColor = "#e44";
3624        return;
3625      }
3626      var phone = showPhone ? ((document.getElementById("sg-capture-phone") || {}).value || "").trim() : "";
3627      var btn = document.getElementById("sg-capture-submit");
3628      btn.textContent = "Saving…";
3629      btn.disabled = true;
3630
3631      var payload = { email: email, sessionId: sessionId, captureType: intentType, outfitSummary: outfitSummary || null };
3632      if (phone) payload.phone = phone;
3633
3634      var xhr = new XMLHttpRequest();
3635      xhr.open("POST", apiUrl("/capture"));
3636      xhr.setRequestHeader("Content-Type", "application/json");
3637      xhr.onreadystatechange = function() {
3638        if (xhr.readyState !== 4) return;
3639        div.remove();
3640        var thankDiv = document.createElement("div");
3641        thankDiv.className = "sg-msg sg-msg-assistant";
3642        thankDiv.innerHTML = '<div class="sg-msg-bubble" style="background:linear-gradient(135deg,#f0faf2,#f9fcf9);border:1px solid #b8dcbc;">' +
3643          '<p style="margin:0;font-size:13px;">Perfect — we\'ve got your details. We\'ll be in touch with your curated look. In the meantime, feel free to ask about anything else.</p>' +
3644          '</div>';
3645        container.appendChild(thankDiv);
3646        container.scrollTop = container.scrollHeight;
3647      };
3648      xhr.send(JSON.stringify(payload));
3649    };
3650  }
3651
3652  // "Send us a message" — human handoff form, agent-triggered (handoff in the
3653  // chat payload) or manual via the envelope button in the header.
3654  // ── Live voice (Speech Engine pilot) ──────────────────────────────────
3655  // Token comes from our signed proxy endpoint, which enforces the paid-plan
3656  // gate server-side; sessions hard-stop after 5 minutes as an abuse cap.
3657  var sgLiveConv = null;
3658  var sgLiveTimer = null;
3659  var sgLiveConvId = null;
3660  var sgLivePoll = null;
3661  var sgLiveSeq = 0;
3662  // Warmed at modal open so tapping the mic starts the call instantly.
3663  var sgVoicePrefetch = null;
3664  function sgPrefetchVoice() {
3665    if (!config.speechEngine || !PROXY_PATH) return;
3666    var modP = import("https://cdn.jsdelivr.net/npm/@elevenlabs/[email protected]/+esm");
3667    var qs = "";
3668    try {
3669      var sz = storedFitcoreSizes() || {};
3670      var parts = [];
3671      ["size", "trouser", "shirt", "casual", "fit", "shoe"].forEach(function(k) {
3672        if (sz[k]) parts.push(k + "=" + encodeURIComponent(String(sz[k]).slice(0, 28)));
3673      });
3674      if (sessionId) parts.push("sessionId=" + encodeURIComponent(sessionId));
3675      if (parts.length) qs = "?" + parts.join("&");
3676    } catch (e) {}
3677    var tokP = fetch(apiUrl("/voice-token" + qs), { credentials: "omit" })
3678      .then(function(r) { return r.ok ? r.json() : null; }).catch(function() { return null; });
3679    sgVoicePrefetch = { modP: modP, tokP: tokP, ts: Date.now() };
3680  }
3681
3682  // Action bridge: while the call runs, poll for this conversation's visual
3683  // payloads (cards, sizes, cart actions) and replay them through the exact
3684  // same handlers a typed chat reply uses.
3685  function sgApplyVoiceVisuals(data) {
3686    var outfits = data.outfits || [];
3687    // Mirror the spoken exchange into the chat thread so every pane change
3688    // has a visible cause — the call and the chat are ONE conversation.
3689    if (data.userText) messages.push({ role: "user", content: data.userText });
3690    if (data.reply || outfits.length) {
3691      messages.push({ role: "assistant", content: data.reply || "Here's what I've found \u2014 take a look.", outfits: outfits });
3692    }
3693    if (data.userText || data.reply || outfits.length) renderMessages();
3694    if (outfits.length) {
3695      lastOutfits = outfits;
3696      persistLastOutfits(outfits);
3697      trackShownProducts(outfits);
3698      sessionLook = []; // voice: the look mirrors the current recommendation
3699      mergeIntoSessionLook(outfits[0]);
3700      renderSessionLook();
3701      showDiscussedProducts([{ items: sessionLook }]);
3702      showViewProductsChip(T("viewOutfit"));
3703    }
3704    if (data.filterCmd) applyAIFilters(data.filterCmd, { chatPreview: true });
3705    if (data.sizes) mergeFitcoreSizes(data.sizes);
3706    if (data.sizeFinder) { switchTab("chat"); setTimeout(openSizeFinderUI, 300); }
3707    if (data.addToCart) agentAddLookToCart(outfits.length ? outfits : (data.outfits || []));
3708    if (data.openCart) agentOpenCart();
3709  }
3710  function sgStartVoicePoll() {
3711    if (sgLivePoll || !sgLiveConvId) return;
3712    sgLivePoll = setInterval(function() {
3713      fetch(apiUrl("/voice-visuals?conv=" + encodeURIComponent(sgLiveConvId) + "&seq=" + sgLiveSeq))
3714        .then(function(r) { return r.ok ? r.json() : null; })
3715        .then(function(d) {
3716          if (!d) return;
3717          var list = d.payloads || (d.seq && d.seq > sgLiveSeq ? [d] : []);
3718          for (var i = 0; i < list.length; i++) {
3719            if (list[i].seq > sgLiveSeq) { sgLiveSeq = list[i].seq; sgApplyVoiceVisuals(list[i]); }
3720          }
3721        }).catch(function() {});
3722    }, 1500);
3723  }
3724  // "Audio on by default": the shopper is invited to turn their mic on the
3725  // moment the chat opens — accepting starts the live spoken session (agent
3726  // audio plays by default); the pulsing waveform is the in-chat off switch.
3727  // Shown once per browser session, never nags.
3728  function showVoiceInvite() {
3729    if (!config.speechEngine || !PROXY_PATH || sgLiveConv) return;
3730    try { if (sessionStorage.getItem("sg-voice-invite-done") === "1") return; } catch (e) {}
3731    var msgs = document.getElementById("sg-messages");
3732    if (!msgs || document.getElementById("sg-voice-invite")) return;
3733    var div = document.createElement("div");
3734    div.id = "sg-voice-invite";
3735    div.style.cssText = "margin:10px 0;padding:12px 14px;border:1px solid " + (config.borderColor || "#e0e0e0") + ";background:rgba(" + hexToRgb(config.primaryColor || "#C6A85B") + ",0.06);";
3736    div.innerHTML = '<div style="font-size:13px;margin-bottom:8px;">Prefer to talk? Turn your microphone on and speak with the stylist \u2014 you can end the call anytime with the pulsing button above.</div>' +
3737      '<div style="display:flex;gap:8px;">' +
3738      '<button id="sg-voice-yes" type="button" style="padding:8px 14px;background:' + (config.primaryColor || "#C6A85B") + ';color:' + (isLightColor(config.primaryColor || "#C6A85B") ? "#111" : "#fff") + ';border:none;font-size:12.5px;font-weight:600;cursor:pointer;">Turn on my mic</button>' +
3739      '<button id="sg-voice-no" type="button" style="padding:8px 14px;background:transparent;border:1px solid ' + (config.borderColor || "#ccc") + ';font-size:12.5px;cursor:pointer;color:inherit;">Maybe later</button>' +
3740      '</div>';
3741    msgs.appendChild(div);
3742    msgs.scrollTop = msgs.scrollHeight;
3743    function done() {
3744      try { sessionStorage.setItem("sg-voice-invite-done", "1"); } catch (e) {}
3745      if (div.parentNode) div.parentNode.removeChild(div);
3746    }
3747    document.getElementById("sg-voice-yes").onclick = function() { done(); sgStartLiveVoice(); };
3748    document.getElementById("sg-voice-no").onclick = done;
3749  }
3750
3751  try { window.addEventListener("pagehide", function() { sgStopLiveVoice(); }); } catch (e) {}
3752  function sgStopLiveVoice() {
3753    try { if (sgLiveConv) sgLiveConv.endSession(); } catch (e) {}
3754    sgLiveConv = null;
3755    if (sgLivePoll) { clearInterval(sgLivePoll); sgLivePoll = null; }
3756    sgLiveConvId = null;
3757    sgLiveSeq = 0;
3758    if (sgLiveTimer) { clearTimeout(sgLiveTimer); sgLiveTimer = null; }
3759    var b = document.getElementById("sg-live-btn");
3760    if (b) { b.classList.remove("sg-live-active"); b.title = "Talk to the stylist"; }
3761    trackEvent("voice_session_ended");
3762  }
3763  function sgStartLiveVoice() {
3764    if (sgLiveConv || !config.speechEngine || !PROXY_PATH) return;
3765    var btn = document.getElementById("sg-live-btn");
3766    var warm = (sgVoicePrefetch && Date.now() - sgVoicePrefetch.ts < 8 * 60_000) ? sgVoicePrefetch : null;
3767    if (!warm) { sgPrefetchVoice(); warm = sgVoicePrefetch; }
3768    navigator.mediaDevices.getUserMedia({ audio: true }).then(function() {
3769      return warm.tokP;
3770    }).then(function(d) {
3771      if (!d || !d.token) throw new Error("voice token unavailable");
3772      return d;
3773    }).then(function(d) {
3774      sgLiveConvId = d.conversationId || null;
3775      sgVoicePrefetch = null; // tokens are single-conversation — never reuse
3776      return warm.modP.then(function(mod) {
3777        return mod.Conversation.startSession({
3778          conversationToken: d.token,
3779          onConnect: function() {
3780            if (btn) { btn.classList.add("sg-live-active"); btn.title = "End voice conversation"; }
3781            sgStartVoicePoll();
3782            trackEvent("voice_session_started");
3783          },
3784          onDisconnect: function() { sgStopLiveVoice(); },
3785          onError: function(e) { console.warn("[StyleGuru voice]", e); sgStopLiveVoice(); }
3786        });
3787      });
3788    }).then(function(conv) {
3789      sgLiveConv = conv;
3790      sgLiveTimer = setTimeout(sgStopLiveVoice, 5 * 60 * 1000);
3791    }).catch(function(e) {
3792      console.warn("[StyleGuru voice] unavailable:", e && e.message);
3793      sgStopLiveVoice();
3794      // Never fail silently — say what went wrong where the customer looks.
3795      var msg = /not found|denied|not allowed|permission/i.test(String(e && e.message))
3796        ? "I couldn't reach your microphone. Please allow microphone access for this site in your browser settings, then tap the voice button again."
3797        : "Voice isn't available right now \u2014 but I'm right here in the chat.";
3798      messages.push({ role: "assistant", content: msg });
3799      renderMessages();
3800    });
3801  }
3802
3803  // Full FitCore experience in an overlay covering the widget: the customer
3804  // works out their size in FitCore's own interface, the result is relayed
3805  // back via postMessage, and closing feeds the sizes into the conversation.
3806  var sgFitcoreResult = null;
3807
3808  function openSizeFinderUI() {
3809    if (config.fitcoreEmbedUrl) { openFitcoreOverlay(); } else { showSizeFinder(); }
3810  }
3811
3812  function openFitcoreOverlay() {
3813    var modal = document.getElementById("sg-modal");
3814    if (!modal || document.getElementById("sg-fitcore-overlay")) return;
3815    trackEvent("size_finder_opened", { mode: "overlay" });
3816    sgFitcoreResult = null;
3817    if (!modal.style.position) modal.style.position = "relative";
3818
3819    var pc = config.primaryColor || "#C6A85B";
3820    var ov = document.createElement("div");
3821    ov.id = "sg-fitcore-overlay";
3822    ov.style.cssText = "position:absolute;top:0;left:0;right:0;bottom:0;z-index:60;background:#f7f4ec;display:flex;flex-direction:column;border-radius:inherit;overflow:hidden;";
3823    ov.innerHTML =
3824      '<div style="display:flex;align-items:center;justify-content:space-between;padding:12px 16px;background:#fff;border-bottom:1px solid rgba(0,0,0,0.08);flex-shrink:0;">' +
3825        '<div style="font-size:14px;font-weight:600;color:#333;">Find your size <span style="font-weight:400;color:#999;font-size:12px;">&mdash; powered by FitCore</span></div>' +
3826        '<button id="sg-fitcore-close" type="button" style="padding:6px 14px;background:transparent;border:1px solid #ccc;border-radius:999px;font-size:12.5px;cursor:pointer;color:#555;">Back to the stylist</button>' +
3827      '</div>' +
3828      // A shopper who already gave their details to the chat's lead gate (or
3829      // is logged in) must never be asked again inside the size finder —
3830      // lead=satisfied tells the FitCore viewer to skip its own gate.
3831      '<iframe id="sg-fitcore-frame" src="' + config.fitcoreEmbedUrl + (sgLeadDone() ? (config.fitcoreEmbedUrl.indexOf("?") === -1 ? "?" : "&") + "lead=satisfied" : "") + '" style="flex:1;width:100%;border:none;background:#f7f4ec;" title="FitCore size finder"></iframe>' +
3832      '<div id="sg-fitcore-bar" style="display:none;flex-shrink:0;padding:10px 16px;background:#fff;border-top:1px solid rgba(0,0,0,0.08);align-items:center;gap:10px;">' +
3833        '<span id="sg-fitcore-bar-text" style="flex:1;font-size:13px;color:#333;"></span>' +
3834        '<button id="sg-fitcore-use" type="button" style="padding:9px 16px;background:' + pc + ';color:#fff;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer;white-space:nowrap;">Use this size &rarr;
3834 continue</button>' +
3835      '</div>';
3836    modal.appendChild(ov);
3837
3838    // Closing with a result in hand still feeds the chat — the customer was
3839    // told "come back and we'll continue", whichever button they reach for.
3840    document.getElementById("sg-fitcore-close").onclick = function() { closeFitcoreOverlay(true); };
3841    document.getElementById("sg-fitcore-use").onclick = function() { closeFitcoreOverlay(true); };
3842  }
3843
3844  function fitcoreResultSummary(r) {
3845    return [
3846      r.size ? "Jacket " + r.size : null,
3847      r.trouser_size ? "Trousers " + r.trouser_size : null,
3848      r.shirt_size ? "Shirt collar " + r.shirt_size : null,
3849      r.casual_size ? "Casual " + r.casual_size : null,
3850      r.shoe_size ? "Shoes " + r.shoe_size : null,
3851      r.recommended_fit ? "Cut " + r.recommended_fit : null,
3852    ].filter(Boolean).join(" · ");
3853  }
3854
3855  function closeFitcoreOverlay(useResult) {
3856    switchTab("chat"); // never strand the customer outside the conversation
3857    var ov = document.getElementById("sg-fitcore-overlay");
3858    if (ov) ov.remove();
3859    var r = sgFitcoreResult;
3860    sgFitcoreResult = null;
3861    if (!useResult || !r || !r.size) return;
3862    trackEvent("size_finder_used", { size: r.size, mode: "overlay" });
3863    var chatInput = document.getElementById("sg-chat-input");
3864    if (chatInput) {
3865      chatInput.value = "I've used the FitCore size finder — " + fitcoreResultSummary(r) + ". Let's carry on with the look.";
3866      var send = document.getElementById("sg-chat-send");
3867      if (send) send.disabled = false;
3868      sendMessage();
3869    }
3870  }
3871
3872  // Results relayed from the FitCore iframe. Origin-checked against the
3873  // configured embed URL.
3874  window.addEventListener("message", function(ev) {
3875    try {
3876      if (!ev.data || ev.data.type !== "fitcore:recommendation") return;
3877      if (!config.fitcoreEmbedUrl || config.fitcoreEmbedUrl.indexOf(ev.origin) !== 0) return;
3878      var r = ev.data.result || {};
3879      if (!r.size) return;
3880      sgFitcoreResult = r;
3881      trackEvent("size_finder_result", { size: r.size, mode: "overlay" });
3882      var bar = document.getElementById("sg-fitcore-bar");
3883      var txt = document.getElementById("sg-fitcore-bar-text");
3884      if (bar && txt) {
3885        txt.textContent = "Your size: " + fitcoreResultSummary(r);
3886        bar.style.display = "flex";
3887      }
3888    } catch (e) {}
3889  });
3890
3891  // ── FitCore storefront handoff ─────────────────────────────────────────────
3892  // FitCore's own storefront widget announces an applied size via
3893  // postMessage {type:'fitcore:apply', size, trouser, shirt, casual, shoe}. We store
3894  // the sizes for every future conversation and offer (never force) a styled
3895  // look: gentle prompt bubble, dismissible, max once per session.
3896  var FITCORE_SIZES_KEY = "sg-fitcore-sizes";
3897
3898  function sanitizeFitcoreSize(v) {
3899    if (typeof v !== "string") return null;
3900    var s = v.trim().slice(0, 28);
3901    // Sizes only: letters, digits, spaces and the punctuation real size
3902    // strings use (16" / 41cm, 36 regular leg). Anything else is not a size.
3903    return s && /^[A-Za-z0-9\s"'.,/–-]+$/.test(s) ? s : null;
3904  }
3905
3906  function storedFitcoreSizes() {
3907    try { return JSON.parse(localStorage.getItem(FITCORE_SIZES_KEY) || "null"); } catch (e) { return null; }
3908  }
3909
3910  function fitcoreSizesSentence(sizes) {
3911    var parts = [];
3912    if (sizes.size) parts.push("jacket/suit " + sizes.size);
3913    if (sizes.trouser) parts.push("trousers " + sizes.trouser);
3914    if (sizes.shirt) parts.push("shirt collar " + sizes.shirt);
3915    if (sizes.casual) parts.push("casual " + sizes.casual);
3916    if (sizes.shoe) parts.push("shoes " + sizes.shoe);
3917    return parts.join(", ");
3918  }
3919
3920  function showFitcorePrompt(sizes) {
3921    if (isModalOpen) return; // sizes are stored; the open conversation already benefits
3922    try { if (sessionStorage.getItem("sg-fitcore-prompt-done") === "1") return; } catch (e) {}
3923    try { sessionStorage.setItem("sg-fitcore-prompt-done", "1"); } catch (e) {}
3924    var existing = document.getElementById("sg-fitcore-prompt");
3925    if (existing) existing.remove();
3926
3927    var headline = sizes.size || sizes.casual || sizes.trouser || sizes.shirt || sizes.shoe;
3928    var onProduct = isProductPage();
3929    var text = headline + " — good to know. " + (onProduct
3930      ? "Want to see this in your size, plus what goes with it?"
3931      : "Want me to build a look in your size?");
3932
3933    var pc = config.primaryColor || "#C6A85B";
3934    var bPosition = config.bellPosition || "bottom_left";
3935    var vertical = (bPosition === "top_right" || bPosition === "top_left") ? "top: 96px;" : "bottom: 160px;";
3936    var horiz = (bPosition === "top_left" || bPosition === "bottom_left") ? "left: 20px;" : (bPosition === "bottom_center" ? "left: 50%; transform: translateX(-50%);" : "right: 20px;");
3937    var bubble = document.createElement("div");
3938    bubble.id = "sg-fitcore-prompt";
3939    bubble.style.cssText = "position:fixed;" + vertical + horiz + "z-index:2147483645;max-width:260px;" +
3940      "background:#fff;color:#1A2332;border:1px solid " + pc + ";border-radius:14px;" +
3941      "box-shadow:0 6px 24px rgba(0,0,0,0.16);padding:12px 34px 12px 14px;" +
3942      "font:500 13px/1.4 " + (config.fontFamily || "inherit") + ";cursor:pointer;" +
3943      "opacity:0;transition:opacity 0.4s ease;";
3944    bubble.textContent = text;
3945    var x = document.createElement("button");
3946    x.setAttribute("aria-label", "Dismiss");
3947    x.innerHTML = "&#10005;";
3948    x.style.cssText = "position:absolute;top:6px;right:8px;border:none;background:none;color:#9aa0ab;font-size:11px;cursor:pointer;padding:2px;";
3949    x.onclick = function (e) { e.stopPropagation(); bubble.remove(); trackEvent("fitcore_prompt_dismissed", {}); };
3950    bubble.appendChild(x);
3951    bubble.onclick = function () {
3952      bubble.remove();
3953      trackEvent("fitcore_prompt_accepted", {});
3954      var handle = ((window.location.pathname || "").match(/\/products\/([^?#/]+)/) || [])[1];
3955      var q = "FitCore just sized me: " + fitcoreSizesSentence(sizes) + ".";
3956      if (handle) {
3957        q += " I'm looking at the \"" + decodeURIComponent(handle).replace(/-/g, " ") +
3958          "\" — show me it in my size and build the rest of the look around it, in my sizes where they apply.";
3959      } else {
3960        q += " Build me a look around them.";
3961      }
3962      openModal(q);
3963    };
3964    document.body.appendChild(bubble);
3965    requestAnimationFrame(function () { bubble.style.opacity = "1"; });
3966    trackEvent("fitcore_prompt_shown", { size: sizes.size || null });
3967    setTimeout(function () { if (bubble.parentNode) { bubble.style.opacity = "0"; setTimeout(function(){ bubble.remove(); }, 500); } }, 25000);
3968  }
3969
3970  window.addEventListener("message", function(ev) {
3971    try {
3972      if (!ev.data) return;
3973      var evType = ev.data.type;
3974      if (evType !== "fitcore:apply" && evType !== "fitcore:result") return;
3975      var sizes = {
3976        size: sanitizeFitcoreSize(ev.data.size),
3977        trouser: sanitizeFitcoreSize(ev.data.trouser),
3978        shirt: sanitizeFitcoreSize(ev.data.shirt),
3979        casual: sanitizeFitcoreSize(ev.data.casual),
3980        shoe: sanitizeFitcoreSize(ev.data.shoe),
3981        fit: sanitizeFitcoreSize(ev.data.fit),
3982      };
3983      if (!sizes.size && !sizes.trouser && !sizes.shirt && !sizes.casual && !sizes.shoe) return;
3984      if (evType === "fitcore:apply") {
3985        // Applied = committed: always overwrites, and is the ONLY prompt trigger.
3986        try { localStorage.setItem(FITCORE_SIZES_KEY, JSON.stringify(sizes)); } catch (e) {}
3987        trackEvent("fitcore_apply_received", sizes);
3988        showFitcorePrompt(sizes);
3989      } else {
3990        // Result-only = size memory, silently: the shopper computed a size but
3991        // didn't apply it. Never prompt, never overwrite an applied size.
3992        if (!storedFitcoreSizes()) {
3993          try { localStorage.setItem(FITCORE_SIZES_KEY, JSON.stringify(sizes)); } catch (e) {}
3994          trackEvent("fitcore_result_stored", sizes);
3995        }
3996      }
3997    } catch (e) {}
3998  });
3999
4000  // ── Top-of-page "Ask the Stylist" button (activation mode top_button) ─────
4001  // The British-Supplements pattern: the assistant lives at the top of every
4002  // page, full-width on mobile — prime position instead of a corner bell.
4003  function renderTopButton() {
4004    if (document.getElementById("sg-top-button")) return;
4005    var pc = config.topButtonColor || config.primaryColor || "#C6A85B";
4006    var txtColor = isLightColor(pc) ? "#111" : "#fff";
4007    var btn = document.createElement("button");
4008    btn.id = "sg-top-button";
4009    btn.type = "button";
4010    var topLabel = config.topButtonLabel || (config.storeVertical && config.storeVertical !== "fashion" ? "Ask us anything" : "Ask the Stylist");
4011    btn.setAttribute("aria-label", topLabel);
4012    btn.innerHTML = '<span style="margin-right:7px">&#10022;</span>' + escapeHtml(topLabel);
4013    btn.style.cssText =
4014      "position:fixed;top:10px;left:50%;transform:translateX(-50%);z-index:2147483643;" +
4015      "background:" + pc + ";color:" + txtColor + ";border:none;border-radius:" + (parseInt(config.borderRadius, 10) === 0 ? "0" : "999px") + ";" +
4016      "padding:10px 22px;font:600 14px/1 " + (config.fontFamily || "inherit") + ";cursor:pointer;" +
4017      "box-shadow:0 4px 16px rgba(0,0,0,0.18);white-space:nowrap;" +
4018      "transition:transform 0.15s ease, box-shadow 0.15s ease;";
4019    // Mobile: full-width bar, still top-pinned
4020    var mq = window.matchMedia("(max-width: 767px)");
4021    function fitTopButton() {
4022      if (mq.matches) {
4023        btn.style.left = "10px";
4024        btn.style.right = "10px";
4025        btn.style.transform = "none";
4026        btn.style.width = "calc(100% - 20px)";
4027      } else {
4028        btn.style.left = "50%";
4029        btn.style.right = "auto";
4030        btn.style.width = "auto";
4031        btn.style.transform = "translateX(-50%)";
4032      }
4033    }
4034    fitTopButton();
4035    if (mq.addEventListener) mq.addEventListener("change", fitTopButton);
4036    btn.onmouseenter = function () { btn.style.boxShadow = "0 6px 22px rgba(0,0,0,0.26)"; };
4037    btn.onmouseleave = function () { btn.style.boxShadow = "0 4px 16px rgba(0,0,0,0.18)"; };
4038    btn.onclick = function () {
4039      trackEvent("top_button_clicked", {});
4040      openModal("");
4041    };
4042    document.body.appendChild(btn);
4043    trackEvent("top_button_shown", {});
4044  }
4045
4046  // ── Agent-driven cart close ────────────────────────────────────────────────
4047  // The stylist works out sizes (estimate_size) → server pre-sizes every card →
4048  // the agent asks ONE question → on yes it flags addToCart, and when the
4049  // customer is done, openCart. The widget executes both here.
4050
4051  /** Server returns freshly worked-out sizes with the reply — remember them so
4052   *  every later conversation (and page) starts size-aware. Newest wins. */
4053  function mergeFitcoreSizes(sizes) {
4054    if (!sizes) return;
4055    try {
4056      var cur = storedFitcoreSizes() || {};
4057      var next = {
4058        size: sanitizeFitcoreSize(sizes.size) || cur.size || null,
4059        trouser: sanitizeFitcoreSize(sizes.trouser) || cur.trouser || null,
4060        shirt: sanitizeFitcoreSize(sizes.shirt) || cur.shirt || null,
4061        casual: sanitizeFitcoreSize(sizes.casual) || cur.casual || null,
4062        shoe: sanitizeFitcoreSize(sizes.shoe) || cur.shoe || null,
4063        fit: sanitizeFitcoreSize(sizes.fit) || cur.fit || null,
4064      };
4065      localStorage.setItem(FITCORE_SIZES_KEY, JSON.stringify(next));
4066      // Live call in progress: the stylist must know the new sizes NOW.
4067      if (sgLiveConvId) {
4068        var body = { conv: sgLiveConvId };
4069        ["size", "trouser", "shirt", "casual", "fit", "shoe"].forEach(function(k) { if (next[k]) body[k] = next[k]; });
4070        fetch(apiUrl("/voice-sizes"), {
4071          method: "POST",
4072          headers: { "Content-Type": "application/json" },
4073          body: JSON.stringify(body),
4074          credentials: "omit"
4075        }).catch(function() {});
4076      }
4077    } catch (e) {}
4078  }
4079
4080  var sgAgentCartBusy = false;
4081  var sgPendingOpenCart = false;
4082
4083  // The presented look survives page navigations (incl. the cart redirect):
4084  // without this, "add my items" on the next page has nothing to add.
4085  function persistLastOutfits(outfits) {
4086    try {
4087      if (!outfits || !outfits.length) return;
4088      var slim = outfits.slice(0, 4).map(function (o) {
4089        return { title: o.title || "", items: (o.items || []).slice(0, 12).map(function (it) {
4090          return { name: it.name, variantId: it.variantId || null, variantTitle: it.variantTitle || null, soldOut: !!it.soldOut, productId: it.productId || null, isSuit: !!it.isSuit };
4091        }) };
4092      });
4093      sessionStorage.setItem("sg-last-outfits-" + shopDomain, JSON.stringify(slim));
4094    } catch (e) {}
4095  }
4096  function restoreLastOutfits() {
4097    try { return JSON.parse(sessionStorage.getItem("sg-last-outfits-" + shopDomain) || "null"); } catch (e) { return null; }
4098  }
4099
4100  /** Add the presented look to the basket — same two-step flow as the
4101   *  Complete Look button (server validates via add-bundle, then Shopify's
4102   *  AJAX cart), without needing the button's DOM. */
4103  function agentAddLookToCart(outfits) {
4104    var items = [];
4105    // This turn's outfits when it presented any; otherwise the LAST look shown
4106    // (an empty array must not shadow it — that was the empty-basket bug).
4107    var source = (outfits && outfits.length) ? outfits : (lastOutfits && lastOutfits.length ? lastOutfits : restoreLastOutfits());
4108    (source || []).forEach(function (o) {
4109      (o.items || []).forEach(function (it) {
4110        if (it.variantId && !it.soldOut) items.push(it);
4111      });
4112    });
4113    if (items.length === 0) return;
4114    var lines = items.map(function (it) { return { variantId: it.variantId, quantity: 1, name: it.name || "" }; });
4115    trackEvent("add_bundle_clicked", { itemCount: lines.length, source: "agent_close" });
4116    if (!PROXY_PATH) {
4117      // Demo/api mode: no storefront cart exists — narrate only.
4118      try { console.log("[StyleGuru] demo mode — would add to cart:", lines); } catch (e) {}
4119      if (sgPendingOpenCart) sgPendingOpenCart = false;
4120      return;
4121    }
4122    sgAgentCartBusy = true;
4123    var xhr = new XMLHttpRequest();
4124    xhr.open("POST", apiUrl("/cart/add-bundle"));
4125    xhr.setRequestHeader("Content-Type", "application/json");
4126    xhr.onreadystatechange = function () {
4127      if (xhr.readyState !== 4) return;
4128      var finish = function () {
4129        sgAgentCartBusy = false;
4130        if (sgPendingOpenCart) { sgPendingOpenCart = false; agentOpenCart(); }
4131      };
4132      try {
4133        var data = JSON.parse(xhr.responseText);
4134        if (!data.ok || !data.added || data.added.length === 0) return finish();
4135        var cartItems = data.added.map(function (a) {
4136          return { id: parseInt(a.variantId.replace(/\D/g, ""), 10), quantity: a.quantity };
4137        });
4138        var cartXhr = new XMLHttpRequest();
4139        cartXhr.open("POST", "/cart/add.js");
4140        cartXhr.setRequestHeader("Content-Type", "application/json");
4141        cartXhr.onreadystatechange = function () {
4142          if (cartXhr.readyState !== 4) return;
4143          if (cartXhr.status === 200 || cartXhr.status === 201) {
4144            markCartAttributed();
4145            trackEvent("add_bundle_success", { itemCount: data.added.length, source: "agent_close", items: lines.slice(0, 10).map(function (l) { return { name: l.name, variantId: String(l.variantId) }; }) });
4146          }
4147          finish();
4148        };
4149        cartXhr.send(JSON.stringify({ items: cartItems }));
4150      } catch (e) { finish(); }
4151    };
4152    var fcS = storedFitcoreSizes() || {};
4153    var hasSuitLine = items.some(function (it) { return it.isSuit || /suit/i.test(it.name || ""); });
4154    var suitSel;
4155    if (hasSuitLine && (fcS.size || fcS.trouser)) {
4156      suitSel = {};
4157      var jNum = fcS.size ? (String(fcS.size).match(/\d{2}/) || [])[0] : null;
4158      if (fcS.size) suitSel.jacketSize = String(fcS.size).slice(0, 20);
4159      var tNum = fcS.trouser ? (String(fcS.trouser).match(/\d{2}/) || [])[0] : null;
4160      if (tNum) suitSel.trouserSize = tNum;
4161      if (jNum) suitSel.waistcoatSize = jNum;
4162    }
4163    xhr.send(JSON.stringify({ lines: lines, sessionId: sessionId, customSelections: suitSel }));
4164  }
4165
4166  /** Take the customer to their basket — waits for an in-flight add first. */
4167  function agentOpenCart() {
4168    if (sgAgentCartBusy) { sgPendingOpenCart = true; return; }
4169    if (!PROXY_PATH) {
4170      try { console.log("[StyleGuru] demo mode — would redirect to /cart"); } catch (e) {}
4171      return;
4172    }
4173    // Never land a customer on an EMPTY basket — the agent can misjudge
4174    // whether anything was actually added, and an empty /cart is a dead end.
4175    try {
4176      var chk = new XMLHttpRequest();
4177      chk.open("GET", "/cart.js");
4178      chk.onreadystatechange = function () {
4179        if (chk.readyState !== 4) return;
4180        var count = 0;
4181        try { count = JSON.parse(chk.responseText).item_count || 0; } catch (e) {}
4182        if (count > 0) {
4183          // A beat so the customer reads the stylist's sign-off first.
4184          setTimeout(function () { window.location.href = "/cart"; }, 600);
4185        } else {
4186          trackEvent("open_cart_blocked_empty");
4187        }
4188      };
4189      chk.send();
4190    } catch (e) {
4191      setTimeout(function () { window.location.href = "/cart"; }, 600);
4192    }
4193  }
4194
4195  // In-chat FitCore size finder: measurements in, exact sizes out, and one tap
4196  // hands the result back to the stylist so the outfit conversation continues.
4197  // Fallback for stores without the full FitCore embed.
4198  function showSizeFinder() {
4199    var container = document.getElementById("sg-messages");
4200    if (!container || !config.fitcoreEnabled) return;
4201    var existing = document.getElementById("sg-size-finder");
4202    if (existing) { existing.scrollIntoView({ block: "nearest" }); return; }
4203    trackEvent("size_finder_opened");
4204
4205    var pc = config.primaryColor || "#C6A85B";
4206    var div = document.createElement("div");
4207    div.className = "sg-msg sg-msg-assistant";
4208    div.id = "sg-size-finder";
4209    var inputStyle = "width:100%;padding:8px 10px;border:1px solid #ccc;border-radius:8px;font-size:13px;line-height:1.45;background:#fff;color:#333;box-sizing:border-box;font-family:inherit;min-height:0;margin:0;letter-spacing:normal;text-transform:none;";
4210    var lblStyle = "display:block;font-size:11px;color:#888;margin:0 0 3px;text-transform:uppercase;letter-spacing:0.04em;";
4211    div.innerHTML =
4212      '<div class="sg-msg-bubble" style="' + sgFormTint() + 'width:100%;">' +
4213        '<p style="margin:0 0 2px;font-size:13px;line-height:1.5;font-weight:600;">Find my size</p>' +
4214        '<p style="margin:0 0 12px;font-size:11.5px;color:#999;">Powered by <strong>FitCore</strong></p>' +
4215        '<div style="display:flex;gap:8px;margin-bottom:8px;">' +
4216          '<div style="flex:1"><span style="' + lblStyle + '">Height (ft)</span><input type="number" id="sg-sf-ft" min="4" max="7" placeholder="5" style="' + inputStyle + '" /></div>' +
4217          '<div style="flex:1"><span style="' + lblStyle + '">Height (in)</span><input type="number" id="sg-sf-in" min="0" max="11" placeholder="10" style="' + inputStyle + '" /></div>' +
4218          '<div style="flex:1"><span style="' + lblStyle + '">Weight (st)</span><input type="number" id="sg-sf-st" min="5" max="35" placeholder="13" style="' + inputStyle + '" /></div>' +
4219          '<div style="flex:1"><span style="' + lblStyle + '">(lb)</span><input type="number" id="sg-sf-lb" min="0" max="13" placeholder="0" style="' + inputStyle + '" /></div>' +
4220        '</div>' +
4221        '<p style="margin:0 0 10px;font-size:11px;color:#999;">Or metric: <input type="number" id="sg-sf-cm" placeholder="cm" style="width:64px;padding:4px 6px;border:1px solid #ddd;border-radius:6px;font-size:12px;"/> <input type="number" id="sg-sf-kg" placeholder="kg" style="width:64px;padding:4px 6px;border:1px solid #ddd;border-radius:6px;font-size:12px;"/></p>' +
4222        '<div style="margin-bottom:12px;">' +
4223          '<span style="' + lblStyle + '">Build</span>' +
4224          '<input type="range" id="sg-sf-build" min="0" max="100" value="50" style="width:100%;accent-color:' + pc + ';margin:4px 0 2px;" />' +
4225          '<div style="display:flex;justify-content:space-between;font-size:10.5px;color:#999;"><span>Leaner</span><span>Average</span><span>Fuller</span></div>' +
4226        '</div>' +
4227        '<div style="display:flex;gap:8px;">' +
4228          '<button id="sg-sf-submit" style="flex:1;padding:8px 12px;background:' + pc + ';color:#fff;border:none;border-radius:8px;font-size:13px;
4228cursor:pointer;font-weight:600;">Find my size</button>' +
4229          '<button id="sg-sf-dismiss" style="padding:8px 12px;background:transparent;color:#999;border:1px solid #ddd;border-radius:8px;font-size:12px;cursor:pointer;">Not now</button>' +
4230        '</div>' +
4231        '<p id="sg-sf-error" style="display:none;margin:8px 0 0;font-size:12px;color:#c0392b;"></p>' +
4232      '</div>';
4233
4234    container.appendChild(div);
4235    container.scrollTop = container.scrollHeight;
4236
4237    document.getElementById("sg-sf-dismiss").onclick = function() { div.remove(); };
4238
4239    document.getElementById("sg-sf-submit").onclick = function() {
4240      var num = function(id) { var v = parseFloat((document.getElementById(id) || {}).value); return isNaN(v) ? 0 : v; };
4241      var heightCm = num("sg-sf-cm") || ((num("sg-sf-ft") * 12 + num("sg-sf-in")) * 2.54);
4242      var weightKg = num("sg-sf-kg") || ((num("sg-sf-st") * 14 + num("sg-sf-lb")) * 0.45359237);
4243      var errEl = document.getElementById("sg-sf-error");
4244      if (heightCm < 120 || heightCm > 230 || weightKg < 30 || weightKg > 300) {
4245        errEl.textContent = "Please enter your height and weight (imperial or metric).";
4246        errEl.style.display = "block";
4247        return;
4248      }
4249      errEl.style.display = "none";
4250      var btn = document.getElementById("sg-sf-submit");
4251      btn.textContent = "Measuring…";
4252      btn.disabled = true;
4253
4254      var xhr = new XMLHttpRequest();
4255      xhr.open("POST", apiUrl("/size-finder"));
4256      xhr.setRequestHeader("Content-Type", "application/json");
4257      xhr.onreadystatechange = function() {
4258        if (xhr.readyState !== 4) return;
4259        var data = null;
4260        try { data = JSON.parse(xhr.responseText); } catch (e) {}
4261        if (xhr.status < 200 || xhr.status >= 300 || !data || !data.size) {
4262          btn.textContent = "Find my size";
4263          btn.disabled = false;
4264          errEl.textContent = (data && data.error) || "The sizing service is busy — please try again in a moment.";
4265          errEl.style.display = "block";
4266          return;
4267        }
4268        trackEvent("size_finder_result", { size: data.size });
4269        var rows = [
4270          ["Jacket", data.size],
4271          ["Trousers", data.trouserSize],
4272          ["Shirt collar", data.shirtSize],
4273          ["Casual", data.casualSize],
4274          ["Shoes", data.shoeSize],
4275          ["Cut", data.recommendedFit],
4276        ].filter(function(r) { return !!r[1]; });
4277        var summary = rows.map(function(r) { return r[0] + " " + r[1]; }).join(" · ");
4278        div.innerHTML =
4279          '<div class="sg-msg-bubble" style="' + sgFormTint() + 'width:100%;">' +
4280            '<p style="margin:0 0 2px;font-size:13px;font-weight:600;">Your FitCore sizes</p>' +
4281            '<p style="margin:0 0 10px;font-size:11.5px;color:#999;">' + (data.confidence ? escapeHtml(data.confidence) + ' confidence · ' : '') + 'Powered by <strong>FitCore</strong></p>' +
4282            rows.map(function(r) {
4283              return '<div style="display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid rgba(0,0,0,0.05);font-size:13px;"><span style="color:#777;">' + escapeHtml(r[0]) + '</span><strong>' + escapeHtml(String(r[1])) + '</strong></div>';
4284            }).join('') +
4285            (data.escalate ? '<p style="margin:10px 0 0;font-size:12px;color:#8a6d3b;">For your build we recommend an in-store fitting for the perfect result.</p>' : '') +
4286            '<div style="display:flex;gap:8px;margin-top:12px;">' +
4287              '<button id="sg-sf-use" style="flex:1;padding:8px 12px;background:' + pc + ';color:#fff;border:none;border-radius:8px;font-size:13px;cursor:pointer;font-weight:600;">Use this size → continue the look</button>' +
4288              '<button id="sg-sf-close" style="padding:8px 12px;background:transparent;color:#999;border:1px solid #ddd;border-radius:8px;font-size:12px;cursor:pointer;">Close</button>' +
4289            '</div>' +
4290          '</div>';
4291        document.getElementById("sg-sf-close").onclick = function() { div.remove(); };
4292        document.getElementById("sg-sf-use").onclick = function() {
4293          trackEvent("size_finder_used", { size: data.size });
4294          div.remove();
4295          var chatInput = document.getElementById("sg-chat-input");
4296          if (chatInput) {
4297            chatInput.value = "I've used the FitCore size finder — " + summary + ". Let's carry on with the look.";
4298            var send = document.getElementById("sg-chat-send");
4299            if (send) send.disabled = false;
4300            sendMessage();
4301          }
4302        };
4303      };
4304      xhr.send(JSON.stringify({
4305        shop: shopDomain,
4306        height_cm: Math.round(heightCm * 10) / 10,
4307        weight_kg: Math.round(weightKg * 10) / 10,
4308        build: num("sg-sf-build") / 100,
4309      }));
4310    };
4311  }
4312
4313  function showHandoffForm(reason, prefillMessage) {
4314    var container = document.getElementById("sg-messages");
4315    if (!container) return;
4316    var existing = document.getElementById("sg-handoff-form");
4317    if (existing) { existing.scrollIntoView({ block: "nearest" }); return; }
4318
4319    var pc = config.primaryColor || "#C6A85B";
4320    var div = document.createElement("div");
4321    div.className = "sg-msg sg-msg-assistant";
4322    div.id = "sg-handoff-form";
4323    var inputStyle = "width:100%;padding:8px 10px;border:1px solid #ccc;border-radius:8px;font-size:13px;line-height:1.45;background:#fff;color:#333;box-sizing:border-box;font-family:inherit;min-height:0;margin:0;letter-spacing:normal;text-transform:none;";
4324    div.innerHTML =
4325      '<div class="sg-msg-bubble" style="' + sgFormTint() + 'width:100%;">' +
4326        '<p style="margin:0 0 10px;font-size:13px;line-height:1.5;font-weight:600;">Send us a message</p>' +
4327        '<p style="margin:0 0 10px;font-size:12.5px;line-height:1.5;color:#666;">Leave your question and the team will reply by email.</p>' +
4328        '<div style="margin-bottom:8px"><input type="text" id="sg-handoff-name" placeholder="Your name (optional)" style="' + inputStyle + '" /></div>' +
4329        '<div style="margin-bottom:8px"><input type="email" id="sg-handoff-email" placeholder="Your email address" style="' + inputStyle + '" /></div>' +
4330        '<div style="margin-bottom:8px"><textarea id="sg-handoff-message" placeholder="Your message" rows="3" style="' + inputStyle + 'resize:vertical;min-height:64px;">' + escapeHtml(prefillMessage || "") + '</textarea></div>' +
4331        '<div style="display:flex;gap:8px;margin-top:10px">' +
4332          '<button id="sg-handoff-submit" style="flex:1;padding:8px 12px;background:' + pc + ';color:#fff;border:none;border-radius:8px;font-size:13px;cursor:pointer;font-weight:600;">Send message</button>' +
4333          '<button id="sg-handoff-dismiss" style="padding:8px 12px;background:transparent;color:#999;border:1px solid #ddd;border-radius:8px;font-size:12px;cursor:pointer;">Not now</button>' +
4334        '</div>' +
4335      '</div>';
4336
4337    container.appendChild(div);
4338    container.scrollTop = container.scrollHeight;
4339
4340    document.getElementById("sg-handoff-dismiss").onclick = function() { div.remove(); };
4341
4342    document.getElementById("sg-handoff-submit").onclick = function() {
4343      var emailEl = document.getElementById("sg-handoff-email");
4344      var msgEl = document.getElementById("sg-handoff-message");
4345      var email = (emailEl.value || "").trim();
4346      var msg = (msgEl.value || "").trim();
4347      var ok = true;
4348      if (!email || email.indexOf("@") === -1 || email.indexOf(".") === -1) { emailEl.style.borderColor = "#e44"; ok = false; }
4349      if (!msg) { msgEl.style.borderColor = "#e44"; ok = false; }
4350      if (!ok) return;
4351
4352      var btn = document.getElementById("sg-handoff-submit");
4353      btn.textContent = "Sending…";
4354      btn.disabled = true;
4355
4356      // Last few exchanges so the team sees the conversation context.
4357      var transcript = "";
4358      try {
4359        transcript = messages.slice(-12).map(function(m) {
4360          return (m.role === "user" ? "Customer: " : "Stylist: ") + String(m.content || "").slice(0, 400);
4361        }).join("\n");
4362      } catch (e) {}
4363
4364      var payload = {
4365        shop: shopDomain,
4366        sessionId: sessionId,
4367        name: (document.getElementById("sg-handoff-name").value || "").trim(),
4368        email: email,
4369        message: msg,
4370        reason: reason || null,
4371        transcript: transcript
4372      };
4373
4374      var xhr = new XMLHttpRequest();
4375      xhr.open("POST", apiUrl("/handoff"));
4376      xhr.setRequestHeader("Content-Type", "application/json");
4377      xhr.onreadystatechange = function() {
4378        if (xhr.readyState !== 4) return;
4379        var success = xhr.status >= 200 && xhr.status < 300;
4380        if (success) startReplyPolling();
4381        div.remove();
4382        var doneDiv = document.createElement("div");
4383        doneDiv.className = "sg-msg sg-msg-assistant";
4384        doneDiv.innerHTML = '<div class="sg-msg-bubble" style="background:linear-gradient(135deg,#f0faf2,#f9fcf9);border:1px solid #b8dcbc;">' +
4385          '<p style="margin:0;font-size:13px;">' +
4386          (success
4387            ? "Your message is with the team — they'll reply to " + escapeHtml(email) + " as soon as they can. Anything else I can help with in the meantime?"
4388            : "Sorry — your message didn't go through. Please try again in a moment.") +
4389          '</p></div>';
4390        container.appendChild(doneDiv);
4391        container.scrollTop = container.scrollHeight;
4392      };
4393      xhr.send(JSON.stringify(payload));
4394    };
4395  }
4396
4397  function trackShownProducts(outfitsOrBundles) {
4398    if (!outfitsOrBundles) return;
4399    for (var gi = 0; gi < outfitsOrBundles.length; gi++) {
4400      var group = outfitsOrBundles[gi];
4401      var items = group.items || [];
4402      for (var ii = 0; ii < items.length; ii++) {
4403        var pid = items[ii].productId || items[ii].id;
4404        if (pid && shownProductIds.indexOf(String(pid)) === -1) {
4405          shownProductIds.push(String(pid));
4406        }
4407      }
4408    }
4409  }
4410
4411  // Rep-style quick replies: the agent sends 2-3 follow-up questions with
4412  // every presentation — pin them to the reply so the next step is a tap.
4413  function attachSuggestions(qs) {
4414    if (!qs || !qs.length) return;
4415    var lm = messages[messages.length - 1];
4416    if (lm && lm.role === "assistant") {
4417      lm.suggestedQuestions = qs.slice(0, 3).map(function (q) { return String(q).slice(0, 80); });
4418    }
4419  }
4420
4421  function sendToAPI(text) {
4422    isLoading = true;
4423    lastSentText = text;
4424    sgEarlyAudioUrl = null;
4425    // True once the stream delivered an error event — the stream-closed
4426    // handler re-renders messages (wiping appended chips), so the retry chip
4427    // must be re-added after that final render.
4428    var streamErrored = false;
4429    showTyping();
4430
4431    reDetectCurrency();
4432    var chatBody = {
4433      message: text,
4434        locale: sgLocale,
4435      sessionId: sessionId,
4436      shop: shopDomain,
4437      stream: true
4438    };
4439    if (detectedCurrency) chatBody.currencyCode = detectedCurrency;
4440    if (devCustomerId) chatBody.customerId = devCustomerId; // dev/testing only; production uses the signed App Proxy param
4441    if (shownProductIds.length > 0) chatBody.shownProductIds = shownProductIds.slice();
4442    // FitCore sizes applied on the storefront — every future recommendation
4443    // is size-aware without the customer repeating themselves.
4444    var fcSizes = storedFitcoreSizes();
4445    if (fcSizes) chatBody.fitcoreSizes = fcSizes;
4446
4447    var streamingMsg = { role: "assistant", content: "", outfits: [] };
4448    messages.push(streamingMsg);
4449    var streamingIdx = messages.length - 1;
4450
4451    fetch(apiUrl("/chat"), {
4452      method: "POST",
4453      headers: { "Content-Type": "application/json" },
4454      body: JSON.stringify(chatBody)
4455    }).then(function(response) {
4456      if (!response.ok) {
4457        throw new Error("HTTP " + response.status);
4458      }
4459
4460      var contentType = response.headers.get("content-type") || "";
4461      if (contentType.indexOf("text/event-stream") === -1) {
4462        return response.json().then(function(data) {
4463          hideTyping();
4464          isLoading = false;
4465          var outfits = data.outfits || [];
4466          var replyContent = data.reply;
4467          if (data.allCandidatesExhausted && outfits.length === 0 && (!data.bundles || data.bundles.length === 0)) {
4468            replyContent += "\n\nYou've seen all available options for this style. Would you like to explore a different colour or style?";
4469          }
4470          messages[streamingIdx] = { role: "assistant", content: replyContent, outfits: outfits };
4471          if (data.sessionId) {
4472            sessionId = data.sessionId;
4473            try { localStorage.setItem("sg_session_" + shopDomain, sessionId); } catch(e) {}
4474          }
4475          // Hybrid model: best outfit → Complete Look zone; filterCmd → product grid
4476          var bestNS = (outfits.length > 0 ? outfits[0] : null) || (data.bundles && data.bundles.length > 0 ? data.bundles[0] : null);
4477          if (bestNS) {
4478            var revealNS = (function (o, d, best) { return function () {
4479              lastOutfits = o.length > 0 ? o : (d.bundles || []);
4480              if (lastOutfits.length) persistLastOutfits(lastOutfits);
4481              trackShownProducts(lastOutfits);
4482              mergeIntoSessionLook(best);
4483              renderSessionLook();
4484              if (d.occasion) window.__sgLastOccasion = d.occasion;
4485              if (d.filterCmd) {
4486                applyAIFilters(d.filterCmd, { chatPreview: true });
4487              } else {
4488                showDiscussedProducts([{ items: sessionLook }]);
4489              }
4490              showViewProductsChip(T("viewOutfit"));
4491            }; })(outfits, data, bestNS);
4492            if (config.leadGate && !sgLeadDone()) {
4493              var gateSummaryNS = (outfits.length > 0 ? outfits : (data.bundles || [])).slice(0, 2)
4494                .map(function (b) { return b.label || b.occasion || b.title || ""; }).filter(Boolean).join(", ");
4495              setTimeout(function () { sgShowLeadGate(revealNS, gateSummaryNS || null); }, 300);
4496            } else {
4497              revealNS();
4498            }
4499          } else if (data.filterCmd) {
4500            applyAIFilters(data.filterCmd, { chatPreview: true });
4501          }
4502          if (data.audioUrl) { playAudio(data.audioUrl, data.audioUrl2); } else { speakFallback(replyContent); }
4503          attachSuggestions(data.suggestedQuestions);
4504          renderMessages();
4505          if (!bestNS && (data.filterCmd || proactiveFilterApplied)) {
4506            showViewProductsChip(T("viewProducts"));
4507          }
4508          if (data.handoff) {
4509            (function(h, q) { setTimeout(function() { showHandoffForm(h.reason || null, q); }, 400); })(data.handoff, text);
4510          }
4511          if (data.sizeFinder) {
4512            setTimeout(openSizeFinderUI, 400);
4513          }
4514          if (data.sizes) mergeFitcoreSizes(data.sizes);
4515          if (data.addToCart) agentAddLookToCart(data.outfits);
4516          if (data.openCart) agentOpenCart();
4517          // Only check capture intent after at least 3 complete exchanges (6 messages)
4518          if (messages.length >= 6) {
4519            var detectedIntentNS = detectIntent(text, replyContent);
4520            if (detectedIntentNS) {
4521              var outfitNamesNS = lastOutfits && lastOutfits.length > 0
4522                ? lastOutfits.slice(0, 2).map(function(b) { return b.label || b.occasion || ""; }
4522).filter(Boolean).join(", ")
4523                : "";
4524              setTimeout(function() { showCapturePrompt(detectedIntentNS, outfitNamesNS || null); }, 1500);
4525            }
4526          }
4527        });
4528      }
4529
4530      // Keep the typing indicator up until the first token arrives — status
4531      // events update its label while the agent works its tools.
4532      var reader = response.body.getReader();
4533      var decoder = new TextDecoder();
4534      var buffer = "";
4535
4536      function processStream() {
4537        return reader.read().then(function(result) {
4538          if (result.done) {
4539            isLoading = false;
4540            hideTyping();
4541            renderMessages();
4542            if (streamErrored) showRetryChip();
4543            return;
4544          }
4545
4546          buffer += decoder.decode(result.value, { stream: true });
4547          var lines = buffer.split("\n");
4548          buffer = lines.pop() || "";
4549
4550          for (var i = 0; i < lines.length; i++) {
4551            var line = lines[i];
4552            if (line.indexOf("data: ") !== 0) continue;
4553            try {
4554              var evt = JSON.parse(line.substring(6));
4555              if (evt.type === "token") {
4556                hideTyping(); // first token replaces the dots (and any status label)
4557                messages[streamingIdx].content += evt.content;
4558                renderMessages();
4559              } else if (evt.type === "status") {
4560                setTypingStatus(evt.label || "");
4561              } else if (evt.type === "audio") {
4562                // First-sentence clip, announced while the reply is still
4563                // streaming — start speaking now, in step with the text.
4564                sgEarlyAudioUrl = evt.audioUrl;
4565                playAudio(evt.audioUrl);
4566              } else if (evt.type === "error") {
4567                messages[streamingIdx].content = evt.message || "Sorry, I'm having trouble right now. Please try again.";
4568                isLoading = false;
4569                streamErrored = true;
4570                hideTyping();
4571                renderMessages();
4572                showRetryChip();
4573              } else if (evt.type === "done") {
4574                hideTyping();
4575                var replyText = evt.reply;
4576                if (evt.allCandidatesExhausted && (!evt.outfits || evt.outfits.length === 0) && (!evt.bundles || evt.bundles.length === 0)) {
4577                  replyText += "\n\nYou've seen all available options for this style. Would you like to explore a different colour or style?";
4578                }
4579                messages[streamingIdx].content = replyText;
4580                messages[streamingIdx].outfits = evt.outfits || [];
4581                if (evt.sessionId) {
4582                  sessionId = evt.sessionId;
4583                  try { localStorage.setItem("sg_session_" + shopDomain, sessionId); } catch(e) {}
4584                }
4585                // Hybrid model: best outfit → Complete Look zone; filterCmd → product grid
4586                var bestST = (evt.outfits && evt.outfits.length > 0 ? evt.outfits[0] : null)
4587                  || (evt.bundles && evt.bundles.length > 0 ? evt.bundles[0] : null);
4588                var revealST = null;
4589                if (bestST) {
4590                  revealST = (function (e, best) { return function () {
4591                    lastOutfits = e.outfits && e.outfits.length > 0 ? e.outfits : (e.bundles || []);
4592                    if (lastOutfits.length) persistLastOutfits(lastOutfits);
4593                    trackShownProducts(lastOutfits);
4594                    mergeIntoSessionLook(best);
4595                    renderSessionLook();
4596                    if (e.filterCmd) {
4597                      applyAIFilters(e.filterCmd, { chatPreview: true });
4598                    } else {
4599                      showDiscussedProducts([{ items: sessionLook }]);
4600                    }
4601                    showViewProductsChip(T("viewOutfit"));
4602                  }; })(evt, bestST);
4603                } else if (evt.filterCmd) {
4604                  applyAIFilters(evt.filterCmd, { chatPreview: true });
4605                }
4606                if (evt.audioUrl) {
4607                  if (sgEarlyAudioUrl && evt.audioUrl === sgEarlyAudioUrl) {
4608                    // First sentence already speaking — just line up the rest.
4609                    if (evt.audioUrl2) queueFollowUpAudio(evt.audioUrl2);
4610                  } else {
4611                    playAudio(evt.audioUrl, evt.audioUrl2);
4612                  }
4613                } else if (!sgEarlyAudioUrl) {
4614                  speakFallback(replyText);
4615                }
4616                sgEarlyAudioUrl = null;
4617                isLoading = false;
4618                attachSuggestions(evt.suggestedQuestions);
4619                renderMessages();
4620                if (revealST) {
4621                  if (config.leadGate && !sgLeadDone()) {
4622                    var gateSummaryST = (evt.outfits && evt.outfits.length > 0 ? evt.outfits : (evt.bundles || [])).slice(0, 2)
4623                      .map(function (b) { return b.label || b.occasion || b.title || ""; }).filter(Boolean).join(", ");
4624                    (function (r, s) { setTimeout(function () { sgShowLeadGate(r, s || null); }, 300); })(revealST, gateSummaryST);
4625                  } else {
4626                    revealST();
4627                  }
4628                } else if (evt.filterCmd || proactiveFilterApplied) {
4629                  showViewProductsChip(T("viewProducts"));
4630                }
4631                if (evt.handoff) {
4632                  (function(h, q) { setTimeout(function() { showHandoffForm(h.reason || null, q); }, 400); })(evt.handoff, text);
4633                }
4634                if (evt.sizeFinder) {
4635                  setTimeout(openSizeFinderUI, 400);
4636                }
4637                if (evt.sizes) mergeFitcoreSizes(evt.sizes);
4638                if (evt.addToCart) agentAddLookToCart(evt.outfits);
4639                if (evt.openCart) agentOpenCart();
4640                // Only check capture intent after at least 3 complete exchanges (6 messages)
4641                if (messages.length >= 6) {
4642                  var detectedIntent = detectIntent(text, replyText);
4643                  if (detectedIntent) {
4644                    var outfitNames = lastOutfits && lastOutfits.length > 0
4645                      ? lastOutfits.slice(0, 2).map(function(b) { return b.label || b.occasion || ""; }).filter(Boolean).join(", ")
4646                      : "";
4647                    setTimeout(function() { showCapturePrompt(detectedIntent, outfitNames || null); }, 1500);
4648                  }
4649                }
4650              }
4651            } catch(e) {
4652              // Never break the stream on a UI error, but don't hide it either.
4653              try { console.error("[StyleGuru] stream event error:", e); } catch (e2) {}
4654            }
4655          }
4656          return processStream();
4657        });
4658      }
4659
4660      return processStream();
4661    }).catch(function(err) {
4662      isLoading = false;
4663      hideTyping();
4664      messages[streamingIdx] = { role: "assistant", content: "Sorry, I'm having trouble right now. Please try again." };
4665      renderMessages();
4666      showRetryChip();
4667    });
4668  }
4669
4670  function renderMessages() {
4671    var container = document.getElementById("sg-messages");
4672    if (!container) return;
4673    var html = "";
4674
4675    if (messages.length === 0) {
4676      html += '<div class="sg-welcome-section">';
4677      html += '<div class="sg-welcome-title">' + escapeHtml(config.stylistName) + '</div>';
4678      var sgReturning = false;
4679      try { sgReturning = localStorage.getItem("sg_used_" + shopDomain) === "1"; } catch (e) {}
4680      html += '<div class="sg-welcome-sub">' + escapeHtml(sgReturning ? T("welcomeBack") : config.welcomeMessage) + '</div>';
4681      // Starter chips: only while the conversation is fresh (this whole welcome
4682      // section renders only when messages.length === 0, so the chips disappear
4683      // as soon as the first user message is sent).
4684      html += '<div class="sg-suggestions">';
4685      html += '<button class="sg-suggestion" data-starter="1" data-msg="Build me an outfit" data-testid="suggestion-outfit">' + T("starterOutfit") + '</button>';
4686      html += '<button class="sg-suggestion" data-starter="1" data-msg="What\'s my size?" data-testid="suggestion-size">' + T("starterSize") + '</button>';
4687      html += '<button class="sg-suggestion" data-starter="1" data-msg="Where\'s my order?" data-testid="suggestion-order">' + T("starterOrder") + '</button>';
4688      html += '</div>';
4689      html += '</div>';
4690    } else {
4691      for (var i = 0; i < messages.length; i++) {
4692        var msg = messages[i];
4693        html += '<div class="sg-msg sg-msg-' + msg.role + '">';
4694        if (msg.storeReply) {
4695          html += '<div style="font-size:10px;letter-spacing:0.08em;text-transform:uppercase;opacity:0.6;margin:0 0 3px 6px;">' + escapeHtml(config.storeName || "") + ' — ' + T("storeReply") + '</div>';
4696          html += '<div class="sg-msg-bubble" style="background:linear-gradient(135deg,#f0faf2,#f9fcf9);border:1px solid #b8dcbc;">' + formatMessageText(msg.content);
4697        } else {
4698        html += '<div class="sg-msg-bubble">' + formatMessageText(msg.content);
4699        }
4700        if (msg.role === "assistant" && msg.outfits && msg.outfits.length > 0) {
4701          html += renderInlineItems(msg.outfits, i);
4702        }
4703        html += '</div>';
4704        // Thumbs on real answers (an assistant reply to a user message):
4705        // up = quiet signal, down = lands in the merchant's Corrections queue.
4706        if (msg.role === "assistant" && i === messages.length - 1 && !isLoading && msg.suggestedQuestions && msg.suggestedQuestions.length) {
4707          html += '<div class="sg-sq-row" style="display:flex;flex-wrap:wrap;gap:6px;margin:6px 0 2px 6px;">';
4708          for (var sq = 0; sq < msg.suggestedQuestions.length; sq++) {
4709            html += '<button class="sg-suggestion" data-sq="' + escapeHtml(msg.suggestedQuestions[sq]) + '" data-testid="chip-suggestion-' + sq + '">' + escapeHtml(msg.suggestedQuestions[sq]) + '</button>';
4710          }
4711          html += '</div>';
4712        }
4713        if (msg.role === "assistant" && i > 0 && messages[i - 1] && messages[i - 1].role === "user" && msg.content) {
4714          if (feedbackVotes[i]) {
4715            html += '<div class="sg-fb-thanks">' + T("thanksFeedback") + '</div>';
4716          } else {
4717            html += '<div class="sg-fb-row" data-fb-row="' + i + '">' +
4718              '<button class="sg-fb-btn" data-fb-idx="' + i + '" data-fb-rating="up" title="Helpful" aria-label="Helpful">' +
4719              '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"/></svg></button>' +
4720              '<button class="sg-fb-btn" data-fb-idx="' + i + '" data-fb-rating="down" title="Not right" aria-label="Not right">' +
4721              '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10 15v4a3 3 0 0 0 3 3l4-9V2H5.72a2 2 0 0 0-2 1.7l-1.38 9a2 2 0 0 0 2 2.3zm7-13h2.67A2.31 2.31 0 0 1 22 4v7a2.31 2.31 0 0 1-2.33 2H17"/></svg></button>' +
4722              '</div>';
4723          }
4724        }
4725        html += '</div>';
4726      }
4727    }
4728
4729    container.innerHTML = html;
4730    container.scrollTop = container.scrollHeight;
4731
4732    var inlineItems = container.querySelectorAll(".sg-inline-item");
4733    for (var ii = 0; ii < inlineItems.length; ii++) {
4734      var testId = inlineItems[ii].getAttribute("data-testid") || "";
4735      var match = testId.match(/inline-item-(\d+)-(\d+)/);
4736      if (match) {
4737        var msgI = parseInt(match[1], 10);
4738        var itemI = parseInt(match[2], 10);
4739        if (messages[msgI] && messages[msgI].outfits) {
4740          var flatInline = [];
4741          for (var fo = 0; fo < messages[msgI].outfits.length; fo++) {
4742            if (messages[msgI].outfits[fo].items) {
4743              for (var fi2 = 0; fi2 < messages[msgI].outfits[fo].items.length; fi2++) {
4744                flatInline.push(messages[msgI].outfits[fo].items[fi2]);
4745              }
4746            }
4747          }
4748          if (flatInline[itemI]) {
4749            inlineItems[ii]._sgItemData = flatInline[itemI];
4750          }
4751          inlineItems[ii].addEventListener("click", function() {
4752            if (this._sgItemData) focusPaneProduct(this._sgItemData);
4753          });
4754        }
4755      }
4756    }
4757
4758    var sqBtns = container.querySelectorAll("[data-sq]");
4759    for (var sqi = 0; sqi < sqBtns.length; sqi++) {
4760      sqBtns[sqi].addEventListener("click", function () {
4761        if (isLoading) return;
4762        var q = this.getAttribute("data-sq");
4763        if (!q) return;
4764        trackEvent("suggestion_chip_used", { q: q.slice(0, 60) });
4765        var row = this.closest(".sg-sq-row");
4766        if (row) row.remove();
4767        messages.push({ role: "user", content: q });
4768        renderMessages();
4769        sendToAPI(q);
4770      });
4771    }
4772
4773    var fbBtns = container.querySelectorAll(".sg-fb-btn");
4774    for (var fb = 0; fb < fbBtns.length; fb++) {
4775      fbBtns[fb].addEventListener("click", function() {
4776        var idx = parseInt(this.getAttribute("data-fb-idx"), 10);
4777        var rating = this.getAttribute("data-fb-rating");
4778        if (feedbackVotes[idx]) return;
4779        feedbackVotes[idx] = rating;
4780        var row = container.querySelector('[data-fb-row="' + idx + '"]');
4781        if (row) { row.outerHTML = '<div class="sg-fb-thanks">' + T("thanksFeedback") + '</div>'; }
4782        trackEvent("answer_feedback", { rating: rating });
4783        try {
4784          var fbXhr = new XMLHttpRequest();
4785          fbXhr.open("POST", apiUrl("/feedback"));
4786          fbXhr.setRequestHeader("Content-Type", "application/json");
4787          fbXhr.send(JSON.stringify({
4788            rating: rating,
4789            question: String(messages[idx - 1].content || "").slice(0, 600),
4790            answer: String(messages[idx].content || "").slice(0, 3000),
4791            anonId: (sessionId && sessionId.length >= 8) ? sessionId : undefined
4792          }));
4793        } catch (e) {}
4794      });
4795    }
4796
4797    var suggestions = container.querySelectorAll(".sg-suggestion");
4798    for (var j = 0; j < suggestions.length; j++) {
4799      suggestions[j].addEventListener("click", function() {
4800        var msgText = this.getAttribute("data-msg");
4801        if (msgText) {
4802          if (this.getAttribute("data-starter")) {
4803            trackEvent("starter_chip_used", { label: this.textContent });
4804          }
4805          var chatInput = document.getElementById("sg-chat-input");
4806          chatInput.value = msgText;
4807          sendMessage();
4808        }
4809      });
4810    }
4811
4812    var inlineCartBtns = container.querySelectorAll(".sg-inline-add-btn:not(:disabled)");
4813    for (var ib = 0; ib < inlineCartBtns.length; ib++) {
4814      inlineCartBtns[ib].addEventListener("click", function() {
4815        inlineAddToCart(this);
4816      });
4817    }
4818
4819    var inlineSelects = container.querySelectorAll(".sg-inline-variant-select:not(.sg-inline-component-select)");
4820    for (var is2 = 0; is2 < inlineSelects.length; is2++) {
4821      inlineSelects[is2].addEventListener("change", function() {
4822        var sel = this;
4823        var vid = sel.value;
4824        if (!vid) return; // back on the "Select size" placeholder
4825        clearSizeNudge(sel);
4826        var inlineItem = sel.closest(".sg-inline-item");
4827        if (inlineItem) {
4828          var btn = inlineItem.querySelector(".sg-inline-add-btn");
4829          if (btn) {
4830            btn.setAttribute("data-variant-id", vid);
4831            btn.removeAttribute("data-product-id");
4832          }
4833        }
4834      });
4835    }
4836
4837    var inlineCompSelects = container.querySelectorAll(".sg-inline-component-select");
4838    for (var ics = 0; ics < inlineCompSelects.length; ics++) {
4839      inlineCompSelects[ics].addEventListener("change", function() {
4840        var sel = this;
4841        if (sel.value) sel.classList.remove("sg-needs-size");
4842        var inlineItem = sel.closest(".sg-inline-item");
4843        if (!inlineItem) return;
4844        var allComps = inlineItem.querySelectorAll(".sg-inline-component-select");
4845        var selections = [];
4846        var allFilled = true;
4847        for (var ai = 0; ai < allComps.length; ai++) {
4848          var s = allComps[ai];
4849          // Advisory selectors capture a preference only — they must neither
4850          // gate variant resolution nor participate in matching.
4851          if (s.getAttribute("data-advisory") === "true") continue;
4852          if (!s.value) { allFilled = false; continue; }
4853          selections.push({
4854            optionIndex: parseInt(s.getAttribute("data-option-index") || "0", 10),
4855            splitIndex: s.hasAttribute("data-split-index") ? parseInt(s.getAttribute("data-split-index"), 10) : undefined,
4856            value: s.value
4857          });
4858        }
4859        if (!allFilled) return;
4860        var itemData = inlineItem._sgItemData;
4861        if (!itemData || !itemData.variants) return;
4862        var matched = findMatchingVariant(itemData.variants, selections);
4863        if (matched) {
4864          clearSizeNudge(sel);
4865          var btn = inlineItem.querySelector(".sg-inline-add-btn");
4866          if (btn) {
4867            btn.setAttribute("data-variant-id", matched.id);
4868            btn.removeAttribute("data-product-id");
4869            if (!matched.available) {
4870              btn.disabled = true;
4871              btn.classList.add("sg-sold-out");
4872              btn.textContent = "Sold Out";
4873            } else {
4874              btn.disabled = false;
4875              btn.classList.remove("sg-sold-out");
4876              var iconColor = isLightColor(config.primaryColor || "#C6A85B") ? "#111" : "#fff";
4877              btn.innerHTML = cartIcon(iconColor) + ' Add';
4878            }
4879          }
4880        }
4881      });
4882    }
4883  }
4884
4885  function renderInlineItems(outfits, msgIdx) {
4886    var items = [];
4887    for (var o = 0; o < outfits.length; o++) {
4888      if (outfits[o].items) {
4889        for (var it = 0; it < outfits[o].items.length; it++) {
4890          items.push(outfits[o].items[it]);
4891        }
4892      }
4893    }
4894    if (items.length === 0) return "";
4895
4896    // Slim references only: the chat TALKS about the pieces; sizing and
4897    // add-to-bag live in the products pane. Tapping a row jumps there.
4898    var html = '<div class="sg-inline-items">';
4899    for (var k = 0; k < items.length; k++) {
4900      var item = items[k];
4901      var displayPrice = formatDisplayPrice(item.formattedPrice || item.price, item.currencyCode);
4902      html += '<div class="sg-inline-item sg-inline-slim" role="button" tabindex="0" data-testid="inline-item-' + msgIdx + '-' + k + '">';
4903      if (item.image) {
4904        html += '<img class="sg-inline-item-img" src="' + escapeHtml(item.image) + '" alt="' + escapeHtml(item.name) + '" loading="lazy" />';
4905      }
4906      html += '<div class="sg-inline-item-info">';
4907      html += '<div class="sg-inline-item-name">' + escapeHtml(item.name) + '</div>';
4908      if (displayPrice) html += '<div class="sg-inline-item-price">' + escapeHtml(displayPrice) + (item.soldOut === true ? ' <span class="sg-inline-soldout">Sold out</span>' : '') + '</div>';
4909      html += '</div>';
4910      html += '<span class="sg-inline-chevron" aria-hidden="true">&#8250;</span>';
4911      html += '</div>';
4912    }
4913    html += '</div>';
4914    return html;
4915  }
4916
4917  // ── Size gate ───────────────────────────────────────────────────────────
4918  // A sized product must have a size actively chosen before Add works —
4919  // otherwise the first variant rides silently into the cart and comes back
4920  // as a return. The nudge highlights the picker and offers the size tools.
4921  function showSizeNudge(btn) {
4922    var card = btn.closest(".sg-product-card") || btn.closest(".sg-inline-item");
4923    if (!card) return;
4924    var selects = card.querySelectorAll(".sg-variant-select:not([data-advisory='true']), .sg-inline-variant-select:not([data-advisory='true'])");
4925    for (var i = 0; i < selects.length; i++) {
4926      if (!selects[i].value) selects[i].classList.add("sg-needs-size");
4927    }
4928    if (!card.querySelector(".sg-size-msg")) {
4929      var msg = document.createElement("div");
4930      msg.className = "sg-size-msg";
4931      msg.innerHTML = 'Choose your size first — <a data-testid="link-size-help">not sure? Ask me</a>';
4932      var actions = card.querySelector(".sg-product-actions");
4933      if (actions) actions.parentNode.insertBefore(msg, actions.nextSibling);
4934      else card.appendChild(msg);
4935      var ask = msg.querySelector("a");
4936      if (ask) ask.addEventListener("click", function() {
4937        var input = document.getElementById("sg-chat-input");
4938        if (input) { input.value = "What's my size?"; sendMessage(); }
4939      });
4940    }
4941    trackEvent("size_gate_blocked");
4942    var first = card.querySelector(".sg-needs-size");
4943    if (first && first.scrollIntoView) first.scrollIntoView({ block: "nearest", behavior: "smooth" });
4944  }
4945
4946  function clearSizeNudge(el) {
4947    var card = el.closest(".sg-product-card") || el.closest(".sg-inline-item");
4948    if (!card) return;
4949    var marked = card.querySelectorAll(".sg-needs-size");
4950    for (var i = 0; i < marked.length; i++) marked[i].classList.remove("sg-needs-size");
4951    var msg = card.querySelector(".sg-size-msg");
4952    if (msg && msg.parentNode) msg.parentNode.removeChild(msg);
4953  }
4954
4955  function inlineAddToCart(btn) {
4956    var variantId = btn.getAttribute("data-variant-id");
4957    var productId = btn.getAttribute("data-product-id");
4958    if (btn.getAttribute("data-size-required") === "1" && !variantId) {
4959      showSizeNudge(btn);
4960      return;
4961    }
4962    if (!variantId && !productId) return;
4963
4964    btn.disabled = true;
4965    var origHtml = btn.innerHTML;
4966    btn.innerHTML = '<span style="width:10px;height:10px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;display:inline-block;animation:sg-spin 0.6s linear infinite"></span>';
4967
4968    var styleEl = document.getElementById("sg-spin-style");
4969    if (!styleEl) {
4970      styleEl = document.createElement("style");
4971      styleEl.id = "sg-spin-style";
4972      styleEl.textContent = '@keyframes sg-spin { to { transform: rotate(360deg); } }';
4973      document.head.appendChild(styleEl);
4974    }
4975
4976    var iconColor = isLightColor(config.primaryColor || "#C6A85B") ? "#111" : "#fff";
4977
4978    if (variantId) {
4979      doInlineCartAdd(variantId, btn, iconColor);
4980    } else if (productId) {
4981      var inlineCard = btn.closest(".sg-inline-item");
4982      var linkEl = inlineCard ? inlineCard.querySelector(".sg-inline-item-name a") : null;
4983      var handle = null;
4984      if (linkEl) {
4985        var href = linkEl.getAttribute("href") || "";
4986        var hMatch = href.match(/\/products\/([^?#]+)/);
4987        if (hMatch) handle = hMatch[1];
4988      }
4989      if (!handle) {
4990        inlineCartError(btn, iconColor);
4991        return;
4992      }
4993      var productUrl = "https://" + shopDomain + "/products/" + handle + ".json";
4994      var xhr = new XMLHttpRequest();
4995      xhr.open("GET", productUrl);
4996      xhr.onreadystatechange = function() {
4997        if (xhr.readyState === 4) {
4998          if (xhr.status === 200) {
4999            try {
5000              var data = JSON.parse(xhr.responseText);
5001              var vid = data.product && data.product.variants && data.product.variants[0] ? data.product.variants[0].id : null;
5002              if (vid) {
5003                doInlineCartAdd(vid, btn, iconColor);
5004              } else {
5005                inlineCartError(btn, iconColor);
5006              }
5007            } catch(e) { inlineCartError(btn, iconColor); }
5008          } else {
5009            inlineCartError(btn, iconColor);
5010          }
5011        }
5012      };
5013      xhr.send();
5014    }
5015  }
5016
5017  function doInlineCartAdd(variantId, btn, iconColor) {
5018    var xhr = new XMLHttpRequest();
5019    xhr.open("POST", "/cart/add.js");
5020    xhr.setRequestHeader("Content-Type", "application/json");
5021    xhr.onreadystatechange = function() {
5022      if (xhr.readyState === 4) {
5023        if (xhr.status === 200) {
5024          markCartAttributed();
5025                  trackEvent("add_bundle_success", { variantId: variantId, source: "inline_chat" });
5026          btn.classList.add("sg-added");
5027          btn.innerHTML = checkIcon("#fff") + ' Added';
5028          btn.disabled = true;
5029          setTimeout(function() {
5030            btn.classList.remove("sg-added");
5031            btn.innerHTML = cartIcon(iconColor) + ' Add';
5032            btn.disabled = false;
5033          }, 3000);
5034        } else {
5035          inlineCartError(btn, iconColor);
5036        }
5037      }
5038    };
5039    xhr.send(JSON.stringify({ id: variantId, quantity: 1 }));
5040  }
5041
5042  function inlineCartError(btn, iconColor) {
5043    btn.innerHTML = 'N/A';
5044    btn.disabled = true;
5045    setTimeout(function() {
5046      btn.innerHTML = cartIcon(iconColor) + ' Add';
5047      btn.disabled = false;
5048    }, 2000);
5049  }
5050
5051  function bundleIcon(color) {
5052    return '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="' + color + '" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/></svg>';
5053  }
5054
5055  function isSuitProduct(item) {
5056    if (item.isSuit === true) return true;
5057    if (item.isSuit === false) return false;
5058    var name = (item.name || "").toLowerCase();
5059    // A single garment whose name merely MENTIONS "suit" — "Wedding Suit
5060    // Jacket", "Navy Suit Trousers" — is not a suit. Offering three-piece
5061    // sizing under a lone jacket or trouser listing is wrong and confusing.
5062    if (/\b(jacket|blazer|trouser|trousers|waistcoat|gilet)\b/.test(name)) return false;
5063    var tags = (item.tags || []).map(function(t) { return t.toLowerCase(); });
5064    return name.indexOf("suit") !== -1 || tags.indexOf("suit") !== -1 || tags.indexOf("3-piece") !== -1 || tags.indexOf("three-piece") !== -1 || tags.indexOf("three piece") !== -1;
5065  }
5066
5067  function detectSuitComponents(item) {
5068    if (!isSuitProduct(item)) return null;
5069    if (!item.options || item.options.length === 0) return null;
5070
5071    var componentMap = {
5072      "jacket": ["jacket", "chest", "jacket size", "jacket chest", "coat size", "chest size", "coat"],
5073      "waistcoat": ["waistcoat", "vest", "waistcoat size", "gilet", "waist coat"],
5074      "trousers": ["trouser", "trousers", "trouser size", "waist", "leg", "inside leg", "trouser waist", "pant", "pants", "trouser waist size"]
5075    };
5076    var components = [];
5077    for (var oi = 0; oi < item.options.length; oi++) {
5078      var optName = (item.options[oi] || "").toLowerCase().trim();
5079      var matched = false;
5080      for (var comp in componentMap) {
5081        var keywords = componentMap[comp];
5082        for (var ki = 0; ki < keywords.length; ki++) {
5083          if (optName.indexOf(keywords[ki]) !== -1) {
5084            components.push({ label: comp.charAt(0).toUpperCase() + comp.slice(1), optionIndex: oi });
5085            matched = true;
5086            break;
5087          }
5088        }
5089        if (matched) break;
5090      }
5091      if (!matched) {
5092        components.push({ label: item.options[oi], optionIndex: oi });
5093      }
5094    }
5095
5096    // Handle slash-encoded suit sizes: "38R / 30 / 38" (Jacket / Trouser / Waistcoat)
5097    if (components.length <= 1 && item.options.length === 1) {
5098      var singleOpt = (item.options[0] || "").toLowerCase();
5099      if (singleOpt === "size" || singleOpt === "title") {
5100        var hasSlash = false;
5101        if (item.variants && item.variants.length > 0) {
5102          var firstTitle = item.variants[0].title || "";
5103          if (firstTitle.indexOf(" / ") !== -1) hasSlash = true;
5104        }
5105        if (hasSlash) {
5106          var firstTitle2 = item.variants[0].title || "";
5107          var slashParts = firstTitle2.split(" / ");
5108          if (slashParts.length >= 3) {
5109            components = [
5110              { label: "Jacket", optionIndex: 0, splitIndex: 0 },
5111              { label: "Waistcoat", optionIndex: 0, splitIndex: 1 },
5112              { label: "Trousers", optionIndex: 0, splitIndex: 2 }
5113            ];
5114          } else {
5115            components = [
5116              { label: "Jacket", optionIndex: 0, splitIndex: 0 },
5117              { label: "Trousers", optionIndex: 0, splitIndex: 1 }
5118            ];
5119          }
5120        }
5121      }
5122    }
5123
5124    if (components.length === 0) return null;
5125
5126    // Normalise component labels — if only one option found and it's "Size" or similar,
5127    // treat it as jacket size (primary variant) and add advisory selectors for trouser/waistcoat.
5128    var hasTrouser = components.some(function(c) { return c.label.toLowerCase().indexOf("trouser") !== -1 || c.label.toLowerCase().indexOf("pant") !== -1; });
5129    var hasWaistcoat = components.some(function(c) { return c.label.toLowerCase().indexOf("waistcoat") !== -1 || c.label.toLowerCase().indexOf("vest") !== -1; });
5130    var hasJacket = components.some(function(c) { return c.label.toLowerCase().indexOf("jacket") !== -1 || c.label.toLowerCase().indexOf("coat") !== -1 || c.label.toLowerCase().indexOf("chest") !== -1; });
5131
5132    // If component label is generic ("Size", "Title"), rename to "Jacket"
5133    if (!hasJacket && components.length === 1) {
5134      components[0].label = "Jacket";
5135      hasJacket = true;
5136    }
5137
5138    var tags = (item.tags || []).map(function(t) { return t.toLowerCase(); });
5139    var name = (item.name || "").toLowerCase();
5140    var isThreePiece = tags.indexOf("3-piece") !== -1 || tags.indexOf("three-piece") !== -1 ||
5141      tags.indexOf("three piece") !== -1 || name.indexOf("three") !== -1 || name.indexOf("3 piece") !== -1;
5142
5143    // Add advisory trouser selector if not already present
5144    if (!hasTrouser) {
5145      components.push({ label: "Trousers", optionIndex: -1, advisory: true });
5146    }
5147    // Add advisory waistcoat selector for ALL suit products (not just 3-piece)
5148    // Leonard Silver and most menswear retailers sell suits with waistcoats as standard
5149    if (!hasWaistcoat) {
5150      components.push({ label: "Waistcoat", optionIndex: -1, advisory: true });
5151    }
5152
5153    return components;
5154  }
5155
5156  function getUniqueOptionValues(variants, optionIndex, splitIndex) {
5157    var seen = {};
5158    var values = [];
5159    for (var v = 0; v < variants.length; v++) {
5160      var vr = variants[v];
5161      var val;
5162      if (splitIndex !== undefined) {
5163        var parts = (vr.title || "").split(" / ");
5164        val = (parts[splitIndex] || "").trim();
5165      } else {
5166        val = vr["option" + (optionIndex + 1)] || "";
5167      }
5168      if (val && !seen[val]) {
5169        seen[val] = true;
5170        var anyAvailable = false;
5171        for (var c = 0; c < variants.length; c++) {
5172          var cv;
5173          if (splitIndex !== undefined) {
5174            var cp = (variants[c].title || "").split(" / ");
5175            cv = (cp[splitIndex] || "").trim();
5176          } else {
5177            cv = variants[c]["option" + (optionIndex + 1)] || "";
5178          }
5179          if (cv === val && variants[c].available) { anyAvailable = true; break; }
5180        }
5181        values.push({ value: val, available: anyAvailable });
5182      }
5183    }
5184    return values;
5185  }
5186
5187  function findMatchingVariant(variants, selections) {
5188    for (var v = 0; v < variants.length; v++) {
5189      var vr = variants[v];
5190      var match = true;
5191      for (var s = 0; s < selections.length; s++) {
5192        var sel = selections[s];
5193        var val;
5194        if (sel.splitIndex !== undefined) {
5195          var parts = (vr.title || "").split(" / ");
5196          val = (parts[sel.splitIndex] || "").trim();
5197        } else {
5198          val = vr["option" + (sel.optionIndex + 1)] || "";
5199        }
5200        if (val !== sel.value) { match = false; break; }
5201      }
5202      if (match) return vr;
5203    }
5204    return null;
5205  }
5206
5207  function componentDesiredValue(comp, values, selectedId, variants) {
5208    // 1) the preselected variant's own value for this component
5209    if (selectedId && variants) {
5210      for (var i = 0; i < variants.length; i++) {
5211        if (String(variants[i].id) === String(selectedId)) {
5212          var v = variants[i];
5213          var val = null;
5214          if (comp.splitIndex !== undefined) val = (v.title || "").split(" / ")[comp.splitIndex];
5215          else if (comp.optionIndex >= 0) val = v["option" + (comp.optionIndex + 1)] || (v.title || "").split(" / ")[comp.optionIndex];
5216          if (val) {
5217            val = String(val).trim();
5218            for (var j = 0; j < values.length; j++) if (values[j].value === val) return val;
5219          }
5220          break;
5221        }
5222      }
5223    }
5224    // 2) the shopper's known FitCore sizes
5225    var szs = storedFitcoreSizes() || {};
5226    var lbl = (comp.label || "").toLowerCase();
5227    if (lbl.indexOf("fit") !== -1 && szs.fit) {
5228      // Match by fit family: FitCore "tailored" belongs with "Classic";
5229      // slim/x-slim with "Slim". Never preselect a contradicting cut.
5230      var classicWords = ["classic", "regular", "tailored"];
5231      var slimWords = ["slim", "x-slim", "xslim", "skinny", "contemporary"];
5232      var fitWord = String(szs.fit).toLowerCase().split(/[\s_]+/)[0];
5233      var family = classicWords.indexOf(fitWord) !== -1 ? classicWords : slimWords.indexOf(fitWord) !== -1 ? slimWords : null;
5234      if (family) {
5235        for (var fv = 0; fv < values.length; fv++) {
5236          var valWord = String(values[fv].value).toLowerCase().split(/[\s_]+/)[0];
5237          if (family.indexOf(valWord) !== -1 && values[fv].available !== false) return values[fv].value;
5238        }
5239      }
5240      return null;
5241    }
5242    if ((lbl.indexOf("shoe") !== -1 || lbl.indexOf("foot") !== -1) && szs.shoe) {
5243      // Shoe sizes are their own system ("UK 9 F" = system + number + Cheaney
5244      // width letter): match on the number, and on system/width only when
5245      // BOTH sides state one — "UK 9 F" belongs with a plain "9", never "9 G".
5246      var shoeRe = /^(UK|EU|US|JPN?)?(\d{1,2}(?:\.\d)?)([EFGH])?$/;
5247      var normSys = function (s) { return s === "JPN" ? "JP" : (s || null); };
5248      var wm = shoeRe.exec(String(szs.shoe).toUpperCase().replace(/[\s"”]+/g, ""));
5249      if (wm) {
5250        for (var sk = 0; sk < values.length; sk++) {
5251          var vm = shoeRe.exec(String(values[sk].value).toUpperCase().replace(/[\s"”]+/g, ""));
5252          if (vm && Number(vm[2]) === Number(wm[2]) &&
5253              (!normSys(vm[1]) || !normSys(wm[1]) || normSys(vm[1]) === normSys(wm[1])) &&
5254              (!vm[3] || !wm[3] || vm[3] === wm[3]) &&
5255              values[sk].available !== false) return values[sk].value;
5256        }
5257      }
5258      return null;
5259    }
5260    var want = (lbl.indexOf("trouser") !== -1 || lbl.indexOf("pant") !== -1) ? szs.trouser
5261      : (lbl.indexOf("waistcoat") !== -1 || lbl.indexOf("vest") !== -1) ? szs.size
5262      : (lbl.indexOf("jacket") !== -1 || lbl.indexOf("chest") !== -1 || lbl.indexOf("coat") !== -1 || lbl.indexOf("size") !== -1) ? szs.size
5263      : null;
5264    if (!want) return null;
5265    var wantU = String(want).toUpperCase().replace(/\s+/g, "");
5266    var num = (String(want).match(/\d{2}(\.\d)?/) || [])[0] || null;
5267    for (var k = 0; k < values.length; k++) {
5268      var valU = String(values[k].value).toUpperCase().replace(/\s+/g, "").replace(/^UK/, "");
5269      if (valU === wantU && values[k].available !== false) return values[k].value;
5270    }
5271    if (num) {
5272      for (var k2 = 0; k2 < values.length; k2++) {
5273        if (String(values[k2].value).indexOf(num) === 0 && values[k2].available !== false) return values[k2].value;
5274      }
5275    }
5276    return null;
5277  }
5278
5279  function renderVariantPicker(variants, selectedId, index, item) {
5280    if (!variants || variants.length === 0) return "";
5281    var suitComponents = item ? detectSuitComponents(item) : null;
5282
5283    // --- SUIT PRODUCT: structured multi-component selectors ---
5284    if (suitComponents) {
5285      var html = '<div class="sg-variant-picker sg-suit-picker" data-testid="variant-picker-' + index + '" data-suit="true">';
5286      for (var ci = 0; ci < suitComponents.length; ci++) {
5287        var comp = suitComponents[ci];
5288        html += '<div class="sg-variant-select-group">';
5289        // Pass-through option names ("Size", "Colour") already say what they are —
5290        // only append " Size" to garment-part labels (Jacket/Waistcoat/Trousers).
5291        var compLabelText = /size|colour|color|title/i.test(comp.label) ? comp.label : comp.label + ' Size';
5292        html += '<label class="sg-variant-select-label">' + escapeHtml(compLabelText) + '</label>';
5293
5294        if (comp.advisory) {
5295          // Advisory selector: captures preference but does not resolve a Shopify variant.
5296          // Standard trouser waist sizes (inches) or waistcoat chest sizes.
5297          var isWaistcoat = comp.label.toLowerCase().indexOf("waistcoat") !== -1;
5298          var advisorySizes = isWaistcoat
5299            ? ["36", "38", "40", "42", "44", "46", "48"]
5300            : ["28", "30", "32", "34", "36", "38", "40", "42", "44"];
5301          html += '<select class="sg-variant-select sg-component-select sg-advisory-select"';
5302          html += ' data-option-index="-1" data-advisory="true"';
5303          html += ' data-component="' + comp.label.toLowerCase() + '"';
5304          html += ' data-testid="variant-select-' + index + '-' + ci + '">';
5305          html += '<option value="">Select ' + escapeHtml(compLabelText.toLowerCase()) + '</option>';
5306          var advStored = storedFitcoreSizes() || {};
5307          var advSource = isWaistcoat ? advStored.size : advStored.trouser;
5308          var advDesired = advSource ? (String(advSource).match(/\d{2}/) || [])[0] : null;
5309          for (var ai = 0; ai < advisorySizes.length; ai++) {
5310            html += '<option value="' + escapeHtml(advisorySizes[ai]) + '"' + (advisorySizes[ai] === advDesired ? ' selected' : '') + '>' + escapeHtml(advisorySizes[ai]) + '</option>';
5311          }
5312          html += '</select>';
5313          html += '<span class="sg-advisory-note">Preference noted — not a stock size</span>';
5314        } else {
5315          // Primary selector: maps to an actual Shopify variant.
5316          var values = getUniqueOptionValues(variants, comp.optionIndex, comp.splitIndex);
5317          html += '<select class="sg-variant-select sg-component-select"';
5318          html += ' data-option-index="' + comp.optionIndex + '"';
5319          if (comp.splitIndex !== undefined) html += ' data-split-index="' + comp.splitIndex + '"';
5320          html += ' data-component="' + comp.label.toLowerCase() + '"';
5321          html += ' data-testid="variant-select-' + index + '-' + ci + '">';
5322          html += '<option value="">Select ' + escapeHtml(compLabelText.toLowerCase()) + '</option>';
5323          var compDesired = componentDesiredValue(comp, values, selectedId, variants);
5324          for (var vi = 0; vi < values.length; vi++) {
5325            var ov = values[vi];
5326            html += '<option value="' + escapeHtml(ov.value) + '"' + (ov.available ? '' : ' disabled') + (ov.value === compDesired && ov.available ? ' selected' : '') + '>';
5327            html += escapeHtml(ov.value) + (ov.available ? '' : ' (Sold Out)');
5328            html += '</option>';
5329          }
5330          html += '</select>';
5331        }
5332        html += '</div>';
5333      }
5334      html += '</div>';
5335      return html;
5336    }
5337
5338    // --- STANDARD PRODUCT: one dropdown per option dimension, not raw variant dump ---
5339    var options = item && item.options ? item.options : null;
5340
5341    if (options && options.length > 0) {
5342      var html = '<div class="sg-variant-picker" data-testid="variant-picker-' + index + '">';
5343      for (var oi = 0; oi < options.length; oi++) {
5344        var rawOptLabel = options[oi] || ("Option " + (oi + 1));
5345        // Strip redundant trailing " Size" — e.g. "Fit Size" → "Fit", "Size Size" → "Size"
5346        var optLabelParts = rawOptLabel.trim().split(/\s+/);
5347        var optLabel = (optLabelParts.length > 1 && optLabelParts[optLabelParts.length - 1].toLowerCase() === 'size')
5348          ? optLabelParts.slice(0, -1).join(' ')
5349          : rawOptLabel;
5350        var values = getUniqueOptionValues(variants, oi);
5351        if (values.length === 0) continue;
5352
5353        html += '<div class="sg-variant-select-group">';
5354        html += '<label class="sg-variant-select-label">' + escapeHtml(optLabel) + '</label>';
5355
5356        if (values.length === 1 && options.length === 1) {
5357          // Single value, single option: show as static label — no dropdown needed
5358          html += '<span class="sg-variant-static-value">' + escapeHtml(values[0].value);
5359          if (!values[0].available) html += ' <span style="color:#ef4444">(Sold Out)</span>';
5360          html += '</span>';
5361        } else {
5362          var stdDesired = componentDesiredValue({ optionIndex: oi, label: optLabel }, values, selectedId, variants);
5363          html += '<select class="sg-variant-select sg-component-select"';
5364          html += ' data-option-index="' + oi + '"';
5365          html += ' data-testid="variant-select-' + index + '-' + oi + '">';
5366          html += '<option value="">Select ' + escapeHtml(optLabel.toLowerCase()) + '</option>';
5367          for (var vi2 = 0; vi2 < values.length; vi2++) {
5368            var ov2 = values[vi2];
5369            html += '<option value="' + escapeHtml(ov2.value) + '"' + (ov2.value === stdDesired && ov2.available !== false ? ' selected' : '') + (ov2.available ? '' : ' disabled') + '>';
5370            html += escapeHtml(ov2.value) + (ov2.available ? '' : ' (Sold Out)');
5371            html += '</option>';
5372          }
5373          html += '</select>';
5374        }
5375        html += '</div>';
5376      }
5377      html += '</div>';
5378      return html;
5379    }
5380
5381    // --- FALLBACK: no options metadata, single size dropdown using variant titles ---
5382    if (variants.length <= 1) return "";
5383    var html = '<div class="sg-variant-picker" data-testid="variant-picker-' + index + '">';
5384    html += '<div class="sg-variant-select-group">';
5385    html += '<label class="sg-variant-select-label">Size</label>';
5386    html += '<select class="sg-variant-select sg-component-select" data-option-index="0"';
5387    html += ' data-testid="variant-select-' + index + '">';
5388    html += '<option value="">Select size</option>';
5389    for (var v = 0; v < variants.length; v++) {
5390      var vr = variants[v];
5391      var lbl = vr.option1 || vr.title || String(v + 1);
5392      html += '<option value="' + escapeHtml(lbl) + '"' + (vr.available ? '' : ' disabled') + '>';
5393      html += escapeHtml(lbl) + (vr.available ? '' : ' (Sold Out)');
5394      html += '</option>';
5395    }
5396    html += '</select>';
5397    html += '</div>';
5398    html += '</div>';
5399    return html;
5400  }
5401
5402  function renderProductCard(item, index, btnIconColor, viewIconColor) {
5403    var itemUrl = item.url || "";
5404    if (shopDomain && itemUrl && itemUrl.charAt(0) === "/") {
5405      itemUrl = "https://" + shopDomain + itemUrl;
5406    }
5407    // Normalise protocol-relative URLs (//cdn.shopify.com/...) → https://...
5408    if (item.image && item.image.startsWith('//')) item.image = 'https:' + item.image;
5409    var hasImage = item.image && item.image.length > 10 && (item.image.startsWith('http://') || item.image.startsWith('https://'));
5410    if (!hasImage) console.log('[StyleGuru] No image for product:', item.name, '| raw:', item.image);
5411    var imgInner = hasImage
5412      ? '<img class="sg-product-img" src="' + escapeHtml(item.image) + '" alt="' + escapeHtml(item.name) + '" loading="lazy" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'" /><div class="sg-product-img-placeholder" style="display:none">
5412' + sparklesIcon() + '</div>'
5413      : '<div class="sg-product-img-placeholder">' + sparklesIcon() + '</div>';
5414    var imgHtml = hasImage
5415      ? '<a class="sg-img-zoom-trigger" href="' + escapeHtml(itemUrl) + '" data-img="' + escapeHtml(item.image) + '" data-name="' + escapeHtml(item.name) + '" tabindex="-1" aria-label="View ' + escapeHtml(item.name) + ' full size">' + imgInner + '</a>'
5416      : imgInner;
5417
5418    var variantHtml = "";
5419    var hasMultipleVariants = item.variants && item.variants.length > 1;
5420    if (!hasMultipleVariants && item.variantTitle) {
5421      variantHtml = '<div class="sg-product-variant"><span class="sg-product-variant-dot"></span> Size: ' + escapeHtml(item.variantTitle) + '</div>';
5422    }
5423
5424    var roleHtml = "";
5425    if (item.role && item.role !== "complement") {
5426      var roleLabel = item.role === "hero" ? "Key Piece" : item.role === "accessory" ? "Accessory" : "";
5427      if (roleLabel) {
5428        roleHtml = '<span class="sg-item-role">' + roleLabel + '</span>';
5429      }
5430    }
5431
5432    var cartDataAttrs = '';
5433    // Sized product, size not established → gate Add until the picker is used.
5434    if (hasMultipleVariants && !item.variantId) {
5435      cartDataAttrs = ' data-size-required="1"';
5436    } else if (item.variantId) {
5437      cartDataAttrs = ' data-variant-id="' + escapeHtml(item.variantId) + '"';
5438    } else if (item.productId) {
5439      cartDataAttrs = ' data-product-id="' + escapeHtml(item.productId) + '"';
5440    }
5441
5442    var isSoldOut = item.soldOut === true;
5443
5444    var html = '<div class="sg-product-card" data-testid="card-product-' + index + '">';
5445    html += imgHtml;
5446    html += '<div class="sg-product-info">';
5447    if (roleHtml) html += roleHtml;
5448    html += '<div class="sg-product-name"><a href="' + escapeHtml(itemUrl) + '" target="_blank" rel="noopener">' + escapeHtml(item.name) + '</a></div>';
5449    var displayPrice = formatDisplayPrice(item.formattedPrice || item.price, item.currencyCode);
5450    if (displayPrice) html += '<div class="sg-product-price">' + escapeHtml(displayPrice) + '</div>';
5451    if (item.sizedForYou && item.variantTitle) {
5452      html += '<div class="sg-sized-chip" style="display:inline-block;margin:2px 0 4px;padding:2px 8px;border-radius:999px;font-size:11px;font-weight:600;background:rgba(' + (hexToRgb(config.primaryColor || "#C6A85B") || "198,168,91") + ',0.14);color:' + (config.primaryColor || "#C6A85B") + ';" data-testid="chip-sized-' + index + '">Sized for you · ' + escapeHtml(item.variantTitle) + '</div>';
5453    }
5454    html += variantHtml;
5455    html += '</div>';
5456    if (hasMultipleVariants) {
5457      html += renderVariantPicker(item.variants, item.variantId, index, item);
5458    }
5459    html += '<div class="sg-product-actions">';
5460    if (isSoldOut) {
5461      html += '<button class="sg-btn-cart sg-sold-out" data-testid="button-add-cart-' + index + '" disabled>Sold Out</button>';
5462    } else {
5463      html += '<button class="sg-btn-cart" data-testid="button-add-cart-' + index + '"' + cartDataAttrs + '>' + cartIcon(btnIconColor) + cartLabel() + '</button>';
5464    }
5465    html += '<a class="sg-btn-view" href="' + escapeHtml(itemUrl) + '" target="_blank" rel="noopener" data-testid="link-view-' + index + '">View ' + externalIcon(viewIconColor) + '</a>';
5466    html += '</div>';
5467    html += '</div>';
5468    return html;
5469  }
5470
5471  function resolveAndAddToCart(itemBtn, callback) {
5472    var variantId = itemBtn.getAttribute("data-variant-id");
5473    var productId = itemBtn.getAttribute("data-product-id");
5474    var iconColor = getCartIconColor();
5475
5476    if (variantId) {
5477      doCartAddWithCallback(variantId, itemBtn, callback);
5478    } else if (productId) {
5479      var handle = productId;
5480      var viewLink = itemBtn.closest(".sg-product-card");
5481      if (viewLink) {
5482        var linkEl = viewLink.querySelector(".sg-btn-view");
5483        if (linkEl) {
5484          var href = linkEl.getAttribute("href") || "";
5485          var hMatch = href.match(/\/products\/([^?#]+)/);
5486          if (hMatch) handle = hMatch[1];
5487        }
5488      }
5489      var productUrl = "https://" + shopDomain + "/products/" + handle + ".json";
5490      var xhr = new XMLHttpRequest();
5491      xhr.open("GET", productUrl);
5492      xhr.onreadystatechange = function() {
5493        if (xhr.readyState === 4) {
5494          if (xhr.status === 200) {
5495            try {
5496              var data = JSON.parse(xhr.responseText);
5497              var vid = data.product && data.product.variants && data.product.variants[0] ? data.product.variants[0].id : null;
5498              if (vid) {
5499                doCartAddWithCallback(vid, itemBtn, callback);
5500              } else {
5501                callback(false);
5502              }
5503            } catch(e) { callback(false); }
5504          } else {
5505            callback(false);
5506          }
5507        }
5508      };
5509      xhr.send();
5510    } else {
5511      callback(false);
5512    }
5513  }
5514
5515  function doCartAddWithCallback(variantId, itemBtn, callback) {
5516    var xhr = new XMLHttpRequest();
5517    xhr.open("POST", "/cart/add.js");
5518    xhr.setRequestHeader("Content-Type", "application/json");
5519    xhr.onreadystatechange = function() {
5520      if (xhr.readyState === 4) {
5521        if (xhr.status === 200) {
5522          markCartAttributed();
5523          itemBtn.classList.add("sg-added");
5524          itemBtn.innerHTML = checkIcon("#fff") + ' Added';
5525          itemBtn.disabled = true;
5526          callback(true);
5527        } else {
5528          callback(false);
5529        }
5530      }
5531    };
5532    xhr.send(JSON.stringify({ id: variantId, quantity: 1 }));
5533  }
5534
5535  function showBundleToast(bundleEl, message, type) {
5536    var existing = bundleEl.querySelector(".sg-bundle-toast");
5537    if (existing) existing.remove();
5538    var toast = document.createElement("div");
5539    toast.className = "sg-bundle-toast sg-toast-" + (type || "success");
5540    var icon = type === "warn"
5541      ? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/>
5541<line x1="12" y1="17" x2="12.01" y2="17"/></svg>'
5542      : type === "error"
5543        ? '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>'
5544        : '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>';
5545    toast.innerHTML = icon + '<span>' + message + '</span>';
5546    var footer = bundleEl.querySelector(".sg-bundle-footer");
5547    if (footer) {
5548      bundleEl.insertBefore(toast, footer);
5549    } else {
5550      bundleEl.appendChild(toast);
5551    }
5552    setTimeout(function() { if (toast.parentNode) toast.remove(); }, 5000);
5553  }
5554
5555  function showSuitSelectionSummary(bundleEl, cs) {
5556    // Remove any prior summary
5557    var existing = bundleEl.querySelector(".sg-suit-summary");
5558    if (existing) existing.remove();
5559
5560    var hasJacket   = cs.jacketSize   && cs.jacketSize.trim();
5561    var hasTrouser  = cs.trouserSize  && cs.trouserSize.trim();
5562    var hasWaistcoat = cs.waistcoatSize && cs.waistcoatSize.trim();
5563    if (!hasJacket && !hasTrouser && !hasWaistcoat) return;
5564
5565    var summaryEl = document.createElement("div");
5566    summaryEl.className = "sg-suit-summary";
5567
5568    var html = '<span class="sg-suit-summary-label">Your selections:</span>';
5569    if (hasJacket)    html += '<span class="sg-suit-summary-tag">Jacket ' + escapeHtml(cs.jacketSize) + '</span>';
5570    if (hasTrouser)   html += '<span class="sg-suit-summary-tag">Trouser ' + escapeHtml(cs.trouserSize) + '</span>';
5571    if (hasWaistcoat) html += '<span class="sg-suit-summary-tag">Waistcoat ' + escapeHtml(cs.waistcoatSize) + '</span>';
5572
5573    // Advisory note only when at least one advisory (trouser/waistcoat) preference is present
5574    if (hasTrouser || hasWaistcoat) {
5575      html += '<span class="sg-suit-summary-advisory">Trouser &amp; waistcoat sizes are recorded as your preferences — not direct stock reservations.</span>';
5576    }
5577
5578    summaryEl.innerHTML = html;
5579
5580    var footer = bundleEl.querySelector(".sg-bundle-footer");
5581    if (footer) {
5582      bundleEl.insertBefore(summaryEl, footer);
5583    } else {
5584      bundleEl.appendChild(summaryEl);
5585    }
5586    // Auto-clear on successful add (will be removed by finishBundleAdd rebuild)
5587  }
5588
5589  function addAllToCart(btn) {
5590    var bundleEl = btn.closest(".sg-bundle");
5591    if (!bundleEl) return;
5592    var itemBtns = bundleEl.querySelectorAll(".sg-btn-cart:not(.sg-sold-out):not(:disabled)");
5593    if (itemBtns.length === 0) return;
5594
5595    var pickers = bundleEl.querySelectorAll(".sg-variant-picker, .sg-inline-variant-select");
5596    var unselected = [];
5597    for (var p = 0; p < pickers.length; p++) {
5598      var picker = pickers[p];
5599      if (picker.classList.contains("sg-variant-picker")) {
5600        // Only check non-advisory selects — advisory selects are optional preference fields
5601        var selects = picker.querySelectorAll(".sg-variant-select:not([data-advisory='true'])");
5602        var hasUnselected = false;
5603        for (var si = 0; si < selects.length; si++) {
5604          if (!selects[si].value) { hasUnselected = true; break; }
5605        }
5606        if (hasUnselected) {
5607          var card = picker.closest(".sg-product-card");
5608          var nameEl = card ? card.querySelector(".sg-product-name") : null;
5609          unselected.push(nameEl ? nameEl.textContent.trim() : "an item");
5610        }
5611      }
5612    }
5613    if (unselected.length > 0) {
5614      showBundleToast(bundleEl, "Please select your size for: " + unselected.join(", "), "warn");
5615      return;
5616    }
5617
5618    var lines = [];
5619    for (var i = 0; i < itemBtns.length; i++) {
5620      var vid = itemBtns[i].getAttribute("data-variant-id");
5621      if (vid) {
5622        lines.push({ variantId: vid, quantity: 1 });
5623      }
5624    }
5625
5626    if (lines.length === 0) {
5627      showBundleToast(bundleEl, "No variant information available. Try adding items individually.", "warn");
5628      return;
5629    }
5630
5631    // Collect customSelections from suit pickers (jacket/trouser/waistcoat sizes)
5632    var customSelections = {};
5633    // Build a set of numeric variant IDs belonging to suit products, so we can
5634    // attach Shopify line item properties to those specific cart lines.
5635    var suitVariantNumericIds = {};
5636    var suitPickers = bundleEl.querySelectorAll(".sg-suit-picker");
5637    for (var sp = 0; sp < suitPickers.length; sp++) {
5638      var jacketSel = suitPickers[sp].querySelector("[data-component='jacket']");
5639      var trouserSel = suitPickers[sp].querySelector("[data-component='trousers']");
5640      var waistcoatSel = suitPickers[sp].querySelector("[data-component='waistcoat']");
5641      if (jacketSel && jacketSel.value) customSelections.jacketSize = jacketSel.value;
5642      if (trouserSel && trouserSel.value) customSelections.trouserSize = trouserSel.value;
5643      if (waistcoatSel && waistcoatSel.value) customSelections.waistcoatSize = waistcoatSel.value;
5644
5645      // Record which variant ID belongs to this suit card
5646      var suitCard = suitPickers[sp].closest(".sg-product-card");
5647      if (suitCard) {
5648        var suitCartBtn = suitCard.querySelector(".sg-btn-cart");
5649        if (suitCartBtn) {
5650          var suitVid = suitCartBtn.getAttribute("data-variant-id");
5651          if (suitVid) suitVariantNumericIds[suitVid.replace(/\D/g, "")] = true;
5652        }
5653      }
5654    }
5655
5656    // Show confirmation summary above the Add button if any suit sizes were selected
5657    if (Object.keys(customSelections).length > 0) {
5658      showSuitSelectionSummary(bundleEl, customSelections);
5659    }
5660
5661    trackEvent("add_bundle_clicked", { itemCount: lines.length });
5662
5663    btn.disabled = true;
5664    var iconColor = getCartIconColor();
5665    btn.innerHTML = '<span style="width:16px;height:16px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;display:inline-block;animation:sg-spin 0.6s linear infinite"></span> Adding ' + lines.length + ' items...';
5666
5667    var styleEl = document.getElementById("sg-spin-style");
5668    if (!styleEl) {
5669      styleEl = document.createElement("style");
5670      styleEl.id = "sg-spin-style";
5671      styleEl.textContent = '@keyframes sg-spin { to { transform: rotate(360deg); } }';
5672      document.head.appendChild(styleEl);
5673    }
5674
5675    var bundleUrl = apiUrl("/cart/add-bundle");
5676    var xhr = new XMLHttpRequest();
5677    xhr.open("POST", bundleUrl);
5678    xhr.setRequestHeader("Content-Type", "application/json");
5679    xhr.onreadystatechange = function() {
5680      if (xhr.readyState !== 4) return;
5681
5682      if (xhr.status === 429) {
5683        btn.disabled = false;
5684        btn.innerHTML = cartIcon(iconColor) + ' Add Complete Outfit';
5685        showBundleToast(bundleEl, "Please wait a moment before trying again.", "warn");
5686        return;
5687      }
5688
5689      try {
5690        var data = JSON.parse(xhr.responseText);
5691
5692        if (!data.ok) {
5693          btn.disabled = false;
5694          btn.innerHTML = cartIcon(iconColor) + ' Add Complete Outfit';
5695          showBundleToast(bundleEl, data.error || "Failed to add to cart.", "error");
5696          return;
5697        }
5698
5699        var addedCount = data.added ? data.added.length : 0;
5700        var removedCount = data.removedUnavailable ? data.removedUnavailable.length : 0;
5701
5702        if (addedCount > 0) {
5703          var cartItems = [];
5704          for (var ci = 0; ci < data.added.length; ci++) {
5705            var numId = data.added[ci].variantId.replace(/\D/g, "");
5706            var cartItem = { id: parseInt(numId, 10), quantity: data.added[ci].quantity };
5707
5708            // Attach Shopify line item properties for suit items so staff can see
5709            // the customer's size selections in the Shopify admin order view.
5710            if (suitVariantNumericIds[numId] && Object.keys(customSelections).length > 0) {
5711              cartItem.properties = {};
5712              if (customSelections.jacketSize)    cartItem.properties["Jacket Size"]                   = customSelections.jacketSize;
5713              if (customSelections.trouserSize)   cartItem.properties["Trouser Size (your preference)"]  = customSelections.trouserSize;
5714              if (customSelections.waistcoatSize) cartItem.properties["Waistcoat Size (your preference)"] = customSelections.waistcoatSize;
5715            }
5716
5717            cartItems.push(cartItem);
5718          }
5719          var cartXhr = new XMLHttpRequest();
5720          cartXhr.open("POST", "/cart/add.js");
5721          cartXhr.setRequestHeader("Content-Type", "application/json");
5722          cartXhr.onreadystatechange = function() {
5723            if (cartXhr.readyState === 4) {
5724              if (cartXhr.status === 200 || cartXhr.status === 201) {
5725                finishBundleAdd(btn, bundleEl, iconColor, itemBtns, data);
5726              } else if (cartXhr.status === 422) {
5727                btn.disabled = false;
5728                btn.innerHTML = cartIcon(iconColor) + ' Add Complete Outfit';
5729                showBundleToast(bundleEl, "Some items could not be added. They may be sold out.", "warn");
5730              } else {
5731                btn.disabled = false;
5732                btn.innerHTML = cartIcon(iconColor) + ' Add Complete Outfit';
5733                showBundleToast(bundleEl, "Could not add to cart. Please try again.", "error");
5734              }
5735            }
5736          };
5737          cartXhr.send(JSON.stringify({ items: cartItems }));
5738        } else {
5739          finishBundleAdd(btn, bundleEl, iconColor, itemBtns, data);
5740        }
5741      } catch(e) {
5742        btn.disabled = false;
5743        btn.innerHTML = cartIcon(iconColor) + ' Add Complete Outfit';
5744        showBundleToast(bundleEl, "Something went wrong. Please try again.", "error");
5745      }
5746    };
5747    xhr.send(JSON.stringify({ lines: lines, customSelections: Object.keys(customSelections).length > 0 ? customSelections : undefined }));
5748  }
5749
5750  function finishBundleAdd(btn, bundleEl, iconColor, itemBtns, data) {
5751    var addedCount = data.added ? data.added.length : 0;
5752    var removedCount = data.removedUnavailable ? data.removedUnavailable.length : 0;
5753    var totalRequested = addedCount + removedCount;
5754
5755    if (addedCount > 0) {
5756      markCartAttributed();
5757                  trackEvent("add_bundle_success", { addedCount: addedCount, removedCount: removedCount });
5758      btn.classList.add("sg-added");
5759      btn.innerHTML = checkIcon("#fff") + ' ' + addedCount + ' Added to Bag';
5760
5761      var addedIds = {};
5762      if (data.added) {
5763        for (var a = 0; a < data.added.length; a++) {
5764          var numId = data.added[a].variantId.replace(/\D/g, "");
5765          addedIds[numId] = true;
5766          addedIds[data.added[a].variantId] = true;
5767        }
5768      }
5769
5770      for (var b = 0; b < itemBtns.length; b++) {
5771        var vid = itemBtns[b].getAttribute("data-variant-id") || "";
5772        var numVid = vid.replace(/\D/g, "");
5773        if (addedIds[vid] || addedIds[numVid]) {
5774          itemBtns[b].classList.add("sg-added");
5775          itemBtns[b].innerHTML = checkIcon("#fff") + ' Added';
5776          itemBtns[b].disabled = true;
5777        }
5778      }
5779    }
5780
5781    if (removedCount > 0 && addedCount > 0) {
5782      showBundleToast(bundleEl, "Added " + addedCount + " items. " + removedCount + " item" + (removedCount > 1 ? "s" : "") + " sold out.", "warn");
5783    } else if (removedCount > 0 && addedCount === 0) {
5784      showBundleToast(bundleEl, "All items are currently sold out.", "error");
5785    } else if (addedCount > 0) {
5786      showBundleToast(bundleEl, addedCount + " item" + (addedCount > 1 ? "s" : "") + " added to your bag!", "success");
5787    }
5788
5789    setTimeout(function() {
5790      btn.classList.remove("sg-added");
5791      btn.innerHTML = cartIcon(iconColor) + ' Add Complete Outfit';
5792      btn.disabled = false;
5793      var addedBtns = bundleEl.querySelectorAll(".sg-btn-cart.sg-added");
5794      for (var c = 0; c < addedBtns.length; c++) {
5795        addedBtns[c].classList.remove("sg-added");
5796        addedBtns[c].innerHTML = cartIcon(iconColor) + cartLabel();
5797        addedBtns[c].disabled = false;
5798      }
5799    }, 4000);
5800  }
5801
5802  // The conversation builds ONE look across turns: every piece the stylist
5803  // presents joins sessionLook (deduped) so earlier items — the black oxfords —
5804  // stay in the pane when the shirts arrive. The zone's ✕ starts a fresh look.
5805  var sessionLook = [];
5806
5807  function sgLookKey(item) {
5808    return String(item.productId || item.variantId || item.url || item.name || "");
5809  }
5810
5811  function mergeIntoSessionLook(outfit) {
5812    if (!outfit || !outfit.items) return;
5813    for (var i = 0; i < outfit.items.length; i++) {
5814      var it = outfit.items[i];
5815      var k = sgLookKey(it);
5816      if (!k) continue;
5817      var exists = false;
5818      for (var j = 0; j < sessionLook.length; j++) {
5819        if (sgLookKey(sessionLook[j]) === k) { exists = true; break; }
5820      }
5821      if (!exists) sessionLook.push(it);
5822    }
5823    // A look, not a wardrobe — keep the freshest pieces.
5824    if (sessionLook.length > 8) sessionLook = sessionLook.slice(sessionLook.length - 8);
5825  }
5826
5827  function sessionLookTotal() {
5828    var total = 0, symbol = "";
5829    for (var i = 0; i < sessionLook.length; i++) {
5830      var p = sessionLook[i].price;
5831      if (p === undefined || p === null) continue;
5832      var s = String(p);
5833      if (!symbol) { var m = s.match(/^[^\d\s.,-]+/); if (m) symbol = m[0]; }
5834      var n = parseFloat(s.replace(/[^0-9.]/g, ""));
5835      if (!isNaN(n)) total += n;
5836    }
5837    return total > 0 ? (symbol || "") + total.toFixed(2) : "";
5838  }
5839
5840  function renderSessionLook() {
5841    if (!sessionLook.length) { hideCompleteLook(); return; }
5842    showCompleteLook({
5843      title: "Your look",
5844      items: sessionLook,
5845      bundlePrice: sessionLookTotal(),
5846      removable: true,
5847    });
5848  }
5849
5850  function removeFromSessionLook(key) {
5851    var removedName = "";
5852    for (var i = sessionLook.length - 1; i >= 0; i--) {
5853      if (sgLookKey(sessionLook[i]) === key) {
5854        removedName = sessionLook[i].name || "";
5855        sessionLook.splice(i, 1);
5856      }
5857    }
5858    trackEvent("look_item_removed", { name: removedName, remaining: sessionLook.length });
5859    renderSessionLook();
5860    showDiscussedProducts([{ items: sessionLook }]);
5861  }
5862
5863  // The grid mirrors the conversation: when the stylist presents pieces,
5864  // show those pieces rather than reloading the generic catalogue.
5865  function showDiscussedProducts(outfits) {
5866    if (!outfits || !outfits.length) return;
5867    try {
5868      renderProducts(outfits, { noTabSwitch: true });
5869      var lbl = document.getElementById("sg-products-header-label");
5870      if (lbl) lbl.textContent = "In this look";
5871    } catch (e) {
5872      try { console.error("[StyleGuru] showDiscussedProducts error:", e); } catch (e2) {}
5873    }
5874  }
5875
5876  // Chat rows are references — the pane is where products are handled.
5877  // Jump there and briefly spotlight the matching card.
5878  function focusPaneProduct(item) {
5879    switchTab("products");
5880    var grid = document.getElementById("sg-products-grid");
5881    if (!grid || !item) return;
5882    var cards = grid.querySelectorAll(".sg-product-card");
5883    var target = null;
5884    for (var i = 0; i < cards.length; i++) {
5885      var d = cards[i]._sgItemData;
5886      if (d && ((d.productId && item.productId && String(d.productId) === String(item.productId)) || (d.name && d.name === item.name))) {
5887        target = cards[i];
5888        break;
5889      }
5890    }
5891    if (!target) {
5892      // The grid has moved on since this chat row was shown — render the
5893      // tapped product into the pane as a standalone card so the tap always
5894      // lands somewhere. Clear/browse restores the full catalogue.
5895      renderProducts([{ title: item.name, items: [item] }], { noTabSwitch: true, source: "browse" });
5896      target = grid.querySelector(".sg-product-card");
5897    }
5898    if (target) {
5899      target.scrollIntoView({ behavior: "smooth", block: "center" });
5900      target.classList.remove("sg-pane-flash");
5901      void target.offsetWidth; // restart the animation on repeat taps
5902      target.classList.add("sg-pane-flash");
5903    }
5904  }
5905
5906  function renderProducts(outfits, opts) {
5907    var grid = document.getElementById("sg-products-grid");
5908    var countEl = document.getElementById("sg-product-count");
5909    if (!grid) return;
5910
5911    var totalItems = 0;
5912    for (var i = 0; i < outfits.length; i++) {
5913      if (outfits[i].items) totalItems += outfits[i].items.length;
5914    }
5915
5916    if (countEl) {
5917      countEl.textContent = totalItems > 0 ? "(" + totalItems + " items)" : "";
5918    }
5919
5920    // Update the mobile tab badge with item count
5921    var tabCountEl = document.getElementById("sg-tab-count");
5922    if (tabCountEl) {
5923      if (totalItems > 0) {
5924        tabCountEl.textContent = totalItems;
5925        tabCountEl.style.display = "inline-block";
5926      } else {
5927        tabCountEl.style.display = "none";
5928      }
5929    }
5930    var browseCountEl = document.getElementById("sg-browse-count");
5931    if (browseCountEl) {
5932      if (totalItems > 0 && totalItems <= 99 && !(opts && opts.source === "browse")) {
5933        browseCountEl.textContent = totalItems;
5934        browseCountEl.style.display = "block";
5935      } else {
5936        browseCountEl.style.display = "none";
5937      }
5938    }
5939
5940    if (totalItems === 0) {
5941      grid.innerHTML = '<div class="sg-empty-products">' + searchIcon() + '<h4>No recommendations yet</h4><p>Ask me about outfits, sizes, or styles and I\'ll show you matching products here.</p></div>';
5942      return;
5943    }
5944
5945    // Auto-switch to products tab on mobile when recommendations arrive —
5946    // suppressed mid-chat, where the "View the outfit" chip handles it.
5947    // Auto-jump to the products tab only for AI recommendations — background
5948    // catalogue loads must never yank a mobile user out of the chat.
5949    if (window.innerWidth <= 640 && !(opts && opts.noTabSwitch) && !(opts && opts.source === "browse")) {
5950      switchTab("products");
5951    }
5952
5953    if (opts && opts.source === "browse") {
5954      // Browsing is not a recommendation — log it lightly so grid loads stop
5955      // polluting the "top recommended products" numbers with the whole catalogue.
5956      trackEvent("catalog_viewed", { itemCount: totalItems });
5957    } else {
5958      var trackItems = [];
5959      for (var ti = 0; ti < outfits.length; ti++) {
5960        if (outfits[ti].items) {
5961          for (var tj = 0; tj < outfits[ti].items.length; tj++) {
5962            var tIt = outfits[ti].items[tj];
5963            trackItems.push({ name: tIt.name || "", productId: tIt.productId || "" });
5964          }
5965        }
5966      }
5967      var bsPayload = { source: "ai", bundleCount: outfits.length, itemCount: totalItems, items: trackItems.slice(0, 20) };
5968      if (opts && opts.occasion) bsPayload.occasion = opts.occasion;
5969      else if (window.__sgLastOccasion) bsPayload.occasion = window.__sgLastOccasion;
5970      trackEvent("bundles_shown", bsPayload);
5971    }
5972
5973    var btnIconColor = isLightColor(config.primaryColor || "#C6A85B") ? "#111" : "#fff";
5974    var viewIconColor = isLightColor(config.backgroundColor || "#fff") ? "#555" : "#ccc";
5975    var pc = config.primaryColor || "#C6A85B";
5976
5977    var html = "";
5978    var globalIdx = 0;
5979
5980    for (var b = 0; b < outfits.length; b++) {
5981      var bundle = outfits[b];
5982      if (!bundle.items || bundle.items.length === 0) continue;
5983
5984      var hasMultipleBundles = bundle.items.length >= 2;
5985
5986      if (hasMultipleBundles) {
5987        html += '<div class="sg-bundle" data-testid="bundle-' + b + '">';
5988        html += '<div class="sg-bundle-header">';
5989        html += '<div class="sg-bundle-title">' + bundleIcon(pc) + ' ' + escapeHtml(bundle.title || "Complete Look") + '</div>';
5990        html += '<div class="sg-bundle-meta">';
5991        if (bundle.bundlePrice) {
5992          html += '<span class="sg-bundle-price">' + escapeHtml(bundle.bundlePrice) + ' total</span>';
5993        }
5994        html += '<span class="sg-bundle-count">' + bundle.items.length + ' items</span>';
5995        html += '</div>';
5996        html += '</div>';
5997        html += '<div class="sg-bundle-items">';
5998      }
5999
6000      for (var j = 0; j < bundle.items.length; j++) {
6001        html += renderProductCard(bundle.items[j], globalIdx, btnIconColor, viewIconColor);
6002        globalIdx++;
6003      }
6004
6005      if (hasMultipleBundles) {
6006        html += '</div>';
6007        var availableCount = 0;
6008        for (var ac = 0; ac < bundle.items.length; ac++) {
6009          if (!bundle.items[ac].soldOut && (bundle.items[ac].variantId || bundle.items[ac].productId)) availableCount++;
6010        }
6011        if (availableCount >= 2) {
6012          html += '<div class="sg-bundle-footer">';
6013          html += '<button class="sg-btn-add-all" data-testid="button-add-all-' + b + '">' + cartIcon(btnIconColor) + ' Add Complete Outfit</button>';
6014          html += '</div>';
6015        }
6016        html += '</div>';
6017      }
6018    }
6019
6020    var hasBundles = outfits.length > 0 && outfits.some(function(o) { return o.items && o.items.length >= 2; });
6021    if (hasBundles) {
6022      grid.classList.add("sg-has-bundles");
6023    } else {
6024      grid.classList.remove("sg-has-bundles");
6025    }
6026
6027    grid.innerHTML = html;
6028
6029    var allCards = grid.querySelectorAll(".sg-product-card");
6030    var flatItems = [];
6031    for (var fi = 0; fi < outfits.length; fi++) {
6032      if (outfits[fi].items) {
6033        for (var fj = 0; fj < outfits[fi].items.length; fj++) {
6034          flatItems.push(outfits[fi].items[fj]);
6035        }
6036      }
6037    }
6038    for (var ci = 0; ci < allCards.length && ci < flatItems.length; ci++) {
6039      allCards[ci]._sgItemData = flatItems[ci];
6040    }
6041
6042    var cartBtns = grid.querySelectorAll(".sg-btn-cart");
6043    for (var cb = 0; cb < cartBtns.length; cb++) {
6044      cartBtns[cb].addEventListener("click", function() {
6045        addToCart(this);
6046      });
6047    }
6048
6049    var variantSelects = grid.querySelectorAll(".sg-variant-select:not(.sg-component-select)");
6050    for (var vs = 0; vs < variantSelects.length; vs++) {
6051      variantSelects[vs].addEventListener("change", function() {
6052        var sel = this;
6053        var vid = sel.value;
6054        var card = sel.closest(".sg-product-card");
6055        if (card && vid) {
6056          clearSizeNudge(sel);
6057          var cartBtn = card.querySelector(".sg-btn-cart");
6058          if (cartBtn) {
6059            cartBtn.setAttribute("data-variant-id", vid);
6060            cartBtn.removeAttribute("data-product-id");
6061          }
6062        }
6063      });
6064    }
6065
6066    var componentSelects = grid.querySelectorAll(".sg-component-select");
6067    for (var cs = 0; cs < componentSelects.length; cs++) {
6068      componentSelects[cs].addEventListener("change", function() {
6069        var sel = this;
6070        if (sel.value) sel.classList.remove("sg-needs-size");
6071        var picker = sel.closest(".sg-variant-picker");
6072        if (!picker) return;
6073        var card = picker.closest(".sg-product-card");
6074        if (!card) return;
6075
6076        var allSelects = picker.querySelectorAll(".sg-component-select");
6077        var selections = [];
6078        var allFilled = true;
6079        for (var ai = 0; ai < allSelects.length; ai++) {
6080          var s = allSelects[ai];
6081          // Skip advisory selects — they don't affect variant resolution
6082          if (s.getAttribute("data-advisory") === "true") continue;
6083          if (!s.value) { allFilled = false; continue; }
6084          var optIdx = parseInt(s.getAttribute("data-option-index") || "0", 10);
6085          if (optIdx < 0) continue; // advisory selector with invalid index
6086          selections.push({
6087            optionIndex: optIdx,
6088            splitIndex: s.hasAttribute("data-split-index") ? parseInt(s.getAttribute("data-split-index"), 10) : undefined,
6089            value: s.value
6090          });
6091        }
6092
6093        if (!allFilled) return;
6094
6095        var cartBtn = card.querySelector(".sg-btn-cart");
6096        if (!cartBtn) return;
6097        var cardIndex = card.getAttribute("data-testid").replace("card-product-", "");
6098        var itemData = card._sgItemData;
6099        if (!itemData || !itemData.variants) return;
6100
6101        var matched = findMatchingVariant(itemData.variants, selections);
6102        if (matched) {
6103          clearSizeNudge(sel);
6104          cartBtn.setAttribute("data-variant-id", matched.id);
6105          cartBtn.removeAttribute("data-product-id");
6106          if (!matched.available) {
6107            cartBtn.disabled = true;
6108            cartBtn.classList.add("sg-sold-out");
6109            cartBtn.textContent = "Sold Out";
6110          } else {
6111            cartBtn.disabled = false;
6112            cartBtn.classList.remove("sg-sold-out");
6113            var iconColor = getCartIconColor();
6114            cartBtn.innerHTML = cartIcon(iconColor) + cartLabel();
6115          }
6116        }
6117      });
6118    }
6119
6120    var addAllBtns = grid.querySelectorAll(".sg-btn-add-all");
6121    for (var ab = 0; ab < addAllBtns.length; ab++) {
6122      addAllBtns[ab].addEventListener("click", function() {
6123        addAllToCart(this);
6124      });
6125    }
6126
6127    // Reset scroll to top and refresh fade indicators
6128    var scrollWrap = document.getElementById("sg-products-scroll");
6129    if (scrollWrap) scrollWrap.scrollTop = 0;
6130    updateScrollFade();
6131  }
6132
6133  function addToCart(btn) {
6134    var variantId = btn.getAttribute("data-variant-id");
6135    var productId = btn.getAttribute("data-product-id");
6136
6137    if (btn.getAttribute("data-size-required") === "1" && !variantId) {
6138      showSizeNudge(btn);
6139      return;
6140    }
6141    if (!variantId && !productId) return;
6142
6143    btn.disabled = true;
6144    btn.innerHTML = '<span style="width:14px;height:14px;border:2px solid currentColor;border-top-color:transparent;border-radius:50%;display:inline-block;animation:sg-spin 0.6s linear infinite"></span> Adding...';
6145
6146    var styleEl = document.getElementById("sg-spin-style");
6147    if (!styleEl) {
6148      styleEl = document.createElement("style");
6149      styleEl.id = "sg-spin-style";
6150      styleEl.textContent = '@keyframes sg-spin { to { transform: rotate(360deg); } }';
6151      document.head.appendChild(styleEl);
6152    }
6153
6154    if (variantId) {
6155      doCartAdd(variantId, btn);
6156    } else {
6157      var handle = productId;
6158      var viewLink = btn.closest(".sg-product-card");
6159      if (viewLink) {
6160        var linkEl = viewLink.querySelector(".sg-btn-view");
6161        if (linkEl) {
6162          var href = linkEl.getAttribute("href") || "";
6163          var hMatch = href.match(/\/products\/([^?#]+)/);
6164          if (hMatch) handle = hMatch[1];
6165        }
6166      }
6167      var productUrl = "https://" + shopDomain + "/products/" + handle + ".json";
6168      var xhr = new XMLHttpRequest();
6169      xhr.open("GET", productUrl);
6170      xhr.onreadystatechange = function() {
6171        if (xhr.readyState === 4) {
6172          if (xhr.status === 200) {
6173            try {
6174              var data = JSON.parse(xhr.responseText);
6175              var vid = data.product && data.product.variants && data.product.variants[0]
6176                ? data.product.variants[0].id : null;
6177              if (vid) {
6178                doCartAdd(vid, btn);
6179              } else {
6180                cartError(btn);
6181              }
6182            } catch(e) { cartError(btn); }
6183          } else {
6184            cartError(btn);
6185          }
6186        }
6187      };
6188      xhr.send();
6189    }
6190  }
6191
6192  function getCartIconColor() {
6193    return isLightColor(config.primaryColor || "#C6A85B") ? "#111" : "#fff";
6194  }
6195
6196  function doCartAdd(variantId, btn) {
6197    var iconColor = getCartIconColor();
6198    var xhr = new XMLHttpRequest();
6199    xhr.open("POST", "/cart/add.js");
6200    xhr.setRequestHeader("Content-Type", "application/json");
6201    xhr.onreadystatechange = function() {
6202      if (xhr.readyState === 4) {
6203        if (xhr.status === 200) {
6204          markCartAttributed();
6205          btn.classList.add("sg-added");
6206          btn.innerHTML = checkIcon("#fff") + ' Added';
6207          btn.disabled = true;
6208          setTimeout(function() {
6209            btn.classList.remove("sg-added");
6210            btn.innerHTML = cartIcon(iconColor) + cartLabel();
6211            btn.disabled = false;
6212          }, 3000);
6213        } else {
6214          cartError(btn);
6215        }
6216      }
6217    };
6218    xhr.send(JSON.stringify({ id: variantId, quantity: 1 }));
6219  }
6220
6221  function cartError(btn) {
6222    var iconColor = getCartIconColor();
6223    btn.innerHTML = 'Unavailable';
6224    btn.disabled = true;
6225    setTimeout(function() {
6226      btn.innerHTML = cartIcon(iconColor) + cartLabel();
6227      btn.disabled = false;
6228    }, 2000);
6229  }
6230
6231  function showTyping() {
6232    var container = document.getElementById("sg-messages");
6233    if (!container) return;
6234    var el = document.createElement("div");
6235    el.className = "sg-msg sg-msg-assistant";
6236    el.id = "sg-typing";
6237    el.innerHTML = '<div class="sg-msg-bubble"><div class="sg-typing"><div class="sg-typing-dot"></div><div class="sg-typing-dot"></div><div class="sg-typing-dot"></div></div></div>';
6238    container.appendChild(el);
6239    container.scrollTop = container.scrollHeight;
6240  }
6241
6242  function hideTyping() {
6243    var el = document.getElementById("sg-typing");
6244    if (el) el.remove();
6245  }
6246
6247  // Tool-aware status: while the agent runs a tool, show what it's doing next
6248  // to the animated dots ("Looking through the collection…"). No-op once the
6249  // reply has started streaming and the typing indicator is gone.
6250  function setTypingStatus(label) {
6251    var el = document.getElementById("sg-typing");
6252    if (!el) return;
6253    var wrap = el.querySelector(".sg-typing");
6254    if (!wrap) return;
6255    var span = wrap.querySelector(".sg-typing-label");
6256    if (!label) {
6257      if (span) span.remove();
6258      return;
6259    }
6260    if (!span) {
6261      span = document.createElement("span");
6262      span.className = "sg-typing-label";
6263      wrap.appendChild(span);
6264    }
6265    span.textContent = label;
6266    var container = document.getElementById("sg-messages");
6267    if (container) container.scrollTop = container.scrollHeight;
6268  }
6269
6270  function rebuildWidget() {
6271    var existing = document.getElementById("sg-fab");
6272    if (existing) existing.remove();
6273    var existingOverlay = document.getElementById("sg-overlay");
6274    if (existingOverlay) existingOverlay.remove();
6275
6276    var container = document.getElementById("style-guru-container");
6277    if (container) {
6278      var shopAttr = container.getAttribute("data-shop");
6279      var apiAttr = container.getAttribute("data-api");
6280      if (shopAttr) shopDomain = shopAttr;
6281      if (apiAttr) API_BASE = apiAttr;
6282    }
6283
6284    applyContainerOverrides();
6285    injectStyles();
6286    hookSearchBox();
6287    createFAB();
6288  }
6289
6290  var _containerObserver = null;
6291
6292  function attachContainerObserver() {
6293    if (_containerObserver) {
6294      _containerObserver.disconnect();
6295      _containerObserver = null;
6296    }
6297    var container = document.getElementById("style-guru-container");
6298    if (!container) return;
6299
6300    _containerObserver = new MutationObserver(function(mutations) {
6301      var settingChanged = false;
6302      for (var i = 0; i < mutations.length; i++) {
6303        if (mutations[i].type === "attributes" && mutations[i].attributeName && mutations[i].attributeName.indexOf("data-") === 0) {
6304          var attr = mutations[i].attributeName;
6305          if (attr !== "data-shop" && attr !== "data-api") {
6306            settingChanged = true;
6307            break;
6308          }
6309        }
6310      }
6311      if (settingChanged) {
6312        rebuildWidget();
6313      }
6314    });
6315    _containerObserver.observe(container, { attributes: true });
6316  }
6317
6318  function setupThemeEditorListeners() {
6319    document.addEventListener("shopify:section:load", function() {
6320      var container = document.getElementById("style-guru-container");
6321      if (!container) return;
6322      rebuildWidget();
6323      attachContainerObserver();
6324    });
6325
6326    document.addEventListener("shopify:section:select", function() {
6327      var container = document.getElementById("style-guru-container");
6328      if (!container) return;
6329      rebuildWidget();
6330      attachContainerObserver();
6331    });
6332
6333    document.addEventListener("shopify:section:deselect", function() {
6334      if (_containerObserver) {
6335        _containerObserver.disconnect();
6336        _containerObserver = null;
6337      }
6338    });
6339
6340    attachContainerObserver();
6341  }
6342
6343  window.StyleGuru = {
6344    open: function(query) { openModal(query || ""); },
6345    close: function() { closeModal(); },
6346    toggle: function(query) {
6347      if (isModalOpen) { closeModal(); }
6348      else { openModal(query || ""); }
6349    }
6350  };
6351
6352  document.addEventListener("click", function(e) {
6353    var el = e.target;
6354    var trigger = null;
6355    while (el && el !== document) {
6356      if (el.hasAttribute && el.hasAttribute("data-styleguru-open")) {
6357        trigger = el;
6358        break;
6359      }
6360      el = el.parentElement;
6361    }
6362    if (trigger) {
6363      e.preventDefault();
6364      var query = trigger.getAttribute("data-styleguru-query") || "";
6365      openModal(query);
6366    }
6367  });
6368
6369  if (document.readyState === "loading") {
6370    document.addEventListener("DOMContentLoaded", function() {
6371      init();
6372      setupThemeEditorListeners();
6373    });
6374  } else {
6375    init();
6376    setupThemeEditorListeners();
6377  }
6378})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.